Skip to content

fix(reports): scope shift X exports and split tax - #1403

Merged
khaira777 merged 9 commits into
mainfrom
fm/flocafe-fix-codex-shift-report-findings
Oct 29, 2026
Merged

khaira777 merged 9 commits into
mainfrom
fm/flocafe-fix-codex-shift-report-findings

Conversation

@khaira777

@khaira777 khaira777 commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1402

Intent

Fix the two confirmed per-staff shift reporting defects identified in the secondary review of merged PRs #1395 and #1393:

  1. Cross-shift bill tax duplication in X/Z reports: When per-staff shifts are enabled and a single taxed bill is collected across multiple staff shifts (multiple payment lines with different cash_session_id values), attribute the bill's stored historical tax components across the contributing sessions proportionally to each session's collected payment share (preserving the bill's exact total tax across all contributing sessions via deterministic minor-unit remainder allocation) rather than attributing 100% of the bill's tax to every session that touched the bill. Never recompute historical tax from current live tax settings.
  2. Staff-filtered X-report export (CSV, XLSX, and PDF) ignoring selected shift: When an operator selects a specific staff shift (session_id) in the cash-close / X-report UI and exports (CSV, XLSX, or PDF), pass session_id from the frontend and make GET /api/reports/x-report/export honor session_id with the same session aggregation and authorization rules as GET /api/reports/x-report, including the shift identity in the exported document and filename rather than silently exporting whole-day figures.

What Changed

  • Allocated stored historical bill tax components sequentially across payment lines in minor units for session-scoped X/Z reports (computeSessionAggregates(), computePeriodAggregates(), mergeDisplayTaxComponents(), reconcileSnapshotAllocations()), excluding cancelled unsettled orders and anchoring pre-settlement timestamp-less payment lines to bills.created_at so partial and cross-shift payments conserve exact bill tax without recomputing from live tax settings.
  • Updated GET /api/reports/x-report/export, cash-close-export.ts, and useCashClose to pass and enforce session_id with the same validation, shift-ownership checks, and session aggregation as GET /api/reports/x-report, emitting Shift #<id> - <staff> metadata and x-report-<date>-shift-<id>.<ext> filenames across CSV, XLSX, and HTML/PDF exports.
  • Added regression tests in tests/cash-sessions.test.ts and tests/reports-export.test.ts and updated docs/ architecture and API references for cross-shift tax attribution and shift-scoped X-report exports.

Risk Assessment

✅ Low: Cross-shift historical tax allocation and shift-scoped X-report exports are well-bounded, conserve minor-unit tax totals without recomputing from live settings, enforce shared shift ownership rules, and cleanly resolve both Round 1 findings on cancelled unsettled orders and legacy payment-line window bounds.

Testing

Built the backend and static desktop frontend, ran targeted suites (test:cash-sessions, test:reports-export including Electron printToPDF, test:cash-closures, and test:tax-components), and drove all five derived scenarios (including live tax-pack mutation, cancelled unsettled partial-payment order, and cross-shift/malformed session_id export guards) against a real isolated FloCafe server via Playwright Chromium and Electron PDF rendering, capturing UI screenshots, shift-scoped CSV/XLSX/HTML/PDF exports, and SQLite closure reconciliation; everything passed.

  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
Staff collect single taxed bills across multiple shifts (including an early shift close before final settlement and an uneven dual-component GST split) and observe proportional cent-conserved tax in s… ✅ pass live 03-x-report-shift-alice-pdf-preview.png, 04-x-report-shift-bob-pdf-preview.png, 05-day-close-z-report-ticket.png, live-validation-summary.json, and npm run test:cash-sessions
Adversarial: Owner replaces the active manual tax configuration with a 25% tax rule after cross-shift bills are recorded, and historical X/Z reports and exports preserve the stored bill tax components… ✅ pass live 03-x-report-shift-alice-pdf-preview.png, 05-day-close-z-report-ticket.png, and live-validation-summary.json
Adversarial: Manager cancels an unsettled order after a partial payment was collected in an open shift, removing its provisional tax share from the shift X-report, shift Z closure, and Day Z report ✅ pass live live-validation-summary.json, 05-day-close-z-report-ticket.png, and npm run test:cash-sessions
Operator selects a specific staff shift in the Close Day modal and exports CSV, Excel (.xlsx), and PDF, receiving shift-scoped figures, shift identity headers, and x-report-&lt;date&gt;-shift-&lt;id&gt;.&lt;ext&gt; f… ✅ pass live 01-x-report-all-shifts-modal.png, 02-x-report-shift-alice-selected-modal.png, 03-x-report-shift-alice-pdf-preview.png, x-report-2026-10-29-shift-2.csv, x-report-2026-10-29-shift-2.xlsx, x-report-2026-…
Adversarial: Unpermitted cashiers, cross-shift cashiers with reports.view, and malformed or nonexistent session_id values are rejected by GET /api/reports/x-report/export ✅ pass live live-validation-summary.json and npm run test:cash-sessions

Close Day modal - All shifts (full day) X-report baseline ($330.00 sales, $386.00 expected cash)
Close Day modal - Shift #2 (Cashier Alice) selected ($168.67 shift sales, $218.67 expected cash)
Rendered PDF/HTML export preview for Shift #2 (Cashier Alice) showing Shift row and proportional tax ($12.00 VAT, $1.66 CGST, $1.67 SGST)
Rendered PDF/HTML export preview for Shift #3 (Cashier Bob) showing complementary proportional tax ($4.00 VAT, $3.34 CGST, $3.33 SGST)
Close Day Step 3 Z-report ticket reconciling whole-day historical tax ($20.00 VAT, $5.00 CGST, $5.00 SGST) and $0.00 cash variance

Evidence: Desktop Electron printToPDF output for Shift #2 X-report (x-report-2026-10-29-shift-2.pdf)
%PDF-1.4
%

... [188373 bytes truncated] ...

pX
endstream
endobj
43 0 obj
<</Type /Font
/Subtype /Type3
/FontMatrix [.00048828125 0 0 -.00048828125 0 0]
/FirstChar 0
/LastChar 46
/FontBBox [68 495 1949 -1951]
/CIDToGIDMap /Identity
/ToUnicode 349 0 R
/FontDescriptor 300 0 R
/Widths [2048 0 0 0 0 0 1298 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1380]
/Encoding <</Type /Encoding
/Differences [0 /g0 /g0 /g0 /g0 /g0 /g0 /g105 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g0 /g12D]>>
/CharProcs <</g0 346 0 R
/g105 347 0 R
/g12D 348 0 R>>>>
endobj
xref
0 350
0000000000 65535 f 
0000000015 00000 n 
0000004788 00000 n 
0000000469 00000 n 
0000008650 00000 n 
0000012802 00000 n 
0000018401 00000 n 
0000020932 00000 n 
0000022537 00000 n 
0000026286 00000 n 
0000032142 00000 n 
0000037834 00000 n 
0000044371 00000 n 
0000046494 00000 n 
0000051668 00000 n 
0000056938 00000 n 
0000062340 00000 n 
0000065256 00000 n 
0000072719 00000 n 
0000076693 00000 n 
0000082000 00000 n 
0000085866 00000 n 
0000087815 00000 n 
0000090388 00000 n 
0000097640 00000 n 
0000101349 00000 n 
0000105458 00000 n 
0000111673 00000 n 
0000118667 00000 n 
0000121235 00000 n 
0000128841 00000 n 
0000136704 00000 n 
0000144393 00000 n 
0000156527 00000 n 
0000161571 00000 n 
0000167290 00000 n 
0000171551 00000 n 
0000174308 00000 n 
0000177688 00000 n 
0000180185 00000 n 
0000181998 00000 n 
0000183868 00000 n 
0000186334 00000 n 
0000188403 00000 n 
0000000506 00000 n 
0000005460 00000 n 
0000005516 00000 n 
0000005635 00000 n 
0000005862 00000 n 
0000005934 00000 n 
0000006769 00000 n 
0000006931 00000 n 
0000008100 00000 n 
0000008420 00000 n 
0000010538 00000 n 
0000010765 00000 n 
0000010888 00000 n 
0000011307 00000 n 
0000012089 00000 n 
0000012483 00000 n 
0000013984 00000 n 
0000014211 00000 n 
0000015129 00000 n 
0000015974 00000 n 
0000016865 00000 n 
0000017305 00000 n 
0000017755 00000 n 
0000018071 00000 n 
0000020226 00000 n 
0000020453 00000 n 
0000020634 00000 n 
0000021540 00000 n 
0000021768 00000 n 
0000022008 00000 n 
0000022307 00000 n 
0000023204 00000 n 
0000023432 00000 n 
0000023504 00000 n 
0000023717 00000 n 
0000024463 00000 n 
0000024905 00000 n 
0000025962 00000 n 
0000028196 00000 n 
0000028424 00000 n 
0000029225 00000 n 
0000029940 00000 n 
0000030704 00000 n 
0000031087 00000 n 
0000031504 00000 n 
0000031811 00000 n 
0000033968 00000 n 
0000034196 00000 n 
0000034325 00000 n 
0000035017 00000 n 
0000035752 00000 n 
0000036142 00000 n 
0000037127 00000 n 
0000037504 00000 n 
0000039065 00000 n 
0000039293 00000 n 
0000039889 00000 n 
0000040061 00000 n 
0000040710 00000 n 
0000041822 00000 n 
0000042935 00000 n 
0000043423 00000 n 
0000043904 00000 n 
0000044027 00000 n 
0000045651 00000 n 
0000045880 00000 n 
0000046195 00000 n 
0000048223 00000 n 
0000048429 00000 n 
0000048496 00000 n 
0000049062 00000 n 
0000049630 00000 n 
0000050005 00000 n 
0000050349 00000 n 
0000051139 00000 n 
0000051469 00000 n 
0000053574 00000 n 
0000053780 00000 n 
0000054099 00000 n 
0000054583 00000 n 
0000054896 00000 n 
0000055613 00000 n 
0000055911 00000 n 
0000056279 00000 n 
0000056601 00000 n 
0000058534 00000 n 
0000058740 00000 n 
0000059372 00000 n 
0000059920 00000 n 
0000060481 00000 n 
0000061085 00000 n 
0000061400 00000 n 
0000061731 00000 n 
0000062004 00000 n 
0000064158 00000 n 
0000064364 00000 n 
0000064703 00000 n 
0000064949 00000 n 
0000066835 00000 n 
0000067048 00000 n 
0000067544 00000 n 
0000067701 00000 n 
0000068240 00000 n 
0000069057 00000 n 
0000069462 00000 n 
0000070122 00000 n 
0000071015 00000 n 
0000071172 00000 n 
0000072075 00000 n 
0000072176 00000 n 
0000072523 00000 n 
0000073997 00000 n 
0000074210 00000 n 
0000074277 00000 n 
0000074472 00000 n 
0000075114 00000 n 
0000075520 00000 n 
0000076368 00000 n 
0000078586 00000 n 
0000078799 00000 n 
0000079461 00000 n 
0000080052 00000 n 
0000080710 00000 n 
0000081038 00000 n 
0000081395 00000 n 
0000081668 00000 n 
0000083807 00000 n 
0000084020 00000 n 
0000084119 00000 n 
0000084440 00000 n 
0000085235 00000 n 
0000085546 00000 n 
0000087032 00000 n 
0000087245 00000 n 
0000087516 00000 n 
0000089539 00000 n 
0000089752 00000 n 
0000090089 00000 n 
0000092207 00000 n 
0000092420 00000 n 
0000092487 00000 n 
0000092682 00000 n 
0000093283 00000 n 
0000093689 00000 n 
0000093854 00000 n 
0000094013 00000 n 
0000094172 00000 n 
0000094374 00000 n 
0000094517 00000 n 
0000094713 00000 n 
0000094895 00000 n 
0000095465 00000 n 
0000095830 00000 n 
0000096218 00000 n 
0000097066 00000 n 
0000097444 00000 n 
0000099715 00000 n 
0000099928 00000 n 
0000100077 00000 n 
0000100450 00000 n 
0000100639 00000 n 
0000100861 00000 n 
0000101025 00000 n 
0000102110 00000 n 
0000102339 00000 n 
0000102412 00000 n 
0000102626 00000 n 
0000103373 00000 n 
0000103582 00000 n 
0000103785 00000 n 
0000104447 00000 n 
0000104890 00000 n 
0000105227 00000 n 
0000107362 00000 n 
0000107591 00000 n 
0000108393 00000 n 
0000109109 00000 n 
0000109851 00000 n 
0000110616 00000 n 
0000111034 00000 n 
0000111342 00000 n 
0000113509 00000 n 
0000113738 00000 n 
0000113868 00000 n 
0000114622 00000 n 
0000115018 00000 n 
0000115711 00000 n 
0000116102 00000 n 
0000117088 00000 n 
0000117466 00000 n 
0000117933 00000 n 
0000118108 00000 n 
0000118312 00000 n 
0000120360 00000 n 
0000120589 00000 n 
0000120757 00000 n 
0000120929 00000 n 
0000121872 00000 n 
0000122108 00000 n 
0000122181 00000 n 
0000122942 00000 n 
0000123714 00000 n 
0000124198 00000 n 
0000124377 00000 n 
0000124547 00000 n 
0000125399 00000 n 
0000125603 00000 n 
0000126276 00000 n 
0000126705 00000 n 
0000127169 00000 n 
0000128247 00000 n 
0000128610 00000 n 
0000130920 00000 n 
0000131156 00000 n 
0000131286 00000 n 
0000131688 00000 n 
0000132397 00000 n 
0000133174 00000 n 
0000133586 00000 n 
0000134624 00000 n 
0000135011 00000 n 
0000135497 00000 n 
0000135708 00000 n 
0000135919 00000 n 
0000136348 00000 n 
0000138441 00000 n 
0000138677 00000 n 
0000139545 00000 n 
0000140295 00000 n 
0000141073 00000 n 
0000141893 00000 n 
0000142303 00000 n 
0000143315 00000 n 
0000143737 00000 n 
0000144053 00000 n 
0000146278 00000 n 
0000146484 00000 n 
0000146982 00000 n 
0000147135 00000 n 
0000147631 00000 n 
0000148392 00000 n 
0000148773 00000 n 
0000149370 00000 n 
0000150209 00000 n 
0000150362 00000 n 
0000151200 00000 n 
0000151698 00000 n 
0000151852 00000 n 
0000152364 00000 n 
0000153135 00000 n 
0000153728 00000 n 
0000154558 00000 n 
0000154711 00000 n 
0000155601 00000 n 
0000155700 00000 n 
0000155941 00000 n 
0000156328 00000 n 
0000158230 00000 n 
0000158436 00000 n 
0000158503 00000 n 
0000158692 00000 n 
0000159258 00000 n 
0000159826 00000 n 
0000160451 00000 n 
0000161241 00000 n 
0000163477 00000 n 
0000163683 00000 n 
0000164315 00000 n 
0000164881 00000 n 
0000165429 00000 n 
0000166033 00000 n 
0000166348 00000 n 
0000166679 00000 n 
0000166952 00000 n 
0000169108 00000 n 
0000169314 00000 n 
0000169413 00000 n 
0000169897 00000 n 
0000170210 00000 n 
0000170927 00000 n 
0000171225 00000 n 
0000172736 00000 n 
0000172942 00000 n 
0000173198 00000 n 
0000174007 00000 n 
0000176057 00000 n 
0000176263 00000 n 
0000177033 00000 n 
0000177378 00000 n 
0000179526 00000 n 
0000179762 00000 n 
0000179887 00000 n 
0000181296 00000 n 
0000181532 00000 n 
0000181699 00000 n 
0000182369 00000 n 
0000182598 00000 n 
0000183086 00000 n 
0000183567 00000 n 
0000184889 00000 n 
0000185118 00000 n 
0000186035 00000 n 
0000187589 00000 n 
0000187795 00000 n 
0000187944 00000 n 
0000188097 00000 n 
trailer
<</Size 350
/Root 46 0 R
/Info 1 0 R>>
startxref
189029
%%EOF
Evidence: UI-downloaded shift-scoped CSV export (x-report-2026-10-29-shift-2.csv)
section,field,name,role,count,value
summary,Business Date,,,,2026-10-29
summary,Shift,,,,Shift #2 - Cashier Alice
summary,Period Start,,,,2026-10-29 15:41:23
summary,Period End,,,,2026-10-29 15:41:26
summary,Gross Sales,,,,168.67
summary,Refunds,,,,0
summary,Net Collections,,,,168.67
summary,Bill Count,,,,3
summary,Expected Cash in Drawer,,,,218.67
summary,Opening Float,,,,50
summary,Pay-in Total,,,,0
summary,Pay-out Total,,,,0
summary,Safe Drops,,,,0
payment,,cash,,3,168.67
staff,,Cashier Alice,cashier,1,66
staff,,Cashier Clara,cashier,1,66
staff,,Cashier Bob,cashier,1,36.67
Evidence: UI-downloaded all-shifts baseline CSV export (x-report-2026-10-29-all-shifts.csv)
section,field,name,role,count,value
summary,Business Date,,,,2026-10-29
summary,Period Start,,,,2026-10-29 00:00:00
summary,Period End,,,,2026-10-30 00:00:00
summary,Gross Sales,,,,330
summary,Refunds,,,,0
summary,Net Collections,,,,330
summary,Bill Count,,,,3
summary,Expected Cash in Drawer,,,,386
summary,Opening Float,,,,100
summary,Pay-in Total,,,,0
summary,Pay-out Total,,,,0
summary,Safe Drops,,,,0
payment,,cash,,5,286
payment,,card,,1,44
staff,,Cashier Bob,cashier,1,110
staff,,Cashier Alice,cashier,1,110
staff,,Cashier Clara,cashier,1,110
  • Evidence: UI-downloaded shift-scoped Excel workbook export (x-report-2026-10-29-shift-2.xlsx) (local file: ~/.no-mistakes/evidence/01M640PZ3VDN1AMED0J1STNB0R/x-report-2026-10-29-shift-2.xlsx)
Evidence: Shift-scoped HTML export document for Shift #2 (x-report-2026-10-29-shift-2.html)
<!DOCTYPE html><html lang="en-CA"><head>
  <meta charset="utf-8">
  <title>FloCafe Shift Validation — X Report (Shift #2 - Cashier Alice) - 2026-10-29</title>
  <style>
    @page { size: A4 portrait; margin: 12mm; }
    * { box-sizing: border-box; }
    body {
      margin: 0;
      padding: 0;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Noto Naskh Arabic", "Noto Sans Arabic", "Noto Sans Bengali", "Vrinda", "Bangla Sangam MN", "Noto Sans Devanagari", "Nirmala UI", "Kohinoor Devanagari", "Devanagari Sangam MN", "Noto Sans Thai", "Leelawadee UI", "Thonburi", "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", "PingFang TC", "Microsoft JhengHei", "Noto Sans CJK TC", "Noto Sans TC", "Hiragino Sans", "Yu Gothic", "Meiryo", "Noto Sans CJK JP", "Noto Sans JP", "Noto Sans", "Helvetica Neue", Arial, sans-serif;
      font-size: 11px;
      line-height: 1.45;
      color: #111827;
      background: #ffffff;
    }
    .report-header {
      border-bottom: 2px solid #111827;
      padding-bottom: 8px;
      margin-bottom: 14px;
      break-inside: avoid;
    }
    .business-name {
      font-size: 18px;
      font-weight: 700;
      margin: 0 0 2px 0;
      unicode-bidi: plaintext;
    }
    .report-title {
      font-size: 14px;
      font-weight: 600;
      margin: 0 0 4px 0;
      color: #1f2937;
    }
    .report-meta {
      font-size: 10px;
      color: #4b5563;
      margin: 0;
    }
    .report-section {
      margin-bottom: 16px;
    }
    .report-section h2 {
      font-size: 12px;
      font-weight: 700;
      text-transform: uppercase;
      letter-spacing: 0.04em;
      color: #374151;
      margin: 0 0 6px 0;
      break-after: avoid;
    }
    table {
      width: 100%;
      border-collapse: collapse;
      table-layout: auto;
    }
    thead {
      display: table-header-group;
    }
    tr {
      break-inside: avoid;
    }
    th, td {
      border: 1px solid #d1d5db;
      padding: 5px 8px;
      text-align: left;
      vertical-align: top;
    }
    thead th {
      background: #f3f4f6;
      font-weight: 600;
      color: #111827;
    }
    tbody th {
      background: #f9fafb;
      font-weight: 500;
      width: 42%;
    }
    .num {
      text-align: right;
      font-variant-numeric: tabular-nums;
      white-space: nowrap;
    }
    .bidi-text {
      unicode-bidi: plaintext;
    }
    .exact-num {
      color: #6b7280;
      font-size: 10px;
    }
    .empty-row td {
      text-align: center;
      color: #6b7280;
      font-style: italic;
    }
  </style>
</head>
<body>
  <header class="report-header">
    <h1 class="business-name" dir="auto">FloCafe Shift Validation</h1>
    <div class="report-title">X Report (Shift #2 - Cashier Alice) - 2026-10-29</div>
    <p class="report-meta">
      Business Date: <strong>2026-10-29</strong>
      · Shift: <strong>Shift #2 - Cashier Alice</strong>
      · Period: <strong>2026-10-29 15:41:23</strong> to <strong>2026-10-29 15:41:27</strong>
      · Currency: <strong>CAD</strong>
    </p>
  </header>

  <section class="report-section" data-section="summary">
    <h2>Summary</h2>
    <table>
      <thead>
        <tr>
          <th>Metric</th>
          <th class="num">Value</th>
        </tr>
      </thead>
      <tbody>
        <tr data-section="summary" data-metric="Business Date" data-value="2026-10-29">
            <th scope="row">Business Date</th>
            <td class="" data-value="2026-10-29"><span dir="auto" class="bidi-text">2026-10-29</span></td>
          </tr>
<tr data-section="summary" data-metric="Shift" data-value="Shift #2 - Cashier Alice">
            <th scope="row">Shift</th>
            <td class="" data-value="Shift #2 - Cashier Alice"><span dir="auto" class="bidi-text">Shift #2 - Cashier Alice</span></td>
          </tr>
<tr data-section="summary" data-metric="Period Start" data-value="2026-10-29 15:41:23">
            <th scope="row">Period Start</th>
            <td class="" data-value="2026-10-29 15:41:23"><span dir="auto" class="bidi-text">2026-10-29 15:41:23</span></td>
          </tr>
<tr data-section="summary" data-metric="Period End" data-value="2026-10-29 15:41:27">
            <th scope="row">Period End</th>
            <td class="" data-value="2026-10-29 15:41:27"><span dir="auto" class="bidi-text">2026-10-29 15:41:27</span></td>
          </tr>
<tr data-section="summary" data-metric="Gross Sales" data-value="168.67">
            <th scope="row">Gross Sales</th>
            <td class="num" data-value="168.67">$168.67</td>
          </tr>
<tr data-section="summary" data-metric="Refunds" data-value="0">
            <th scope="row">Refunds</th>
            <td class="num" data-value="0">$0.00</td>
          </tr>
<tr data-section="summary" data-metric="Net Collections" data-value="168.67">
            <th scope="row">Net Collections</th>
            <td class="num" data-value="168.67">$168.67</td>
          </tr>
<tr data-section="summary" data-metric="Bill Count" data-value="3">
            <th scope="row">Bill Count</th>
            <td class="num" data-value="3">3</td>
          </tr>
<tr data-section="summary" data-metric="Expected Cash in Drawer" data-value="218.67">
            <th scope="row">Expected Cash in Drawer</th>
            <td class="num" data-value="218.67">$218.67</td>
          </tr>
<tr data-section="summary" data-metric="Opening Float" data-value="50">
            <th scope="row">Opening Float</th>
            <td class="num" data-value="50">$50.00</td>
          </tr>
<tr data-section="summary" data-metric="Pay-in Total" data-value="0">
            <th scope="row">Pay-in Total</th>
            <td class="num" data-value="0">$0.00</td>
          </tr>
<tr data-section="summary" data-metric="Pay-out Total" data-value="0">
            <th scope="row">Pay-out Total</th>
            <td class="num" data-value="0">$0.00</td>
          </tr>
<tr data-section="summary" data-metric="Safe Drops" data-value="0">
            <th scope="row">Safe Drops</th>
            <td class="num" data-value="0">$0.00</td>
          </tr>
      </tbody>
    </table>
  </section>

  <section class="report-section" data-section="payments">
    <h2>Payment Methods</h2>
    <table>
      <thead>
        <tr>
          <th>Method</th>
          <th class="num">Count</th>
          <th class="num">Total</th>
        </tr>
      </thead>
      <tbody>
        <tr data-section="payment" data-method="cash" data-count="3" data-total="168.67">
            <td data-col="method" data-value="cash"><span dir="auto" class="bidi-text">cash</span></td>
            <td class="num" data-col="count" data-value="3">3</td>
            <td class="num" data-col="total" data-value="168.67">$168.67</td>
          </tr>
      </tbody>
    </table>
  </section>

  <section class="report-section" data-section="taxes">
      <h2>Tax Breakdown</h2>
      <table>
        <thead>
          <tr>
            <th>Tax</th>
            <th class="num">Rate (%)</th>
            <th class="num">Amount</th>
          </tr>
        </thead>
        <tbody>
          <tr data-section="tax" data-title="VAT" data-rate="10" data-amount="12">
            <td data-col="title" data-value="VAT"><span dir="auto" class="bidi-text">VAT</span></td>
            <td class="num" data-col="rate" data-value="10">10%</td>
            <td class="num" data-col="amount" data-value="12">$12.00</td>
          </tr>
<tr data-section="tax" data-title="CGST" data-rate="5" data-amount="1.66">
            <td data-col="title" data-value="CGST"><span dir="auto" class="bidi-text">CGST</span></td>
            <td class="num" data-col="rate" data-value="5">5%</td>
            <td class="num" data-col="amount" data-value="1.66">$1.66</td>
          </tr>
<tr data-section="tax" data-title="SGST" data-rate="5" data-amount="1.67">
            <td data-col="title" data-value="SGST"><span dir="auto" class="bidi-text">SGST</span></td>
            <td class="num" data-col="rate" data-value="5">5%</td>
            <td class="num" data-col="amount" data-value="1.67">$1.67</td>
          </tr>
        </tbody>
      </table>
    </section>

  <section class="report-section" data-section="staff">
    <h2>Staff Sales</h2>
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Role</th>
          <th class="num">Orders</th>
          <th class="num">Revenue</th>
        </tr>
      </thead>
      <tbody>
        <tr data-section="staff" data-name="Cashier Alice" data-role="cashier" data-orders="1" data-revenue="66">
            <td data-col="name" data-value="Cashier Alice"><span dir="auto" class="bidi-text">Cashier Alice</span></td>
            <td data-col="role" data-value="cashier"><span dir="auto" class="bidi-text">cashier</span></td>
            <td class="num" data-col="orderCount" data-value="1">1</td>
            <td class="num" data-col="revenue" data-value="66">$66.00</td>
          </tr>
<tr data-section="staff" data-name="Cashier Clara" data-role="cashier" data-orders="1" data-revenue="66">
            <td data-col="name" data-value="Cashier Clara"><span dir="auto" class="bidi-text">Cashier Clara</span></td>
            <td data-col="role" data-value="cashier"><span dir="auto" class="bidi-text">cashier</span></td>
            <td class="num" data-col="orderCount" data-value="1">1</td>
            <td class="num" data-col="revenue" data-value="66">$66.00</td>
          </tr>
<tr data-section="staff" data-name="Cashier Bob" data-role="cashier" data-orders="1" data-revenue="36.67">
            <td data-col="name" data-value="Cashier Bob"><span dir="auto" class="bidi-text">Cashier Bob</span></td>
            <td data-col="role" data-value="cashier"><span dir="auto" class="bidi-text">cashier</span></td>
            <td class="num" data-col="orderCount" data-value="1">1</td>
            <td class="num" data-col="revenue" data-value="36.67">$36.67</td>
          </tr>
      </tbody>
    </table>
  </section>


</body></html>
  • Evidence: Live API, browser export network trace, and SQLite cash_closures reconciliation summary (local file: ~/.no-mistakes/evidence/01M640PZ3VDN1AMED0J1STNB0R/live-validation-summary.json)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed ✅
  • 🚨 main/routes/cash-closures.ts:1018 - Session tax aggregation in computePeriodAggregates broadened WHERE b.paid_at IS NOT NULL to WHERE b.payment_details IS NOT NULL without excluding unsettled bills whose parent order was cancelled (o.status = &#39;cancelled&#39;). When an operator records a partial payment on a bill during a shift and a manager subsequently cancels the order via PATCH /api/orders/:id/status (status: &#39;cancelled&#39;, main/routes/orders.ts:2981-2988), orders.status and all order_items.status become &#39;cancelled&#39; while bills.paid_at stays NULL and bills.tax_snapshot / bills.tax_amount remain populated. resolveTaxComponents(doc) (main/services/tax-components.ts:252-319) then sees zero non-terminal items and falls back to document.tax_snapshot, causing GET /api/reports/x-report?session_id=..., GET /api/reports/x-report/export?session_id=..., and POST /api/cash-sessions/:id/close (immutable cash_closures.tax_components_json) to attribute proportional tax for the cancelled order while whole-day X/Z (main/routes/cash-closures.ts:1060, WHERE b.paid_at &gt;= ? AND b.paid_at &lt; ?), GET /api/reports/tax-components (main/routes/reports.ts:477, AND o.status != &#39;cancelled&#39;), and shiftUnpaidBillsInWindow (main/services/cash-day-close-guards.ts:91, AND o.status != &#39;cancelled&#39;) all report zero. Fix main/routes/cash-closures.ts:1016-1018 by joining orders o ON o.id = b.order_id and requiring (b.paid_at IS NOT NULL OR o.status != &#39;cancelled&#39;).
  • ⚠️ main/routes/cash-closures.ts:1030 - In computePeriodAggregates, the session TaxBillRow EXISTS predicate removed the line_time &gt;= datetime(session.opened_at) lower-bound check on untagged legacy payment lines (cash_session_id IS NULL) and placed the correlated (SELECT cs.id FROM cash_sessions cs WHERE ... ORDER BY datetime(cs.opened_at) DESC, cs.id DESC LIMIT 1) subquery ahead of any window filter. Because cash_sessions has no index on opened_at or closed_at and line_time &lt; datetime(end) is true for every historical bill from the past, upgraded databases execute a full scan and sort of cash_sessions for every pre-v91 payment line in bills synchronously on the main thread (O(N_legacy_lines * N_sessions)). Because the cash_sessions subquery can match session.id only when line_time &gt;= datetime(session.opened_at), guard the untagged branch in main/routes/cash-closures.ts:1027-1049 with NULLIF(json_extract(je.value, &#39;$.cash_session_id&#39;), &#39;&#39;) IS NULL AND ${LEGACY_STABLE_PAYMENT_LINE_TIME_SQL} &gt;= datetime(?) AND ${LEGACY_STABLE_PAYMENT_LINE_TIME_SQL} &lt; datetime(?) before evaluating the cash_sessions overlap tie-breaker.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
Staff collect single taxed bills across multiple shifts (including an early shift close before final settlement and an uneven dual-component GST split) and observe proportional cent-conserved tax in s… ✅ pass live 03-x-report-shift-alice-pdf-preview.png, 04-x-report-shift-bob-pdf-preview.png, 05-day-close-z-report-ticket.png, live-validation-summary.json, and npm run test:cash-sessions
Adversarial: Owner replaces the active manual tax configuration with a 25% tax rule after cross-shift bills are recorded, and historical X/Z reports and exports preserve the stored bill tax components… ✅ pass live 03-x-report-shift-alice-pdf-preview.png, 05-day-close-z-report-ticket.png, and live-validation-summary.json
Adversarial: Manager cancels an unsettled order after a partial payment was collected in an open shift, removing its provisional tax share from the shift X-report, shift Z closure, and Day Z report ✅ pass live live-validation-summary.json, 05-day-close-z-report-ticket.png, and npm run test:cash-sessions
Operator selects a specific staff shift in the Close Day modal and exports CSV, Excel (.xlsx), and PDF, receiving shift-scoped figures, shift identity headers, and x-report-&lt;date&gt;-shift-&lt;id&gt;.&lt;ext&gt; f… ✅ pass live 01-x-report-all-shifts-modal.png, 02-x-report-shift-alice-selected-modal.png, 03-x-report-shift-alice-pdf-preview.png, x-report-2026-10-29-shift-2.csv, x-report-2026-10-29-shift-2.xlsx, x-report-2026-…
Adversarial: Unpermitted cashiers, cross-shift cashiers with reports.view, and malformed or nonexistent session_id values are rejected by GET /api/reports/x-report/export ✅ pass live live-validation-summary.json and npm run test:cash-sessions
  • npm run build && npm run build:frontend
  • npm run test:cash-sessions
  • npm run test:reports-export
  • npm run test:cash-closures
  • npm run test:tax-components
  • ELECTRON_RUN_AS_NODE=1 FLO_DEV_USER_DATA=/tmp/flo-live-shift-8c33b41f PORT=43101 KDS_PORT=43102 SERVER_APP_PORT=43103 &#34;$(node -p &#34;require(&#39;electron&#39;)&#34;)&#34; dev-server.js (isolated live FloCafe instance driving multi-shift billing, live tax mutation, cancelled partial order, export authz/validation, Playwright Chromium Close Day shift-scoped CSV/XLSX/PDF exports, Day Z close, Electron webContents.printToPDF, and SQLite cash_closures reconciliation)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

- Allocate cross-shift paid bill historical tax components proportionally across contributing cash sessions with deterministic minor-unit remainder reconciliation in computeSessionAggregates without recomputing from live tax settings.

- Pass selectedSessionId from useCashClose.exportReport and scope GET /api/reports/x-report/export (CSV, XLSX, HTML/PDF) with matching shift validation, authorization, shift labels, and shift filenames.

- Add cross-shift tax and shift-scoped export integration/hook regression tests and update API/permissions docs.
@coderabbitai

coderabbitai Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Fix cross-shift tax attribution and shift-scoped X-report exports

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Split historical bill tax across contributing shifts without duplicating tax in X/Z reports.
• Export the selected shift’s figures and identity in CSV, XLSX, and PDF reports.
• Add regression coverage and document shift reporting and access rules.
Diagram

graph TD
  UI["Cash Close UI"] --> API["X Report API"] --> SESSION["Session Aggregates"] --> EXPORT["Export Serializer"]
  DATA[("Bills and Payments")] --> SESSION --> CLOSE["Shift Z Close"]
  API --> DAY["Day Aggregates"] --> EXPORT
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Persist tax allocation with each payment
  • ➕ Makes shift reporting less dependent on allocation at read time.
  • ➖ Requires storage and migration decisions for existing bills.
  • ➖ Must handle later payments and changes to partially paid bills.

Recommendation: Keep the PR’s read-time allocation: it uses stored historical tax, reuses deterministic minor-unit helpers, and corrects existing bills without a migration. Shared session validation and aggregation also reduce drift between X exports and shift-close snapshots.

Files changed (12) +1231 / -131

Bug fix (5) +395 / -120
useCashClose.tsPass selected shift to X-report exports +13/-3

Pass selected shift to X-report exports

• Sends the selected session ID for CSV, XLSX, and PDF exports and uses a shift-specific filename. Unscoped X and Z exports retain their existing parameters.

frontend/src/hooks/useCashClose.ts

cash-closures.tsAllocate stored bill tax across contributing sessions +212/-8

Allocate stored bill tax across contributing sessions

• Adds shared session aggregation and apportions resolved historical tax components by each session’s collected payment share. Minor-unit rounding and reconciliation preserve component and bill totals across shifts.

main/routes/cash-closures.ts

cash-sessions.tsUse shared aggregation for shift close +3/-22

Use shared aggregation for shift close

• Switches session-close snapshots to the same aggregation used by shift reports, including the corrected tax attribution.

main/routes/cash-sessions.ts

reports.tsScope X-report exports to authorized shifts +136/-81

Scope X-report exports to authorized shifts

• Shares session lookup and access checks between the JSON and export endpoints. Shift exports now use session totals and a shift-specific filename, while exports without a session ID remain whole-day reports.

main/routes/reports.ts

cash-close-export.tsShow shift identity in exported X reports +31/-6

Show shift identity in exported X reports

• Adds optional shift metadata to X exports and displays it in CSV/XLSX summaries and HTML titles and headers without changing Z-report metrics.

main/services/cash-close-export.ts

Refactor (1) +1 / -1
bills.tsExpose minor-unit tax reconciliation helper +1/-1

Expose minor-unit tax reconciliation helper

• Exports the existing snapshot allocation reconciliation function for reuse in shift tax attribution.

main/routes/bills.ts

Tests (2) +817 / -0
cash-sessions.test.tsCover cross-shift tax and session-close reconciliation +330/-0

Cover cross-shift tax and session-close reconciliation

• Tests proportional allocation, minor-unit remainders, unchanged historical tax after live-setting changes, export permissions, and reconciliation of shift Z tax with day Z tax.

tests/cash-sessions.test.ts

reports-export.test.tsCover shift exports across formats and the frontend hook +487/-0

Cover shift exports across formats and the frontend hook

• Checks scoped CSV, XLSX, HTML/PDF, unscoped day exports, access errors, filenames, and shift figures. Hook tests verify that CSV, XLSX, and PDF requests carry the selected session ID.

tests/reports-export.test.ts

Documentation (4) +18 / -10
business-time.mdLink shift timing guidance to tax attribution +2/-1

Link shift timing guidance to tax attribution

• Adds a pointer from concurrent-shift behavior to the explanation of cross-shift tax attribution.

docs/architecture/business-time.md

taxation.mdDocument historical tax allocation across shifts +11/-4

Document historical tax allocation across shifts

• Explains how day reports aggregate stored tax and session reports split it by payment share without consulting live tax settings.

docs/architecture/taxation.md

api.mdSpecify shift-scoped X-report export behavior +1/-1

Specify shift-scoped X-report export behavior

• Documents the optional session ID, validation and authorization errors, shift labels, and shift-specific filenames.

docs/reference/api.md

roles-and-permissions.mdClarify shift export access rules +4/-4

Clarify shift export access rules

• Extends the own-shift restriction to X-report exports for non-owner and non-manager roles.

docs/reference/roles-and-permissions.md

@khaira777 khaira777 changed the title fix(reports): split cross-shift tax and scope shift X-report exports fix(reports): scope shift X exports and split tax Oct 29, 2026
@qodo-code-review

qodo-code-review Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Shift exports overstate expected cash ✗ Dismissed
Description
The session export assigns the full sessionExpectedCash result to expectedCash, while the JSON
X-report subtracts openingFloatCents and reports the float separately. Whenever a shift has an
opening float, CSV, XLSX, and HTML exports show an expected-cash figure higher than the on-screen
X-report by that amount.
Code

main/routes/reports.ts[R1625-1626]

+          expectedCash: totalExpectedCashCents / minorFactor,
+          openingFloat: openingFloatCents / minorFactor,
Relevance

●● Moderate

Recommendation generated with fallback mode.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Both routes calculate the same total, but the JSON response removes the float before exposing
expected cash; the new export branch does not. The export metrics also list Opening Float
separately.

main/routes/reports.ts[1340-1346]
main/routes/reports.ts[1370-1379]
main/routes/reports.ts[1606-1629]
main/services/cash-close-export.ts[163-183]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Shift-scoped X exports include the opening float in expected cash, unlike the JSON X-report, which shows the float separately.

## Fix Focus Areas
- main/routes/reports.ts[1611-1626]
- tests/reports-export.test.ts[1642-1645]

## Recommended Fix
Assign exported `expectedCash` from `(totalExpectedCashCents - openingFloatCents) / minorFactor`, and update CSV, XLSX, and HTML expectations to assert parity with the JSON X-report.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Overlapping shifts duplicate legacy tax ✓ Resolved
Description
The new payment-line resolver assigns a line without cash_session_id to whichever session is
currently being aggregated when its timestamp falls inside that session's window. If two shifts
overlap and both match such a line, each calculation can build a single bucket for itself and assign
the bill's entire tax to both shifts.
Code

main/routes/cash-closures.ts[R1103-1104]

+          WHEN raw_session_id IS NOT NULL THEN CAST(raw_session_id AS INTEGER)
+          WHEN line_time >= datetime(?) AND line_time < datetime(?) THEN ?
Relevance

●●● Strong

Overlapping legacy timestamp windows can duplicate one payment’s full tax across shifts, directly
violating report allocation correctness.

PR-#836

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The bill selector accepts an untagged line in either matching session window. The added resolver
then prefers the session being calculated over other matching sessions, and the single-bucket path
returns the full stored components.

main/routes/cash-closures.ts[1021-1059]
main/routes/cash-closures.ts[1100-1119]
main/routes/cash-closures.ts[1122-1154]
main/services/shift-session-gate.ts[25-29]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An unowned historical payment line can resolve to different sessions depending on which overlapping shift report is being computed, defeating tax conservation.

## Fix Focus Areas
- main/routes/cash-closures.ts[1021-1059]
- main/routes/cash-closures.ts[1100-1154]

## Recommended Fix
Use one deterministic ownership rule for untagged payment lines across all session calculations, and apply that same rule to bill selection and allocation. Add a regression test with overlapping shifts and an untagged payment line that checks the sum of shift tax against the bill tax.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. A second copy of the tax-merge logic now has to be kept in sync ✓ Resolved
Description
mergeSessionTaxComponents re-implements the title+rate merge and 6-decimal rounding that
mergeComponents in main/services/tax-components.ts already does behind aggregateTaxComponents.
If the merge key or rounding later changes in one copy only, whole-day and session-scoped X/Z
reports will group or round the same components differently.
Code

main/routes/cash-closures.ts[R831-836]

+function mergeSessionTaxComponents(lists: DisplayTaxComponent[][]): DisplayTaxComponent[] {
+  const merged = new Map<string, { title: string; rate: number | null; amount: Decimal }>();
+  for (const list of lists) {
+    for (const comp of list) {
+      const rateKey = comp.rate === null ? '' : new Decimal(comp.rate).toString();
+      const key = `${comp.title}\u0000${rateKey}`;
Relevance

●●● Strong

Duplicate tax aggregation logic creates clear synchronization risk; centralizing the shared merge
behavior is a deterministic maintainability improvement.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The two functions use the same key (title\u0000rate), the same Decimal summing, and the same
toDecimalPlaces(6) output.

main/services/tax-components.ts[122-139]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`mergeSessionTaxComponents` duplicates `mergeComponents` from tax-components.ts.

## Fix Focus Areas
- main/routes/cash-closures.ts[831-854]
- main/services/tax-components.ts[122-139]

## Recommended Fix
Export a helper from tax-components.ts, such as `mergeDisplayTaxComponents(lists: DisplayTaxComponent[][])`, that wraps `mergeComponents`. Use it in cash-closures.ts and delete the local copy.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Tax drops out of shift Z reports when the first shift closes before the bill is paid ✓ Resolved
Description
The session bill query filters on b.paid_at IS NOT NULL, while perBillComponents allocates a
cross-shift bill’s tax according to each session’s payment share only after the bill is fully paid.
If session A takes a partial payment and closes before session B settles the bill, A’s immutable Z
receives no tax for that payment, B’s Z receives only B’s share, and the missing share appears in
neither Z even though sessionFinancialTotals includes A’s payment in its gross.
Code

main/routes/cash-closures.ts[R1153-1168]

+      if (!buckets.has(session.id)) return [];
+      if (buckets.size === 1) return resolveTaxComponents(doc);
+
+      const docComponents = resolveTaxComponents(doc);
+      if (docComponents.length === 0) return [];
+
+      const sessionIds = Array.from(buckets.keys());
+      const weights = Array.from(buckets.values());
+      const sessionIndex = sessionIds.indexOf(session.id);
+      const { compMinors, totalMinor } = roundComponentsToMinorUnits(docComponents, minorFactor);
+      const entries = compMinors.map((minor) => ({
+        allocations: allocateSignedMinorUnits(minor, weights),
+      }));
+      if (entries.length > 1) {
+        const targetSessionMinors = allocateSignedMinorUnits(totalMinor, weights);
+        reconcileSnapshotAllocations(entries, targetSessionMinors);
Relevance

● Weak

A closely matching prior finding about exports including opening float was rejected by the team.

PR-#922

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
paid_at is written only when payment_status becomes paid (bills.ts line 3848), but payment
lines can be recorded earlier and a shift can close with a partially paid bill outstanding. The
session bill query excludes that bill until paid_at is set, while closeSessionTxn freezes the
session’s Z at close, so later settlement cannot add A’s allocated tax to its stored Z. The new
proportional allocation gives B only its own share; previously the later session took the full tax,
keeping the shift totals equal to the bill’s tax. sessionFinancialTotals has no paid_at filter,
so A’s gross still includes its partial payment.

main/routes/bills.ts[3844-3850]
main/routes/cash-closures.ts[1021-1058]
main/routes/cash-closures.ts[729-760]
main/routes/cash-sessions.ts[117-120]
main/routes/bills.ts[3794-3807]
main/routes/bills.ts[3846-3858]
main/routes/cash-closures.ts[1021-1059]
tests/cash-sessions.test.ts[1034-1058]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Session tax is allocated by payment share only for bills with `paid_at` set. A session can close after taking a partial payment but before the bill settles, leaving its immutable Z without its eventual tax share; the settling session then records only its own share.

## Fix Focus Areas
- main/routes/cash-closures.ts[1021-1059]
- main/routes/cash-closures.ts[1122-1182]
- main/routes/cash-sessions.ts[117-120]

## Recommended Fix
Choose and consistently apply a settlement policy before persisting shift closures. One approach is to allocate tax on partially paid bills as collections occur, using each payment line’s share of the bill total. Alternatively, keep allocation at settlement but assign shares belonging to already closed contributing sessions (`cash_sessions.status = 'closed'` and `closed_at` <= `bill.paid_at`) to the session completing the bill, or to the last open contributor being reported. Other viable approaches are to prevent closure while contributing bills remain unsettled, or to persist a stable provisional allocation and explicitly reconcile it on settlement without silently leaving a closed snapshot short. Add a regression test in which A takes a partial payment and closes, then B settles the bill and closes; verify that their stored Z tax totals sum to the bill’s tax.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Linked PRs:
  🌐 PR #1395
  🌐 PR #1393
  🌐 PR #39
✅ Compliance rules (platform): 5 rules
✅ Cross-repo context — repo relationships
  Explored: repo: FreeOpenSourcePOS/docs.flopos.com (sha: abe7cff3) — View relationship
Review mode: Auto: 🧠 Deep: Complex tax allocation, authorization, exports, session aggregation, and frontend paths warrant redundant review.

Grey Divider

Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread main/routes/reports.ts
Comment thread main/routes/cash-closures.ts Outdated
Comment thread main/routes/cash-closures.ts Outdated
Include partially paid bills in session X/Z tax aggregation and allocate historical tax components sequentially across payment lines against the remaining bill balance in minor units so shifts closed before settlement retain their collected tax share.

Resolve untagged legacy payment lines deterministically across overlapping shifts via cash_sessions independent of the caller session, and reuse mergeDisplayTaxComponents from tax-components.ts.
@khaira777 khaira777 changed the title fix(reports): scope shift X exports and split tax fix(reports): allocate cross-shift tax and scope shift X-report exports Oct 29, 2026
@khaira777 khaira777 changed the title fix(reports): allocate cross-shift tax and scope shift X-report exports fix(reports): scope shift X exports and split tax Oct 29, 2026
@khaira777

Copy link
Copy Markdown
Contributor Author

Fixed the partial-close-before-settlement tax conservation finding in 99eba49 (commits 5540eaa, 8978cc6, and 98f7770): session-scoped X/Z tax aggregation in computePeriodAggregates() now includes partially paid bills and allocates resolved historical tax components sequentially across payment lines against the remaining bill balance in minor units, preserving causal immutability when an early contributing shift closes before settlement while conserving exact whole-bill tax across subsequent shifts.

@khaira777 khaira777 changed the title fix(reports): scope shift X exports and split tax fix(reports): allocate cross-shift tax and scope shift X-report exports Oct 29, 2026
@khaira777 khaira777 changed the title fix(reports): allocate cross-shift tax and scope shift X-report exports fix(reports): scope shift X exports and split tax Oct 29, 2026
@khaira777
khaira777 merged commit 07d4c26 into main Oct 29, 2026
23 checks passed
@khaira777
khaira777 deleted the fm/flocafe-fix-codex-shift-report-findings branch October 29, 2026 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(reports): scope shift X exports and split cross-shift tax

1 participant