app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

activation.rs (7122B)


      1 use radroots_app_view::{
      2     AccountSurfaceActivationProjection, ActiveSurface, FarmId, FarmerActivationProjection,
      3     SelectedSurfaceProjection,
      4 };
      5 use rusqlite::{Connection, OptionalExtension, params};
      6 
      7 use crate::AppSqliteError;
      8 
      9 pub struct AppActivationRepository<'a> {
     10     connection: &'a Connection,
     11 }
     12 
     13 impl<'a> AppActivationRepository<'a> {
     14     pub const fn new(connection: &'a Connection) -> Self {
     15         Self { connection }
     16     }
     17 
     18     pub fn load_surface_activation(
     19         &self,
     20         account_id: &str,
     21     ) -> Result<Option<AccountSurfaceActivationProjection>, AppSqliteError> {
     22         let row = self
     23             .connection
     24             .query_row(
     25                 "SELECT account_id, selected_surface, farmer_farm_id
     26                  FROM account_surface_activations
     27                  WHERE account_id = ?1
     28                  LIMIT 1",
     29                 [account_id],
     30                 |row| {
     31                     Ok((
     32                         row.get::<_, String>(0)?,
     33                         row.get::<_, String>(1)?,
     34                         row.get::<_, Option<String>>(2)?,
     35                     ))
     36                 },
     37             )
     38             .optional()
     39             .map_err(|source| AppSqliteError::Query {
     40                 operation: "load account surface activation",
     41                 source,
     42             })?;
     43 
     44         row.map(|(account_id, selected_surface, farmer_farm_id)| {
     45             Ok(AccountSurfaceActivationProjection::new(
     46                 account_id,
     47                 SelectedSurfaceProjection::new(parse_active_surface(
     48                     "account_surface_activations.selected_surface",
     49                     selected_surface,
     50                 )?),
     51                 FarmerActivationProjection {
     52                     farm_id: parse_optional_farm_id(
     53                         "account_surface_activations.farmer_farm_id",
     54                         farmer_farm_id,
     55                     )?,
     56                 },
     57             ))
     58         })
     59         .transpose()
     60     }
     61 
     62     pub fn save_surface_activation(
     63         &self,
     64         projection: &AccountSurfaceActivationProjection,
     65     ) -> Result<(), AppSqliteError> {
     66         self.connection
     67             .execute(
     68                 "INSERT INTO account_surface_activations (
     69                     account_id,
     70                     selected_surface,
     71                     farmer_farm_id,
     72                     updated_at
     73                 ) VALUES (?1, ?2, ?3, strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
     74                 ON CONFLICT(account_id) DO UPDATE SET
     75                     selected_surface = excluded.selected_surface,
     76                     farmer_farm_id = excluded.farmer_farm_id,
     77                     updated_at = excluded.updated_at",
     78                 params![
     79                     projection.account_id,
     80                     projection.active_surface().storage_key(),
     81                     projection
     82                         .farmer_activation
     83                         .farm_id
     84                         .map(|farm_id| farm_id.to_string()),
     85                 ],
     86             )
     87             .map_err(|source| AppSqliteError::Query {
     88                 operation: "save account surface activation",
     89                 source,
     90             })?;
     91 
     92         Ok(())
     93     }
     94 
     95     pub fn clear_surface_activation(&self, account_id: &str) -> Result<(), AppSqliteError> {
     96         self.connection
     97             .execute(
     98                 "DELETE FROM account_surface_activations WHERE account_id = ?1",
     99                 [account_id],
    100             )
    101             .map_err(|source| AppSqliteError::Query {
    102                 operation: "clear account surface activation",
    103                 source,
    104             })?;
    105 
    106         Ok(())
    107     }
    108 }
    109 
    110 fn parse_active_surface(
    111     field: &'static str,
    112     value: String,
    113 ) -> Result<ActiveSurface, AppSqliteError> {
    114     match value.as_str() {
    115         "personal" => Ok(ActiveSurface::Personal),
    116         "farmer" => Ok(ActiveSurface::Farmer),
    117         _ => Err(AppSqliteError::DecodeEnum { field, value }),
    118     }
    119 }
    120 
    121 fn parse_optional_farm_id(
    122     field: &'static str,
    123     value: Option<String>,
    124 ) -> Result<Option<FarmId>, AppSqliteError> {
    125     value
    126         .map(|value| {
    127             value
    128                 .parse()
    129                 .map_err(|_| AppSqliteError::DecodeId { field, value })
    130         })
    131         .transpose()
    132 }
    133 
    134 #[cfg(test)]
    135 mod tests {
    136     use radroots_app_view::{
    137         AccountSurfaceActivationProjection, ActiveSurface, FarmId, FarmerActivationProjection,
    138         SelectedSurfaceProjection,
    139     };
    140 
    141     use crate::{AppSqliteStore, DatabaseTarget};
    142 
    143     #[test]
    144     fn load_surface_activation_returns_none_for_unknown_account() {
    145         let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
    146 
    147         let projection = store
    148             .load_surface_activation("acct_missing")
    149             .expect("missing activation should load");
    150 
    151         assert_eq!(projection, None);
    152     }
    153 
    154     #[test]
    155     fn surface_activation_round_trips_farmer_binding() {
    156         let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
    157         let projection = AccountSurfaceActivationProjection::new(
    158             "acct_farmer",
    159             SelectedSurfaceProjection::new(ActiveSurface::Farmer),
    160             FarmerActivationProjection::active(FarmId::new()),
    161         );
    162 
    163         store
    164             .save_surface_activation(&projection)
    165             .expect("surface activation should save");
    166 
    167         let loaded = store
    168             .load_surface_activation("acct_farmer")
    169             .expect("surface activation should load")
    170             .expect("surface activation should exist");
    171 
    172         assert_eq!(loaded, projection);
    173     }
    174 
    175     #[test]
    176     fn surface_activation_upsert_and_clear_are_explicit() {
    177         let store = AppSqliteStore::open(DatabaseTarget::InMemory).expect("store should open");
    178         let first = AccountSurfaceActivationProjection::new(
    179             "acct_surface",
    180             SelectedSurfaceProjection::new(ActiveSurface::Farmer),
    181             FarmerActivationProjection::active(FarmId::new()),
    182         );
    183         let second = AccountSurfaceActivationProjection::new(
    184             "acct_surface",
    185             SelectedSurfaceProjection::new(ActiveSurface::Farmer),
    186             FarmerActivationProjection::inactive(),
    187         );
    188 
    189         store
    190             .save_surface_activation(&first)
    191             .expect("initial surface activation should save");
    192         store
    193             .save_surface_activation(&second)
    194             .expect("updated surface activation should save");
    195 
    196         let loaded = store
    197             .load_surface_activation("acct_surface")
    198             .expect("updated surface activation should load")
    199             .expect("updated surface activation should exist");
    200         assert_eq!(loaded.active_surface(), ActiveSurface::Personal);
    201         assert_eq!(loaded, second);
    202 
    203         store
    204             .clear_surface_activation("acct_surface")
    205             .expect("surface activation should clear");
    206         assert_eq!(
    207             store
    208                 .load_surface_activation("acct_surface")
    209                 .expect("cleared surface activation should load"),
    210             None
    211         );
    212     }
    213 }