1use anyhow::Result;
15use rust_decimal::Decimal;
16use sqlx::{PgPool, Row};
17use uuid::Uuid;
18
19use backbone_orm::org_scope;
20use backbone_orm::company_scope::{fetch_one_scalar_scoped, fetch_optional_scalar_scoped};
25use backbone_orm::org_scope::fetch_one_row_scoped;
26
27use crate::domain::entity::PosInvoice;
28
29pub const TABLE_NAME: &str = "pos.pos_invoices";
31
32pub struct PosInvoiceRepository(
37 backbone_orm::GenericCrudRepository<PosInvoice, backbone_orm::SoftDelete>,
38);
39
40impl std::ops::Deref for PosInvoiceRepository {
41 type Target = backbone_orm::GenericCrudRepository<PosInvoice, backbone_orm::SoftDelete>;
42 fn deref(&self) -> &Self::Target { &self.0 }
43}
44
45impl PosInvoiceRepository {
46 pub fn new(pool: PgPool) -> Self {
48 Self(backbone_orm::GenericCrudRepository::new(pool, TABLE_NAME))
49 }
50}
51
52pub struct NewDraftInvoiceRow<'a> {
58 pub id: Uuid,
59 pub client_uuid: Option<Uuid>,
60 pub pos_profile_id: Uuid,
61 pub opening_entry_id: Uuid,
62 pub branch_id: Option<Uuid>,
63 pub customer_id: Option<Uuid>,
64 pub pos_table_id: Option<Uuid>,
65 pub receipt_number: &'a str,
66 pub posting_at: chrono::NaiveDateTime,
67 pub net_total: Decimal,
68 pub tax_total: Decimal,
69 pub grand_total: Decimal,
70 pub rounding_adjustment: Decimal,
71 pub rounded_total: Decimal,
72}
73
74pub struct SyncLookupRow {
78 pub id: Uuid,
79 pub status: String,
80 pub pos_profile_id: Uuid,
81 pub opening_entry_id: Uuid,
82 pub branch_id: Option<Uuid>,
83 pub customer_id: Option<Uuid>,
84 pub pos_table_id: Option<Uuid>,
86 pub is_return: bool,
87 pub return_against: Option<Uuid>,
89 pub net_total: Decimal,
90 pub tax_total: Decimal,
91 pub grand_total: Decimal,
92 pub rounding_adjustment: Decimal,
93 pub rounded_total: Decimal,
94 pub paid_total: Decimal,
95 pub change_due: Decimal,
96}
97
98pub struct NewReturnInvoiceRow {
102 pub id: Uuid,
103 pub pos_profile_id: Uuid,
104 pub opening_entry_id: Uuid,
105 pub branch_id: Option<Uuid>,
106 pub customer_id: Option<Uuid>,
107 pub receipt_number: String,
108 pub posting_at: chrono::DateTime<chrono::Utc>,
109 pub net_total: Decimal,
110 pub tax_total: Decimal,
111 pub grand_total: Decimal,
112 pub rounded_total: Decimal,
113 pub billing_invoice_id: Uuid,
114 pub return_against: Uuid,
115}
116
117pub struct TenderHeaderRow {
119 pub status: String,
120 pub rounded_total: Decimal,
121}
122
123pub struct PaidStateRow {
125 pub status: String,
126 pub billing_invoice_id: Option<Uuid>,
127 pub payment_entry_id: Option<Uuid>,
128}
129
130pub struct RecognitionRow {
133 pub customer_id: Option<Uuid>,
134 pub branch_id: Option<Uuid>,
135 pub posting_at: chrono::DateTime<chrono::Utc>,
136 pub rounded_total: Decimal,
137 pub grand_total: Decimal,
138 pub tax_total: Decimal,
139 pub paid_total: Decimal,
140 pub billing_invoice_id: Option<Uuid>,
141 pub receivable_account_id: Option<Uuid>,
142 pub income_account_id: Option<Uuid>,
143 pub cash_account_id: Option<Uuid>,
144 pub tax_account_id: Option<Uuid>,
145 pub tax_rate: Decimal,
146 pub warehouse_id: Option<Uuid>,
147 pub cogs_account_id: Option<Uuid>,
148 pub inventory_account_id: Option<Uuid>,
149 pub currency: String,
150 pub default_customer_id: Option<Uuid>,
151}
152
153pub struct SyncLockRow {
155 pub status: String,
156 pub paid_total: Decimal,
157 pub rounded_total: Decimal,
158}
159
160pub struct StockReliefRow {
162 pub stock_delivery_note_id: Option<Uuid>,
163 pub branch_id: Option<Uuid>,
164 pub customer_id: Option<Uuid>,
165 pub posting_at: chrono::DateTime<chrono::Utc>,
166 pub warehouse_id: Option<Uuid>,
167 pub cogs_account_id: Option<Uuid>,
168 pub inventory_account_id: Option<Uuid>,
169}
170
171pub struct ReturnSourceRow {
174 pub pos_profile_id: Uuid,
175 pub opening_entry_id: Uuid,
176 pub branch_id: Option<Uuid>,
177 pub customer_id: Option<Uuid>,
178 pub billing_invoice_id: Option<Uuid>,
179 pub payment_entry_id: Option<Uuid>,
180 pub rounded_total: Decimal,
181 pub net_total: Decimal,
182 pub tax_total: Decimal,
183 pub grand_total: Decimal,
184 pub posting_at: chrono::DateTime<chrono::Utc>,
185 pub status: String,
186}
187
188pub struct ReceiptHeaderRow {
193 pub receipt_number: String,
194 pub posting_at: chrono::DateTime<chrono::Utc>,
195 pub net_total: Decimal,
196 pub tax_total: Decimal,
197 pub rounding_adjustment: Decimal,
198 pub rounded_total: Decimal,
199 pub paid_total: Decimal,
200 pub change_due: Decimal,
201 pub status: String,
202 pub register_name: String,
203 pub currency: String,
204 pub tax_rate: Decimal,
205 pub is_invoiced: bool,
206}
207
208impl PosInvoiceRepository {
211 pub async fn insert_draft(
219 &self,
220 conn: &mut sqlx::PgConnection,
221 i: &NewDraftInvoiceRow<'_>,
222 ) -> Result<(), sqlx::Error> {
223 sqlx::query(
224 r#"INSERT INTO pos.pos_invoices
225 (id, client_uuid, pos_profile_id, opening_entry_id, branch_id, customer_id, pos_table_id, receipt_number,
226 posting_at, net_total, tax_total, grand_total, rounding_adjustment, rounded_total,
227 paid_total, change_due, is_return, status)
228 VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,0,0,false,'draft'::pos_invoice_status)"#,
229 )
230 .bind(i.id).bind(i.client_uuid).bind(i.pos_profile_id).bind(i.opening_entry_id).bind(i.branch_id)
231 .bind(i.customer_id).bind(i.pos_table_id).bind(i.receipt_number).bind(i.posting_at).bind(i.net_total).bind(i.tax_total)
232 .bind(i.grand_total).bind(i.rounding_adjustment).bind(i.rounded_total)
233 .execute(conn)
234 .await?;
235 Ok(())
236 }
237
238 pub async fn find_by_client_uuid(
242 &self,
243 pool: &PgPool,
244 client_uuid: Uuid,
245 ) -> Result<Option<SyncLookupRow>, sqlx::Error> {
246 let row = org_scope::fetch_optional_row_scoped(
247 pool,
248 sqlx::query(
249 r#"SELECT id, status::text AS st, pos_profile_id, opening_entry_id, branch_id, customer_id,
250 pos_table_id, is_return, return_against, net_total, tax_total, grand_total, rounding_adjustment,
251 rounded_total, paid_total, change_due
252 FROM pos.pos_invoices WHERE client_uuid=$1 AND (metadata->>'deleted_at') IS NULL"#,
253 )
254 .bind(client_uuid),
255 )
256 .await?;
257 Ok(row.map(|r| SyncLookupRow {
258 id: r.get("id"), status: r.get("st"), pos_profile_id: r.get("pos_profile_id"),
259 opening_entry_id: r.get("opening_entry_id"), branch_id: r.get("branch_id"),
260 customer_id: r.get("customer_id"), pos_table_id: r.get("pos_table_id"), is_return: r.get("is_return"),
261 return_against: r.get("return_against"), net_total: r.get("net_total"),
262 tax_total: r.get("tax_total"), grand_total: r.get("grand_total"),
263 rounding_adjustment: r.get("rounding_adjustment"), rounded_total: r.get("rounded_total"),
264 paid_total: r.get("paid_total"), change_due: r.get("change_due"),
265 }))
266 }
267
268 pub async fn update_draft_from_sync(
275 &self,
276 conn: &mut sqlx::PgConnection,
277 pos_invoice_id: Uuid,
278 opening_entry_id: Uuid,
279 branch_id: Option<Uuid>,
280 customer_id: Option<Uuid>,
281 pos_table_id: Option<Uuid>,
282 net_total: Decimal,
283 tax_total: Decimal,
284 grand_total: Decimal,
285 rounding_adjustment: Decimal,
286 rounded_total: Decimal,
287 paid_total: Decimal,
288 change_due: Decimal,
289 ) -> Result<u64, sqlx::Error> {
290 let res = sqlx::query(
291 r#"UPDATE pos.pos_invoices SET
292 opening_entry_id=$2, branch_id=$3, customer_id=$4, pos_table_id=$5,
293 net_total=$6, tax_total=$7, grand_total=$8, rounding_adjustment=$9,
294 rounded_total=$10, paid_total=$11, change_due=$12
295 WHERE id=$1 AND status='draft'::pos_invoice_status"#,
296 )
297 .bind(pos_invoice_id).bind(opening_entry_id).bind(branch_id).bind(customer_id).bind(pos_table_id)
298 .bind(net_total).bind(tax_total).bind(grand_total).bind(rounding_adjustment)
299 .bind(rounded_total).bind(paid_total).bind(change_due)
300 .execute(conn)
301 .await?;
302 Ok(res.rows_affected())
303 }
304
305 pub async fn lock_for_sync_on(
310 &self,
311 conn: &mut sqlx::PgConnection,
312 pos_invoice_id: Uuid,
313 ) -> Result<Option<SyncLockRow>, sqlx::Error> {
314 let row = sqlx::query(
315 "SELECT status::text AS st, paid_total, rounded_total FROM pos.pos_invoices WHERE id=$1 AND (metadata->>'deleted_at') IS NULL FOR UPDATE",
316 )
317 .bind(pos_invoice_id)
318 .fetch_optional(conn)
319 .await?;
320 Ok(row.map(|r| SyncLockRow {
321 status: r.get("st"), paid_total: r.get("paid_total"), rounded_total: r.get("rounded_total"),
322 }))
323 }
324
325 pub async fn find_draft_on_table(
332 &self,
333 conn: &mut sqlx::PgConnection,
334 pos_table_id: Uuid,
335 exclude_invoice_id: Option<Uuid>,
336 ) -> Result<Option<Uuid>, sqlx::Error> {
337 let row = sqlx::query(
338 r#"SELECT id FROM pos.pos_invoices
339 WHERE pos_table_id=$1 AND status='draft'::pos_invoice_status
340 AND (metadata->>'deleted_at') IS NULL AND ($2::uuid IS NULL OR id <> $2)
341 LIMIT 1"#,
342 )
343 .bind(pos_table_id)
344 .bind(exclude_invoice_id)
345 .fetch_optional(conn)
346 .await?;
347 Ok(row.map(|r| r.get("id")))
348 }
349
350 pub async fn find_draft_on_table_scoped(
355 &self,
356 pool: &PgPool,
357 pos_table_id: Uuid,
358 exclude_invoice_id: Option<Uuid>,
359 ) -> Result<Option<Uuid>, sqlx::Error> {
360 let row = org_scope::fetch_optional_row_scoped(
361 pool,
362 sqlx::query(
363 r#"SELECT id FROM pos.pos_invoices
364 WHERE pos_table_id=$1 AND status='draft'::pos_invoice_status
365 AND (metadata->>'deleted_at') IS NULL AND ($2::uuid IS NULL OR id <> $2)
366 LIMIT 1"#,
367 )
368 .bind(pos_table_id)
369 .bind(exclude_invoice_id),
370 )
371 .await?;
372 Ok(row.map(|r| r.get("id")))
373 }
374
375 pub async fn soft_delete_lines_for_ticket(
379 &self,
380 conn: &mut sqlx::PgConnection,
381 pos_invoice_id: Uuid,
382 now: chrono::DateTime<chrono::Utc>,
383 ) -> Result<(), sqlx::Error> {
384 sqlx::query(
385 r#"UPDATE pos.pos_invoice_items SET metadata = jsonb_set(metadata, '{deleted_at}', to_jsonb($2::timestamptz))
386 WHERE pos_invoice_id=$1 AND (metadata->>'deleted_at') IS NULL"#,
387 )
388 .bind(pos_invoice_id).bind(now)
389 .execute(conn)
390 .await?;
391 Ok(())
392 }
393
394 pub async fn count_draft_orders_for_session(
396 &self,
397 pool: &PgPool,
398 opening_entry_id: Uuid,
399 ) -> Result<i64, sqlx::Error> {
400 fetch_one_scalar_scoped(
401 pool,
402 sqlx::query_scalar(
403 "SELECT COUNT(*) FROM pos.pos_invoices WHERE opening_entry_id=$1 AND status='draft'::pos_invoice_status AND is_return=false AND (metadata->>'deleted_at') IS NULL",
404 )
405 .bind(opening_entry_id),
406 )
407 .await
408 }
409
410 pub async fn count_linked_unposted_for_session(
414 &self,
415 pool: &PgPool,
416 opening_entry_id: Uuid,
417 ) -> Result<i64, sqlx::Error> {
418 fetch_one_scalar_scoped(
419 pool,
420 sqlx::query_scalar(
421 "SELECT COUNT(*) FROM pos.pos_invoices WHERE opening_entry_id=$1 AND status='draft'::pos_invoice_status AND billing_invoice_id IS NOT NULL AND (metadata->>'deleted_at') IS NULL",
422 )
423 .bind(opening_entry_id),
424 )
425 .await
426 }
427
428 pub async fn fetch_tender_header(
438 &self,
439 conn: &mut sqlx::PgConnection,
440 pos_invoice_id: Uuid,
441 ) -> Result<Option<TenderHeaderRow>, sqlx::Error> {
442 let row = sqlx::query(
443 "SELECT status::text AS st, rounded_total FROM pos.pos_invoices WHERE id=$1 AND (metadata->>'deleted_at') IS NULL FOR UPDATE",
444 )
445 .bind(pos_invoice_id)
446 .fetch_optional(conn)
447 .await?;
448 Ok(row.map(|r| TenderHeaderRow {
449 status: r.get("st"), rounded_total: r.get("rounded_total"),
450 }))
451 }
452
453 pub async fn update_tender_totals(
456 &self,
457 conn: &mut sqlx::PgConnection,
458 pos_invoice_id: Uuid,
459 paid_total: Decimal,
460 change_due: Decimal,
461 ) -> Result<(), sqlx::Error> {
462 sqlx::query("UPDATE pos.pos_invoices SET paid_total=$2, change_due=$3 WHERE id=$1")
463 .bind(pos_invoice_id).bind(paid_total).bind(change_due)
464 .execute(conn)
465 .await?;
466 Ok(())
467 }
468
469 pub async fn fetch_paid_state(
472 &self,
473 pool: &PgPool,
474 pos_invoice_id: Uuid,
475 ) -> Result<Option<PaidStateRow>, sqlx::Error> {
476 let row = org_scope::fetch_optional_row_scoped(
477 pool,
478 sqlx::query(
479 "SELECT status::text AS st, billing_invoice_id, payment_entry_id FROM pos.pos_invoices WHERE id=$1 AND (metadata->>'deleted_at') IS NULL",
480 )
481 .bind(pos_invoice_id),
482 )
483 .await?;
484 Ok(row.map(|r| PaidStateRow {
485 status: r.get("st"), billing_invoice_id: r.get("billing_invoice_id"),
486 payment_entry_id: r.get("payment_entry_id"),
487 }))
488 }
489
490 pub async fn fetch_for_recognition(
499 &self,
500 pool: &PgPool,
501 pos_invoice_id: Uuid,
502 ) -> Result<Option<RecognitionRow>, sqlx::Error> {
503 let row = org_scope::fetch_optional_row_scoped(
504 pool,
505 sqlx::query(
506 r#"SELECT i.customer_id, i.pos_profile_id, i.branch_id, i.posting_at, i.rounded_total, i.grand_total,
507 i.tax_total, i.paid_total, i.billing_invoice_id,
508 p.receivable_account_id, p.income_account_id, p.cash_account_id, p.tax_account_id,
509 ROUND(COALESCE(i.tax_total / NULLIF(i.net_total, 0), 0), 6) AS tax_rate,
510 p.warehouse_id, p.cogs_account_id, p.inventory_account_id,
511 p.currency, p.default_customer_id
512 FROM pos.pos_invoices i JOIN pos.pos_profiles p ON p.id = i.pos_profile_id
513 WHERE i.id=$1 AND i.status='draft'::pos_invoice_status AND (i.metadata->>'deleted_at') IS NULL"#,
514 )
515 .bind(pos_invoice_id),
516 )
517 .await?;
518 Ok(row.map(|r| RecognitionRow {
519 customer_id: r.get("customer_id"), branch_id: r.get("branch_id"),
520 posting_at: r.get("posting_at"), rounded_total: r.get("rounded_total"), grand_total: r.get("grand_total"),
521 tax_total: r.get("tax_total"), paid_total: r.get("paid_total"),
522 billing_invoice_id: r.get("billing_invoice_id"),
523 receivable_account_id: r.get("receivable_account_id"), income_account_id: r.get("income_account_id"),
524 cash_account_id: r.get("cash_account_id"), tax_account_id: r.get("tax_account_id"),
525 tax_rate: r.get("tax_rate"), warehouse_id: r.get("warehouse_id"), cogs_account_id: r.get("cogs_account_id"),
526 inventory_account_id: r.get("inventory_account_id"), currency: r.get("currency"),
527 default_customer_id: r.get("default_customer_id"),
528 }))
529 }
530
531 pub async fn link_billing_invoice(
540 &self,
541 pool: &PgPool,
542 pos_invoice_id: Uuid,
543 billing_invoice_id: Uuid,
544 ) -> Result<u64, sqlx::Error> {
545 let res = org_scope::execute_scoped(
546 pool,
547 sqlx::query("UPDATE pos.pos_invoices SET billing_invoice_id=$2 WHERE id=$1 AND status='draft'::pos_invoice_status AND billing_invoice_id IS NULL")
548 .bind(pos_invoice_id).bind(billing_invoice_id),
549 )
550 .await?;
551 Ok(res.rows_affected())
552 }
553
554 pub async fn fetch_for_stock_relief(
558 &self,
559 pool: &PgPool,
560 pos_invoice_id: Uuid,
561 ) -> Result<Option<StockReliefRow>, sqlx::Error> {
562 let row = org_scope::fetch_optional_row_scoped(
563 pool,
564 sqlx::query(
565 r#"SELECT i.stock_delivery_note_id, i.branch_id, COALESCE(i.customer_id, p.default_customer_id) AS customer_id,
566 i.posting_at, p.warehouse_id, p.cogs_account_id, p.inventory_account_id
567 FROM pos.pos_invoices i JOIN pos.pos_profiles p ON p.id = i.pos_profile_id
568 WHERE i.id=$1 AND i.status='paid'::pos_invoice_status AND (i.metadata->>'deleted_at') IS NULL"#,
569 )
570 .bind(pos_invoice_id),
571 )
572 .await?;
573 Ok(row.map(|r| StockReliefRow {
574 stock_delivery_note_id: r.get("stock_delivery_note_id"), branch_id: r.get("branch_id"),
575 customer_id: r.get("customer_id"), posting_at: r.get("posting_at"),
576 warehouse_id: r.get("warehouse_id"), cogs_account_id: r.get("cogs_account_id"),
577 inventory_account_id: r.get("inventory_account_id"),
578 }))
579 }
580
581 pub async fn link_stock_delivery_note(
584 &self,
585 pool: &PgPool,
586 pos_invoice_id: Uuid,
587 delivery_note_id: Uuid,
588 ) -> Result<u64, sqlx::Error> {
589 let res = org_scope::execute_scoped(
590 pool,
591 sqlx::query("UPDATE pos.pos_invoices SET stock_delivery_note_id=$2 WHERE id=$1 AND stock_delivery_note_id IS NULL")
592 .bind(pos_invoice_id).bind(delivery_note_id),
593 )
594 .await?;
595 Ok(res.rows_affected())
596 }
597
598 pub async fn mark_paid(
606 &self,
607 pool: &PgPool,
608 pos_invoice_id: Uuid,
609 billing_invoice_id: Uuid,
610 payment_entry_id: Uuid,
611 ) -> Result<u64, sqlx::Error> {
612 let res = org_scope::execute_scoped(
613 pool,
614 sqlx::query(
615 "UPDATE pos.pos_invoices SET status='paid'::pos_invoice_status, billing_invoice_id=$2, payment_entry_id=$3 WHERE id=$1 AND status='draft'::pos_invoice_status",
616 )
617 .bind(pos_invoice_id).bind(billing_invoice_id).bind(payment_entry_id),
618 )
619 .await?;
620 Ok(res.rows_affected())
621 }
622
623 pub async fn fetch_return_source(
626 &self,
627 pool: &PgPool,
628 pos_invoice_id: Uuid,
629 ) -> Result<Option<ReturnSourceRow>, sqlx::Error> {
630 let row = org_scope::fetch_optional_row_scoped(
631 pool,
632 sqlx::query(
633 r#"SELECT pos_profile_id, opening_entry_id, branch_id, customer_id, billing_invoice_id,
634 payment_entry_id, rounded_total, net_total, tax_total, grand_total, posting_at, status::text AS st
635 FROM pos.pos_invoices WHERE id=$1 AND (metadata->>'deleted_at') IS NULL"#,
636 )
637 .bind(pos_invoice_id),
638 )
639 .await?;
640 Ok(row.map(|r| ReturnSourceRow {
641 pos_profile_id: r.get("pos_profile_id"),
642 opening_entry_id: r.get("opening_entry_id"), branch_id: r.get("branch_id"),
643 customer_id: r.get("customer_id"), billing_invoice_id: r.get("billing_invoice_id"),
644 payment_entry_id: r.get("payment_entry_id"), rounded_total: r.get("rounded_total"),
645 net_total: r.get("net_total"), tax_total: r.get("tax_total"), grand_total: r.get("grand_total"),
646 posting_at: r.get("posting_at"), status: r.get("st"),
647 }))
648 }
649
650 pub async fn mark_returned(
654 &self,
655 pool: &PgPool,
656 pos_invoice_id: Uuid,
657 ) -> Result<u64, sqlx::Error> {
658 let res = org_scope::execute_scoped(
659 pool,
660 sqlx::query("UPDATE pos.pos_invoices SET status='returned'::pos_invoice_status WHERE id=$1 AND status='paid'::pos_invoice_status")
661 .bind(pos_invoice_id),
662 )
663 .await?;
664 Ok(res.rows_affected())
665 }
666
667 pub async fn insert_return_ticket(
670 &self,
671 pool: &PgPool,
672 r: &NewReturnInvoiceRow,
673 ) -> Result<(), sqlx::Error> {
674 org_scope::execute_scoped(
675 pool,
676 sqlx::query(
677 r#"INSERT INTO pos.pos_invoices
678 (id, pos_profile_id, opening_entry_id, branch_id, customer_id, receipt_number,
679 posting_at, net_total, tax_total, grand_total, rounding_adjustment, rounded_total,
680 paid_total, change_due, billing_invoice_id, is_return, return_against, status)
681 VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,0,$11,$11,0,$12,true,$13,'returned'::pos_invoice_status)"#,
682 )
683 .bind(r.id).bind(r.pos_profile_id).bind(r.opening_entry_id)
684 .bind(r.branch_id).bind(r.customer_id).bind(&r.receipt_number)
685 .bind(r.posting_at).bind(r.net_total).bind(r.tax_total).bind(r.grand_total).bind(r.rounded_total)
686 .bind(r.billing_invoice_id).bind(r.return_against),
687 )
688 .await?;
689 Ok(())
690 }
691
692 pub async fn find_return_ticket(
695 &self,
696 pool: &PgPool,
697 original_pos_invoice_id: Uuid,
698 ) -> Result<Option<Uuid>, sqlx::Error> {
699 fetch_optional_scalar_scoped(
700 pool,
701 sqlx::query_scalar(
702 "SELECT id FROM pos.pos_invoices WHERE return_against=$1 AND is_return=true AND (metadata->>'deleted_at') IS NULL LIMIT 1",
703 )
704 .bind(original_pos_invoice_id),
705 )
706 .await
707 }
708
709 pub async fn sum_change_due_for_session(
712 &self,
713 pool: &PgPool,
714 opening_entry_id: Uuid,
715 ) -> Result<Decimal, sqlx::Error> {
716 fetch_one_scalar_scoped(
717 pool,
718 sqlx::query_scalar(
719 "SELECT COALESCE(SUM(change_due),0) FROM pos.pos_invoices WHERE opening_entry_id=$1 AND status='paid'::pos_invoice_status AND (metadata->>'deleted_at') IS NULL",
720 )
721 .bind(opening_entry_id),
722 )
723 .await
724 }
725
726 pub async fn session_totals(
729 &self,
730 pool: &PgPool,
731 opening_entry_id: Uuid,
732 ) -> Result<(Decimal, i64), sqlx::Error> {
733 let row = fetch_one_row_scoped(
734 pool,
735 sqlx::query(
736 "SELECT COALESCE(SUM(rounded_total),0), COUNT(*) FROM pos.pos_invoices WHERE opening_entry_id=$1 AND status='paid'::pos_invoice_status AND (metadata->>'deleted_at') IS NULL",
737 )
738 .bind(opening_entry_id),
739 )
740 .await?;
741 Ok((row.get(0), row.get(1)))
742 }
743
744 pub async fn fetch_receipt_header(
747 &self,
748 pool: &PgPool,
749 pos_invoice_id: Uuid,
750 ) -> Result<Option<ReceiptHeaderRow>, sqlx::Error> {
751 let row = org_scope::fetch_optional_row_scoped(
752 pool,
753 sqlx::query(
754 r#"SELECT i.receipt_number, i.posting_at, i.net_total, i.tax_total, i.rounding_adjustment,
755 i.rounded_total, i.paid_total, i.change_due, i.status::text AS status,
756 p.name AS register_name, p.currency,
757 ROUND(COALESCE(i.tax_total / NULLIF(i.net_total, 0), 0), 6) AS tax_rate,
758 (i.billing_invoice_id IS NOT NULL) AS is_invoiced
759 FROM pos.pos_invoices i JOIN pos.pos_profiles p ON p.id = i.pos_profile_id
760 WHERE i.id=$1 AND (i.metadata->>'deleted_at') IS NULL"#,
761 )
762 .bind(pos_invoice_id),
763 )
764 .await?;
765 Ok(row.map(|r| ReceiptHeaderRow {
766 receipt_number: r.get("receipt_number"), posting_at: r.get("posting_at"),
767 net_total: r.get("net_total"), tax_total: r.get("tax_total"),
768 rounding_adjustment: r.get("rounding_adjustment"), rounded_total: r.get("rounded_total"),
769 paid_total: r.get("paid_total"), change_due: r.get("change_due"), status: r.get("status"),
770 register_name: r.get("register_name"), currency: r.get("currency"), tax_rate: r.get("tax_rate"),
771 is_invoiced: r.get("is_invoiced"),
772 }))
773 }
774}
775
776backbone_core::impl_crud_repository!(PosInvoiceRepository, PosInvoice, soft_delete);