use rust_decimal::Decimal;
use uuid::Uuid;
use crate::infrastructure::persistence::NewTenderRow;
use super::pos_events::{PosEvent, PosTenderCompleted};
use super::pos_write_service::{legacy_company_echo, money, relay_ambient_scope, PosError, PosWriteService, TenderOutcome};
impl PosWriteService {
pub async fn add_tender(&self, pos_invoice_id: Uuid, method: &str, amount: Decimal, reference_no: Option<String>) -> Result<TenderOutcome, PosError> {
if amount <= Decimal::ZERO { return Err(PosError::NegativeAmount); }
let mut tx = self.rpool().begin().await?;
relay_ambient_scope(&mut tx).await?;
let hdr = self.invoices
.fetch_tender_header(&mut *tx, pos_invoice_id).await?
.ok_or(PosError::InvoiceNotFound(pos_invoice_id))?;
if hdr.status != "draft" { return Err(PosError::NotDraft); }
let rounded_total = hdr.rounded_total;
self.payments.insert_tender(&mut tx, &NewTenderRow {
id: Uuid::new_v4(),
pos_invoice_id,
client_uuid: None,
payment_method: method,
amount: money(amount),
reference_no: reference_no.as_deref(),
}).await?;
let paid_total = self.payments.sum_paid_total_on(&mut tx, pos_invoice_id).await?;
let change_due = if paid_total > rounded_total { paid_total - rounded_total } else { Decimal::ZERO };
self.invoices.update_tender_totals(&mut tx, pos_invoice_id, paid_total, change_due).await?;
let fully_tendered = paid_total >= rounded_total;
if fully_tendered && (paid_total - money(amount)) < rounded_total {
if let Some(schema) = &self.outbox_schema {
let rec = tender_completed_outbox_record(pos_invoice_id, legacy_company_echo(), chrono::Utc::now());
backbone_outbox::outbox::stage(&mut *tx, schema, &rec)
.await
.map_err(|e| PosError::Db(sqlx::Error::Protocol(e.to_string())))?;
}
}
tx.commit().await?;
if fully_tendered && (paid_total - money(amount)) < rounded_total {
self.sink.publish(PosEvent::PosTenderCompleted(PosTenderCompleted {
pos_invoice_id, company_id: legacy_company_echo(),
}));
}
Ok(TenderOutcome { paid_total, change_due, fully_tendered })
}
}
pub(super) fn tender_completed_outbox_record(
pos_invoice_id: Uuid,
company_id: Uuid,
now: chrono::DateTime<chrono::Utc>,
) -> backbone_outbox::OutboxRecord {
let payload = serde_json::json!({
"pos_invoice_id": pos_invoice_id,
"company_id": company_id,
});
backbone_outbox::OutboxRecord::new(
"PosTenderCompleted",
"PosSale",
pos_invoice_id.to_string(),
company_id,
payload,
now,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tender_completed_record_is_fenced_by_company() {
let company = Uuid::new_v4();
let ticket = Uuid::new_v4();
let rec = tender_completed_outbox_record(ticket, company, chrono::Utc::now());
assert_eq!(
rec.company_id, company,
"PosTenderCompleted outbox record must carry the owning tenant (ADR-0011 fence)"
);
assert_eq!(rec.event_type, "PosTenderCompleted");
assert_eq!(rec.aggregate_type, "PosSale");
assert_eq!(rec.aggregate_id, ticket.to_string());
}
}