// rocie - An enterprise grocery management system // // Copyright (C) 2026 Benedikt Peetz // SPDX-License-Identifier: GPL-3.0-or-later // // This file is part of Rocie. // // You should have received a copy of the License along with this program. // If not, see . use std::str::FromStr; use serde::{Deserialize, Serialize}; use sqlx::query; use uuid::Uuid; use crate::{ app::App, storage::{ migrate::get_current_date, sql::{ barcode::{Barcode, BarcodeId}, insert::{Operations, Transactionable}, unit::{Unit, UnitAmount}, }, }, }; #[derive(Debug, Deserialize, Serialize)] pub(crate) enum Operation { Buy { buy_id: Uuid, id: BarcodeId, }, Consume { buy_id: Uuid, id: BarcodeId, amount: UnitAmount, }, } impl Transactionable for Operation { type ApplyError = apply::Error; type UndoError = undo::Error; async fn apply(self, txn: &mut sqlx::SqliteConnection) -> Result<(), apply::Error> { match self { Operation::Buy { buy_id, id } => { let id = id.to_db(); let buy_id = buy_id.to_string(); let timestamp = get_current_date(); query!( " INSERT INTO buys (buy_id, barcode_id, timestamp) VALUES (?, ?, ?) ", buy_id, id, timestamp, ) .execute(txn) .await?; } Operation::Consume { buy_id, id, amount } => { let id = id.to_db(); let buy_id = buy_id.to_string(); let old_amount = { let record = query!( " SELECT used_amount FROM buys WHERE buy_id = ?; ", buy_id ) .fetch_one(&mut *txn) .await?; u32::try_from(record.used_amount.unwrap_or(0)) .expect("Should be strictly positive") }; // TODO: Check, that this does not overflow the maximum amount. <2025-09-21> let new_amount = amount.value + old_amount; // TODO(@bpeetz): We need to add the amount. <2025-09-09> query!( " UPDATE buys SET used_amount = ? WHERE barcode_id = ? AND buy_id = ? ", new_amount, id, buy_id ) .execute(txn) .await?; } } Ok(()) } async fn undo(self, txn: &mut sqlx::SqliteConnection) -> Result<(), undo::Error> { match self { Operation::Buy { buy_id, id } => { let id = id.to_db(); let buy_id = buy_id.to_string(); query!( " DELETE FROM buys WHERE barcode_id = ? AND buy_id = ? ", id, buy_id ) .execute(txn) .await?; } Operation::Consume { buy_id, id, amount } => { todo!("We would need to subtract the amount."); } } Ok(()) } } pub(crate) mod undo { #[derive(thiserror::Error, Debug)] pub(crate) enum Error { #[error("Failed to execute undo sql statments: {0}")] SqlError(#[from] sqlx::Error), } } pub(crate) mod apply { #[derive(thiserror::Error, Debug)] pub(crate) enum Error { #[error("Failed to execute apply sql statments: {0}")] SqlError(#[from] sqlx::Error), } } impl Barcode { pub(crate) fn buy(&self, ops: &mut Operations) { let id = Uuid::new_v4(); ops.push(Operation::Buy { buy_id: id, id: self.id, }); } pub(crate) async fn consume( &self, app: &App, amount: UnitAmount, ops: &mut Operations, ) -> Result<(), consume::Error> { assert_eq!( self.amount.unit, amount.unit, "We currently do not support unit conversions yet" ); if amount.value > self.amount.value { let foreign_amount_unit = Unit::from_id(app, amount.unit).await?; if let Some(foreign_amount_unit) = foreign_amount_unit { let self_amount_unit = Unit::from_id(app, self.amount.unit) .await? .expect("This unit id should always exist"); return Err(consume::Error::ConsumedMoreThanAvailable { consumed: Box::new((amount, foreign_amount_unit)), available: Box::new((self.amount, self_amount_unit)), }); } return Err(consume::Error::UnitIdDoesNotExist(amount.unit)); } let barcode_id = self.id.to_db(); let buy_id = { let record = query!( " SELECT buy_id FROM buys WHERE barcode_id = ? AND (used_amount IS NULL OR used_amount < ?) ORDER BY timestamp DESC LIMIT 1; ", barcode_id, self.amount.value ) .fetch_optional(&app.db) .await?; if let Some(found) = record { Uuid::from_str(&found.buy_id).expect("Was a uuid, should still be one") } else { return Err(consume::Error::NoMoreAvailable); } }; ops.push(Operation::Consume { id: self.id, amount, buy_id, }); Ok(()) } } pub(crate) mod consume { use actix_web::ResponseError; use crate::storage::{ self, sql::unit::{Unit, UnitAmount, UnitId}, }; #[derive(thiserror::Error, Debug)] pub(crate) enum Error { #[error("Failed to execute apply sql statments: {0}")] Sql(#[from] sqlx::Error), #[error("Failed to fetch an unit from a specified amount id value")] UnitGet(#[from] storage::sql::get::unit::from_id::Error), #[error("The specified unit-id does not exist: {0}")] UnitIdDoesNotExist(UnitId), #[error("No more of this product is available, buy more. ")] NoMoreAvailable, #[error( "Consumed more than available: consumed {} {}, but available: {} {}", consumed.0.value, consumed.1.short_name, available.0.value, available.1.short_name, )] ConsumedMoreThanAvailable { consumed: Box<(UnitAmount, Unit)>, available: Box<(UnitAmount, Unit)>, }, } impl ResponseError for Error {} }