Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
DROP INDEX IF EXISTS idx_inflation_rates_country_year;
DROP TABLE IF EXISTS inflation_rates;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CREATE TABLE inflation_rates (
id TEXT PRIMARY KEY NOT NULL,
country_code TEXT NOT NULL,
year INTEGER NOT NULL,
rate NUMERIC NOT NULL,
reference_date TEXT,
data_source TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(country_code, year)
);

CREATE INDEX idx_inflation_rates_country_year ON inflation_rates(country_code, year);
69 changes: 69 additions & 0 deletions src-core/src/inflation/inflation_model.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use chrono::NaiveDateTime;
use diesel::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Queryable, Insertable, Identifiable, Serialize, Deserialize, Debug, Clone)]
#[diesel(table_name = crate::schema::inflation_rates)]
#[serde(rename_all = "camelCase")]
pub struct InflationRate {
pub id: String,
pub country_code: String,
pub year: i32,
pub rate: f64,
pub reference_date: Option<String>,
pub data_source: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
}

#[derive(Insertable, AsChangeset, Serialize, Deserialize, Debug, Clone)]
#[diesel(table_name = crate::schema::inflation_rates)]
#[serde(rename_all = "camelCase")]
pub struct NewInflationRate {
pub id: Option<String>,
pub country_code: String,
pub year: i32,
pub rate: f64,
pub reference_date: Option<String>,
pub data_source: String,
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct InflationAdjustedValue {
pub year: i32,
pub nominal_value: f64,
pub real_value: f64,
pub inflation_rate: Option<f64>,
pub cumulative_inflation: f64,
pub reference_date: String,
}

// World Bank API response structures
// These fields are used for JSON deserialization even though they're not directly accessed
#[derive(Deserialize, Debug)]
#[allow(dead_code)]
pub struct WorldBankResponse(pub WorldBankMeta, pub Option<Vec<WorldBankDataPoint>>);

#[derive(Deserialize, Debug)]
#[allow(dead_code)]
pub struct WorldBankMeta {
pub page: i32,
pub pages: i32,
pub total: i32,
}

#[derive(Deserialize, Debug)]
#[allow(dead_code)]
pub struct WorldBankDataPoint {
pub date: String,
pub value: Option<f64>,
pub country: WorldBankCountry,
}

#[derive(Deserialize, Debug)]
#[allow(dead_code)]
pub struct WorldBankCountry {
pub id: String,
pub value: String,
}
201 changes: 201 additions & 0 deletions src-core/src/inflation/inflation_repository.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
use async_trait::async_trait;
use diesel::prelude::*;
use diesel::r2d2::{ConnectionManager, Pool};
use diesel::sqlite::SqliteConnection;
use std::sync::Arc;
use uuid::Uuid;

use super::inflation_model::{InflationRate, NewInflationRate};
use super::inflation_traits::InflationRateRepositoryTrait;
use crate::db::{get_connection, WriteHandle};
use crate::errors::{Error, Result};
use crate::schema::inflation_rates;

pub struct InflationRateRepository {
pool: Arc<Pool<ConnectionManager<SqliteConnection>>>,
writer: WriteHandle,
}

impl InflationRateRepository {
pub fn new(pool: Arc<Pool<ConnectionManager<SqliteConnection>>>, writer: WriteHandle) -> Self {
InflationRateRepository { pool, writer }
}

fn get_inflation_rate_impl(&self, id_param: &str) -> Result<InflationRate> {
let mut conn = get_connection(&self.pool)?;
inflation_rates::table
.find(id_param)
.first(&mut conn)
.map_err(Error::from)
}

fn get_inflation_rates_impl(&self) -> Result<Vec<InflationRate>> {
let mut conn = get_connection(&self.pool)?;
inflation_rates::table
.order(inflation_rates::year.desc())
.load(&mut conn)
.map_err(Error::from)
}

fn get_inflation_rates_by_country_impl(&self, country_code: &str) -> Result<Vec<InflationRate>> {
let mut conn = get_connection(&self.pool)?;
inflation_rates::table
.filter(inflation_rates::country_code.eq(country_code.to_uppercase()))
.order(inflation_rates::year.desc())
.load(&mut conn)
.map_err(Error::from)
}

fn get_inflation_rate_for_year_impl(
&self,
country_code: &str,
year: i32,
) -> Result<Option<InflationRate>> {
let mut conn = get_connection(&self.pool)?;
inflation_rates::table
.filter(inflation_rates::country_code.eq(country_code.to_uppercase()))
.filter(inflation_rates::year.eq(year))
.first(&mut conn)
.optional()
.map_err(Error::from)
}
}

#[async_trait]
impl InflationRateRepositoryTrait for InflationRateRepository {
fn get_inflation_rate(&self, id_param: &str) -> Result<InflationRate> {
self.get_inflation_rate_impl(id_param)
}

fn get_inflation_rates(&self) -> Result<Vec<InflationRate>> {
self.get_inflation_rates_impl()
}

fn get_inflation_rates_by_country(&self, country_code: &str) -> Result<Vec<InflationRate>> {
self.get_inflation_rates_by_country_impl(country_code)
}

fn get_inflation_rate_for_year(
&self,
country_code: &str,
year: i32,
) -> Result<Option<InflationRate>> {
self.get_inflation_rate_for_year_impl(country_code, year)
}

async fn create_inflation_rate(&self, new_rate: NewInflationRate) -> Result<InflationRate> {
let new_rate_owned = new_rate.clone();

self.writer
.exec(
move |conn: &mut SqliteConnection| -> Result<InflationRate> {
let new_rate_record = (
inflation_rates::id.eq(Uuid::new_v4().to_string()),
inflation_rates::country_code.eq(new_rate_owned.country_code.to_uppercase()),
inflation_rates::year.eq(new_rate_owned.year),
inflation_rates::rate.eq(new_rate_owned.rate),
inflation_rates::reference_date.eq(new_rate_owned.reference_date),
inflation_rates::data_source.eq(new_rate_owned.data_source),
inflation_rates::created_at.eq(chrono::Utc::now().naive_utc()),
inflation_rates::updated_at.eq(chrono::Utc::now().naive_utc()),
);

diesel::insert_into(inflation_rates::table)
.values(new_rate_record)
.get_result(conn)
.map_err(Error::from)
},
)
.await
}

async fn update_inflation_rate(
&self,
id_param: &str,
updated_rate: NewInflationRate,
) -> Result<InflationRate> {
let id_owned = id_param.to_string();
let updated_rate_owned = updated_rate.clone();

self.writer
.exec(
move |conn: &mut SqliteConnection| -> Result<InflationRate> {
let target = inflation_rates::table.find(id_owned);
diesel::update(target)
.set((
inflation_rates::country_code
.eq(updated_rate_owned.country_code.to_uppercase()),
inflation_rates::year.eq(updated_rate_owned.year),
inflation_rates::rate.eq(updated_rate_owned.rate),
inflation_rates::reference_date.eq(updated_rate_owned.reference_date),
inflation_rates::data_source.eq(updated_rate_owned.data_source),
inflation_rates::updated_at.eq(chrono::Utc::now().naive_utc()),
))
.get_result(conn)
.map_err(Error::from)
},
)
.await
}

async fn delete_inflation_rate(&self, id_param: &str) -> Result<()> {
let id_owned = id_param.to_string();
self.writer
.exec(move |conn: &mut SqliteConnection| -> Result<()> {
diesel::delete(inflation_rates::table.find(id_owned))
.execute(conn)
.map_err(Error::from)
.map(|_| ())
})
.await
}

async fn upsert_inflation_rate(&self, rate: NewInflationRate) -> Result<InflationRate> {
let rate_owned = rate.clone();
let country_code_upper = rate_owned.country_code.to_uppercase();

self.writer
.exec(
move |conn: &mut SqliteConnection| -> Result<InflationRate> {
// Check if exists
let existing: Option<InflationRate> = inflation_rates::table
.filter(inflation_rates::country_code.eq(&country_code_upper))
.filter(inflation_rates::year.eq(rate_owned.year))
.first(conn)
.optional()
.map_err(Error::from)?;

if let Some(existing_rate) = existing {
// Update
diesel::update(inflation_rates::table.find(&existing_rate.id))
.set((
inflation_rates::rate.eq(rate_owned.rate),
inflation_rates::reference_date.eq(&rate_owned.reference_date),
inflation_rates::data_source.eq(&rate_owned.data_source),
inflation_rates::updated_at.eq(chrono::Utc::now().naive_utc()),
))
.get_result(conn)
.map_err(Error::from)
} else {
// Insert
let new_rate_record = (
inflation_rates::id.eq(Uuid::new_v4().to_string()),
inflation_rates::country_code.eq(&country_code_upper),
inflation_rates::year.eq(rate_owned.year),
inflation_rates::rate.eq(rate_owned.rate),
inflation_rates::reference_date.eq(&rate_owned.reference_date),
inflation_rates::data_source.eq(&rate_owned.data_source),
inflation_rates::created_at.eq(chrono::Utc::now().naive_utc()),
inflation_rates::updated_at.eq(chrono::Utc::now().naive_utc()),
);

diesel::insert_into(inflation_rates::table)
.values(new_rate_record)
.get_result(conn)
.map_err(Error::from)
}
},
)
.await
}
}
Loading