use rust_decimal::Decimal;
use uuid::Uuid;
use crate::infrastructure::persistence::DiscountRow;
use super::pos_compute::ComputeLineIn;
use super::pos_write_service::{money, PosError, PosWriteService};
impl PosWriteService {
pub(super) async fn resolve_order_discount(
&self,
conn: &mut sqlx::PgConnection,
pos_profile_id: Uuid,
discount_id: Option<Uuid>,
) -> Result<Option<DiscountRow>, PosError> {
let Some(discount_id) = discount_id else { return Ok(None) };
let cfg = self
.profiles
.fetch_tax_config(&mut *conn, pos_profile_id)
.await?
.ok_or(PosError::ProfileNotFound(pos_profile_id))?;
if !cfg.allow_discount {
return Err(PosError::DiscountNotAllowed);
}
let d = self
.discounts
.fetch_discount(&mut *conn, discount_id)
.await?
.ok_or(PosError::DiscountNotFound(discount_id))?;
if d.percentage < Decimal::ZERO || d.percentage > Decimal::ONE {
return Err(PosError::DiscountInvalid("percentage must be between 0 and 1"));
}
Ok(Some(d))
}
pub(super) fn fold_order_discount(lines: &mut [ComputeLineIn], pct: Decimal) -> Result<Decimal, PosError> {
let mut folded = Decimal::ZERO;
for l in lines.iter_mut() {
let gross = l.quantity * l.unit_price;
let share = money(gross * pct);
let new_discount = l.discount_amount + share;
if new_discount > gross {
return Err(PosError::DiscountInvalid("combined discounts exceed line gross"));
}
l.discount_amount = new_discount;
folded += share;
}
Ok(folded)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn line(q: i64, price: i64, disc: i64) -> ComputeLineIn {
ComputeLineIn {
item_id: Uuid::new_v4(),
revenue_account_id: None,
description: None,
course: None,
quantity: Decimal::from(q),
unit_price: Decimal::from(price),
discount_amount: Decimal::from(disc),
}
}
#[test]
fn folds_pro_rata_by_line_gross() {
let mut lines = vec![line(2, 25_000, 0), line(1, 50_000, 0)];
let folded = PosWriteService::fold_order_discount(&mut lines, Decimal::new(1, 1)).unwrap();
assert_eq!(folded, Decimal::from(10_000));
assert_eq!(lines[0].discount_amount, Decimal::from(5_000));
assert_eq!(lines[1].discount_amount, Decimal::from(5_000));
}
#[test]
fn folds_onto_existing_line_discount() {
let mut lines = vec![line(1, 100, 10)];
let folded = PosWriteService::fold_order_discount(&mut lines, Decimal::new(125, 3)).unwrap();
assert_eq!(folded, Decimal::new(125, 1)); assert_eq!(lines[0].discount_amount, Decimal::new(225, 1)); }
#[test]
fn refuses_when_combined_discounts_exceed_gross() {
let mut lines = vec![line(1, 100, 90)];
assert!(matches!(
PosWriteService::fold_order_discount(&mut lines, Decimal::new(2, 1)), Err(PosError::DiscountInvalid(_))
));
}
#[test]
fn rounds_each_line_share_to_money() {
let mut lines = vec![line(3, 33, 0)]; let folded = PosWriteService::fold_order_discount(&mut lines, Decimal::new(1, 1)).unwrap();
assert_eq!(folded, Decimal::new(990, 2));
assert_eq!(lines[0].discount_amount, Decimal::new(990, 2));
}
}