backbone_pos/application/service/
pos_sale.rs1use rust_decimal::Decimal;
18use uuid::Uuid;
19
20use crate::infrastructure::persistence::NewDraftInvoiceRow;
21
22use super::pos_compute::ComputeLineIn;
23use super::pos_ports::PosTaxComputePort;
24use super::pos_write_service::{
25 legacy_company_echo, map_invoice_dup, relay_ambient_scope, PosError, PosWriteService, NewCartSale, NewSale,
26 NewSaleLine,
27};
28
29impl PosWriteService {
30 pub async fn ring_sale(&self, sale: NewSale, tax: &dyn PosTaxComputePort) -> Result<Uuid, PosError> {
35 if sale.lines.is_empty() {
36 return Err(PosError::EmptyDocument);
37 }
38 let mut tx = self.db_pool.begin().await?;
39 relay_ambient_scope(&mut tx).await?;
40 let st = self.openings.fetch_status_on(&mut *tx, sale.opening_entry_id).await?;
43 if st.as_deref() != Some("open") {
44 return Err(PosError::SessionNotOpen);
45 }
46 let session_profile = self
49 .openings
50 .fetch_profile_id_on(&mut *tx, sale.opening_entry_id)
51 .await?
52 .ok_or(PosError::SessionNotFound(sale.opening_entry_id))?;
53 if session_profile != sale.pos_profile_id {
54 return Err(PosError::SessionRegisterMismatch);
55 }
56
57 if let Some(table) = sale.pos_table_id {
62 if self.tables.fetch_table(&mut *tx, table).await?.is_none() {
63 return Err(PosError::TableNotFound(table));
64 }
65 if let Some(occupant) = self.invoices.find_draft_on_table(&mut *tx, table, None).await? {
66 return Err(PosError::TableOccupied { pos_table_id: table, draft_invoice_id: occupant });
67 }
68 }
69
70 let order_discount = self
74 .resolve_order_discount(&mut tx, sale.pos_profile_id, sale.discount_id)
75 .await?;
76
77 let mut compute_lines: Vec<ComputeLineIn> = sale
80 .lines
81 .iter()
82 .map(|l| ComputeLineIn {
83 item_id: l.item_id,
84 revenue_account_id: l.revenue_account_id,
85 description: l.description.clone(),
86 course: l.course,
87 quantity: l.quantity,
88 unit_price: l.unit_price,
89 discount_amount: l.discount_amount,
90 })
91 .collect();
92 if let Some(d) = &order_discount {
93 Self::fold_order_discount(&mut compute_lines, d.percentage)?;
94 }
95 let computed = self
96 .compute_ticket(
97 &mut *tx,
98 sale.pos_profile_id,
99 sale.posting_at.date(),
100 super::pos_ports::PosTaxDocumentType::Invoice,
101 compute_lines,
102 tax,
103 )
104 .await?;
105 let id = Uuid::new_v4();
106 let r = self.invoices.insert_draft(&mut tx, &NewDraftInvoiceRow {
107 id,
108 client_uuid: None,
109 pos_profile_id: sale.pos_profile_id,
110 opening_entry_id: sale.opening_entry_id,
111 branch_id: sale.branch_id,
112 customer_id: sale.customer_id,
113 pos_table_id: sale.pos_table_id,
114 receipt_number: &sale.receipt_number,
115 posting_at: sale.posting_at,
116 net_total: computed.net_total,
117 tax_total: computed.tax_total,
118 grand_total: computed.grand_total,
119 rounding_adjustment: computed.rounding_adjustment,
120 rounded_total: computed.rounded_total,
121 }).await;
122 if let Err(e) = r {
123 return Err(map_invoice_dup(
124 e, &sale.receipt_number, None, sale.pos_table_id,
125 &self.invoices, &self.db_pool, None,
126 ).await);
127 }
128 for l in &computed.lines {
129 self.items.insert_line(&mut tx, &crate::infrastructure::persistence::NewInvoiceItemRow {
130 id: Uuid::new_v4(),
131 pos_invoice_id: id,
132 client_uuid: None,
133 item_id: l.item_id,
134 description: l.description.as_deref(),
135 course: l.course,
136 quantity: l.quantity,
137 unit_price: l.unit_price,
138 discount_amount: l.discount_amount,
139 net_amount: l.net_amount,
140 revenue_account_id: l.revenue_account_id,
141 }).await?;
142 }
143 tx.commit().await?;
144 Ok(id)
145 }
146
147 pub async fn ring_sale_priced(
153 &self,
154 o: NewCartSale,
155 pricing: &dyn crate::application::service::pos_cart_pricing::CartPricingPort,
156 tax: &dyn PosTaxComputePort,
157 ) -> Result<Uuid, PosError> {
158 use crate::application::service::pos_cart_pricing::{CartPriceLine, CartPriceRequest};
159 if o.lines.is_empty() {
160 return Err(PosError::EmptyDocument);
161 }
162 let refs: Vec<Uuid> = o.lines.iter().map(|_| Uuid::new_v4()).collect();
163 let req = CartPriceRequest {
164 customer_id: o.customer_id,
165 customer_group_id: o.customer_group_id,
166 coupon_code: o.coupon_code.clone(),
167 lines: o
168 .lines
169 .iter()
170 .zip(&refs)
171 .map(|(l, r)| CartPriceLine {
172 line_ref: *r,
173 item_id: l.item_id,
174 item_group_id: l.item_group_id,
175 brand_id: l.brand_id,
176 list_price: l.list_price,
177 quantity: l.quantity,
178 })
179 .collect(),
180 };
181 let priced = pricing
182 .price_cart(&req)
183 .await
184 .map_err(|e| PosError::PricingRejected { code: e.code, message: e.message })?;
185
186 let mut lines = Vec::with_capacity(o.lines.len());
187 for (l, r) in o.lines.iter().zip(&refs) {
188 let pl = priced
189 .lines
190 .iter()
191 .find(|p| p.line_ref == *r)
192 .ok_or_else(|| PosError::PricingRejected {
193 code: "pricing_line_missing".into(),
194 message: "pricer omitted a line".into(),
195 })?;
196 let gross = super::pos_write_service::money(pl.unit_price * l.quantity);
197 let discount_amount = (gross - pl.net_line_total).max(Decimal::ZERO);
198 lines.push(NewSaleLine {
199 item_id: l.item_id,
200 revenue_account_id: l.revenue_account_id,
201 description: l.description.clone(),
202 course: l.course,
203 quantity: l.quantity,
204 unit_price: pl.unit_price,
205 discount_amount,
206 });
207 }
208 for rl in &priced.reward_lines {
210 lines.push(NewSaleLine {
211 item_id: rl.item_id,
212 revenue_account_id: None,
213 description: Some("promo reward (free)".into()),
214 course: None,
215 quantity: rl.quantity,
216 unit_price: Decimal::ZERO,
217 discount_amount: Decimal::ZERO,
218 });
219 }
220
221 self.ring_sale(NewSale {
222 pos_profile_id: o.pos_profile_id,
223 opening_entry_id: o.opening_entry_id,
224 branch_id: o.branch_id,
225 customer_id: o.customer_id,
226 pos_table_id: o.pos_table_id,
227 discount_id: o.discount_id,
228 receipt_number: o.receipt_number,
229 posting_at: o.posting_at,
230 lines,
231 }, tax)
232 .await
233 }
234}