Skip to content

fix(pos,billing): close post-merge review gaps - #1410

Merged
khaira777 merged 10 commits into
mainfrom
fm/flocafe-fix-post-merge-review-1392-1394-1400
Oct 30, 2026
Merged

khaira777 merged 10 commits into
mainfrom
fm/flocafe-fix-post-merge-review-1392-1394-1400

Conversation

@khaira777

@khaira777 khaira777 commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1409. Fix post-merge review defects across PRs #1392, #1394, and #1400: isolate POS checkout continuations and committed carts across session switches, recheck payments.take inside payment correction transaction, include custom charges in daily invoice export, and allow authenticated staff to read idle-lock policy.

What Changed

  • POS session isolation and cart protection: Added session epoch and auth token tracking across POS checkout continuations, table cart additions, and payment flows to halt downstream processing on session switch, clear untouched matching carts to prevent duplicate submissions, and preserve recovery records under the originating user.
  • Daily invoice export custom charges: Updated the daily invoice export data extraction and HTML generation to parse charges_breakdown and render itemised custom fee lines alongside standard service, packaging, and delivery charges.
  • Staff permissions and idle-lock policy: Required the payments.take permission inside payment method correction transactions and allowed all active authenticated staff members to read the auto_lock_minutes idle-lock setting.

Risk Assessment

✅ Low: All post-merge review requirements across session boundary isolation, in-transaction permission re-checks, custom invoice charges, and idle-lock access are cleanly addressed with surgical diffs and executable tests.

Testing

Exercised all four core functional areas live against the running backend API and Next.js frontend in headless Chromium, capturing 4 browser UI screenshots, the rendered daily invoices HTML export, and 2 structured API response evidence files, all passing with zero regressions or findings.

  • Live validation: ✅ go - 4 of 4 scenarios driven live against the product
Scenario Result Live Evidence
Authenticated staff reads idle-lock policy while unauthenticated or deactivated requests are rejected ✅ pass live ~/.no-mistakes/evidence/01M64ZSQNNR1Q74M6KMYT8RX7B/scenario-1-idle-lock-policy.json
Payment method correction transaction rechecks payments.take permission ✅ pass live ~/.no-mistakes/evidence/01M64ZSQNNR1Q74M6KMYT8RX7B/scenario-2-payment-correction-permission.json
Daily Invoices HTML export includes custom charges in totals breakdown ✅ pass live ~/.no-mistakes/evidence/01M64ZSQNNR1Q74M6KMYT8RX7B/daily-invoices-export.html
POS session switch isolates checkout continuations and permits subsequent identical orders ✅ pass live ~/.no-mistakes/evidence/01M64ZSQNNR1Q74M6KMYT8RX7B/03-subsequent-identical-order-modal.png

POS Owner Session with Cart Item

Evidence: POS Owner Session with Cart Item
Screenshot of owner POS session with an active item in the cart prior to locking session.
![POS Locked Session PIN User Picker](https://github.com/user-attachments/assets/dcb48bee-0a6d-4b57-a9f7-8151a098f6ef)
Evidence: POS Locked Session PIN User Picker
Screenshot of locked POS terminal presenting PIN tile picker for quick staff switching.
![Subsequent Identical Order Checkout Modal](https://github.com/user-attachments/assets/3916360d-a866-4430-a72e-ea52b43c288e)
Evidence: Subsequent Identical Order Checkout Modal
Screenshot demonstrating prepaid checkout modal open for a subsequent order with identical items, proving false-positive committed cart duplicate blocking is resolved.
![Subsequent Identical Order Paid Successfully](https://github.com/user-attachments/assets/2916d62d-85d8-4338-a7a6-3b16f8890c16)
Evidence: Subsequent Identical Order Paid Successfully
Screenshot showing the subsequent order successfully paid with cash, verifying end-to-end checkout completion.
Evidence: Daily Invoices Export HTML with Custom Eco Fee

Rendered daily invoices HTML export containing itemised Eco Fee in invoice-totals section.

<!doctype html>
<html lang="th-TH">
<head>
<meta charset="utf-8">
<title>Compiled Daily Invoices — 2026-10-30</title>
<style>
  @page { size: A4 portrait; margin: 10mm; }
  * { box-sizing: border-box; }
  body {
    font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Noto Sans Arabic", "Noto Sans Hebrew", "Noto Sans Devanagari", "Noto Sans Bengali", "Noto Sans Tamil", "Noto Sans Thai", "Noto Sans CJK SC", "PingFang SC", "Microsoft YaHei", "Hiragino Sans", "Malgun Gothic", sans-serif;
    color: #0f172a;
    margin: 0;
    padding: 10px;
    font-size: 9.5px;
    line-height: 1.3;
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
  .cover-page {
    padding: 2px 0 6px;
  }
  .cover-page.has-invoices {
    page-break-after: always;
    break-after: page;
  }
  .cover-header {
    border-bottom: 2px solid #0f172a;
    padding-bottom: 6px;
    margin-bottom: 8px;
  }
  .cover-header h1 {
    font-size: 15px;
    margin: 0 0 3px;
  }
  .cover-store-details {
    font-size: 10.5px;
    color: #1e293b;
    margin-bottom: 3px;
  }
  .cover-meta {
    font-size: 9.5px;
    color: #475569;
  }
  .kpi-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 6px;
    margin: 8px 0 10px;
  }
  .kpi-card {
    border: 1px solid #cbd5e1;
    border-radius: 4px;
    padding: 5px 7px;
    background: #f8fafc;
  }
  .kpi-label {
    font-size: 8.5px;
    text-transform: uppercase;
    color: #475569;
    letter-spacing: 0.03em;
  }
  .kpi-value {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Noto Sans Arabic", "Noto Sans Hebrew", "Noto Sans Devanagari", "Noto Sans Bengali", "Noto Sans Tamil", "Noto Sans Thai", "Noto Sans CJK SC", "PingFang SC", "Microsoft YaHei", "Hiragino Sans", "Malgun Gothic", sans-serif;
    font-size: 12px;
    font-weight: 700;
    color: #0f172a;
    margin-top: 2px;
  }
  h2 {
    font-size: 11px;
    margin: 8px 0 4px;
    border-bottom: 1px solid #cbd5e1;
    padding-bottom: 2px;
  }
  table {
    width: 100%;
    border-collapse: collapse;
    margin-bottom: 6px;
    table-layout: fixed;
  }
  th, td {
    border: 1px solid #cbd5e1;
    padding: 2px 5px;
    text-align: left;
    vertical-align: top;
    overflow-wrap: anywhere;
  }
  th {
    background: #f1f5f9;
    font-weight: 600;
  }
  td.num, th.num {
    text-align: right;
    font-variant-numeric: tabular-nums;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Noto Sans Arabic", "Noto Sans Hebrew", "Noto Sans Devanagari", "Noto Sans Bengali", "Noto Sans Tamil", "Noto Sans Thai", "Noto Sans CJK SC", "PingFang SC", "Microsoft YaHei", "Hiragino Sans", "Malgun Gothic", sans-serif;
  }
  td.mono, .mono {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Noto Sans Arabic", "Noto Sans Hebrew", "Noto Sans Devanagari", "Noto Sans Bengali", "Noto Sans Tamil", "Noto Sans Thai", "Noto Sans CJK SC", "PingFang SC", "Microsoft YaHei", "Hiragino Sans", "Malgun Gothic", sans-serif;
  }
  td.muted {
    color: #64748b;
    font-style: italic;
  }
  .invoices-flow {
    display: block;
  }
  .appendix-section {
    page-break-before: always;
    break-before: page;
    padding-top: 4px;
  }
  .invoice-card {
    border: 1px solid #94a3b8;
    border-radius: 4px;
    padding: 7px 9px;
    margin-bottom: 8px;
    background: #ffffff;
    page-break-inside: avoid;
    break-inside: avoid;
  }
  .invoice-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    border-bottom: 1px solid #e2e8f0;
    padding-bottom: 4px;
    margin-bottom: 4px;
  }
  .invoice-store-name {
    font-size: 11px;
    font-weight: 700;
  }
  .invoice-store-meta {
    font-size: 9px;
    color: #475569;
  }
  .invoice-title-right {
    text-align: right;
  }
  .invoice-number {
    font-size: 11px;
    font-weight: 700;
    margin-top: 2px;
  }
  .status-badge {
    display: inline-block;
    padding: 1px 5px;
    border-radius: 3px;
    font-size: 9px;
    font-weight: 700;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Noto Sans Arabic", "Noto Sans Hebrew", "Noto Sans Devanagari", "Noto Sans Bengali", "Noto Sans Tamil", "Noto Sans Thai", "Noto Sans CJK SC", "PingFang SC", "Microsoft YaHei", "Hiragino Sans", "Malgun Gothic", sans-serif;
  }
  .status-paid {
    background: #dcfce7;
    color: #166534;
    border: 1px solid #86efac;
  }
  .status-partially_refunded {
    background: #fef3c7;
    color: #92400e;
    border: 1px solid #fcd34d;
  }
  .status-refunded {
    background: #fee2e2;
    color: #991b1b;
    border: 1px solid #fca5a5;
  }
  .invoice-meta-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 2px 10px;
    font-size: 9px;
    margin-bottom: 5px;
    color: #334155;
  }
  .invoice-items-table {
    margin-bottom: 5px;
    font-size: 9px;
  }
  .invoice-items-table th, .invoice-items-table td {
    padding: 2px 4px;
  }
  .item-meta {
    font-size: 8px;
    color: #64748b;
  }
  .invoice-footer-grid {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: 10px;
    font-size: 9px;
  }
  .invoice-payments {
    flex: 1;
  }
  .payment-pill {
    display: inline-block;
    background: #f1f5f9;
    border: 1px solid #cbd5e1;
    border-radius: 3px;
    padding: 1px 4px;
    margin-right: 4px;
    margin-bottom: 2px;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Noto Sans Arabic", "Noto Sans Hebrew", "Noto Sans Devanagari", "Noto Sans Bengali", "Noto Sans Tamil", "Noto Sans Thai", "Noto Sans CJK SC", "PingFang SC", "Microsoft YaHei", "Hiragino Sans", "Malgun Gothic", sans-serif;
  }
  .invoice-totals {
    width: 210px;
  }
  .totals-line {
    display: flex;
    justify-content: space-between;
    padding: 1px 0;
  }
  .totals-grand {
    border-top: 1px solid #0f172a;
    margin-top: 2px;
    padding-top: 2px;
    font-weight: 700;
    font-size: 10px;
  }
  .refund-box {
    margin-top: 5px;
    padding: 4px 6px;
    background: #fff1f2;
    border: 1px solid #fecdd3;
    border-radius: 3px;
    font-size: 9px;
    color: #881337;
  }
  .refund-box-header {
    display: flex;
    justify-content: space-between;
    margin-bottom: 2px;
  }
  .refund-list {
    margin: 0;
    padding-left: 14px;
  }
  .empty-invoices {
    padding: 12px;
    border: 1px dashed #cbd5e1;
    color: #64748b;
    text-align: center;
  }
</style>
</head>
<body>
  <section class="cover-page has-invoices" data-section="cover-page">
    <header class="cover-header">
      <h1>Daily Invoices &amp; Fiscal Summary Report</h1>
      <div class="cover-store-details">
        <strong>FloCafe Store</strong>
        · <span>Tax ID: N/A</span>
        
      </div>
      <div class="cover-meta">
        Business Date: <strong>2026-10-30</strong>
        · Timezone: Asia/Bangkok
        · Day Start: 00:00
        · Currency: THB
      </div>
    </header>

    <div class="kpi-grid" data-section="cover-kpis">
      <div class="kpi-card" data-kpi="total_invoices_count" data-value="2">
        <div class="kpi-label">Total Invoices Count</div>
        <div class="kpi-value">2</div>
      </div>
      <div class="kpi-card" data-kpi="total_gross_sales" data-value="138.4">
        <div class="kpi-label">Total Gross Sales (THB)</div>
        <div class="kpi-value">฿138.40</div>
      </div>
      <div class="kpi-card" data-kpi="net_base" data-value="130">
        <div class="kpi-label">Net Base (THB)</div>
        <div class="kpi-value">฿130.00</div>
      </div>
      <div class="kpi-card" data-kpi

... [5441 bytes truncated] ...

></tr>
<tr data-section="summary" data-metric="gross_margin_percent" data-value=""><td class="mono">gross_margin_percent</td><td class="mono"></td></tr>
<tr data-section="summary" data-metric="cost_coverage_status" data-value="unknown"><td class="mono">cost_coverage_status</td><td class="mono">unknown</td></tr>
<tr data-section="summary" data-metric="tax_detail_complete" data-value="true"><td class="mono">tax_detail_complete</td><td class="mono">true</td></tr>
<tr data-section="summary" data-metric="tax_detail_warning" data-value=""><td class="mono">tax_detail_warning</td><td class="mono"></td></tr>
<tr data-section="summary" data-metric="payment_cash" data-value="74.2"><td class="mono">payment_cash</td><td class="num">฿74.20</td></tr>
<tr data-section="summary" data-metric="payment_card" data-value="64.2"><td class="mono">payment_card</td><td class="num">฿64.20</td></tr>
      </tbody>
    </table>
  </section>

  <section class="invoices-flow" data-section="invoices">
    <h2>Issued Invoices (2)</h2>
    <article class="invoice-card" data-section="invoice" data-bill-number="INV-20261030-0001" data-order-number="ORD-20261030-0001" data-status="paid">
              <header class="invoice-header">
                <div class="invoice-store">
                  <div class="invoice-store-name">FloCafe Store</div>
                  <div class="invoice-store-meta">
                    <span>Tax ID: N/A</span>
                    
                  </div>
                </div>
                <div class="invoice-title-right">
                  <span class="status-badge status-paid" data-status-badge="[PAID]">[PAID]</span>
                  <div class="invoice-number mono">Invoice #INV-20261030-0001</div>
                </div>
              </header>

              <div class="invoice-meta-grid">
                <div><strong>Order:</strong> <span class="mono">ORD-20261030-0001</span> (takeaway)</div>
                <div><strong>Paid:</strong> <span class="mono">2026-10-30 06:38:34</span> (Created: <span class="mono">2026-10-30 06:38:34</span>)</div>
                <div><strong>Staff:</strong> E2E owner</div>
                <div><strong>Customer:</strong> Walk-in</div>
              </div>

              <table class="invoice-items-table">
                <thead>
                  <tr>
                    <th class="num" style="width: 8%;">Qty</th>
                    <th style="width: 44%;">Item</th>
                    <th class="num" style="width: 16%;">Unit (THB)</th>
                    <th class="num" style="width: 16%;">Subtotal (THB)</th>
                    <th class="num" style="width: 16%;">Total (THB)</th>
                  </tr>
                </thead>
                <tbody>
                  <tr class="invoice-item-row" data-item-subtotal="60" data-item-total="64.2">
                  <td class="num">1</td>
                  <td>
                    <span class="item-name">E2E Coffee</span>
                    
                  </td>
                  <td class="num">฿60.00</td>
                  <td class="num" data-col="item-subtotal">฿60.00</td>
                  <td class="num" data-col="item-total">฿64.20</td>
                </tr>
                </tbody>
              </table>

              <div class="invoice-footer-grid">
                <div class="invoice-payments">
                  <div class="payments-label"><strong>Payments:</strong> <span class="payment-pill">card: ฿64.20 (Tx: TX-LIVE-001)</span></div>
                </div>
                <div class="invoice-totals">
                  <div class="totals-line"><span>Subtotal</span><span class="mono">฿60.00</span></div>
                  
                  
                  <div class="totals-line tax-component-line"><span>Tax (7.00%)</span><span class="mono">฿4.20</span></div>
                  
                  
                  <div class="totals-line totals-grand"><span>Total (THB)</span><span class="mono">฿64.20</span></div>
                </div>
              </div>

              
            </article>
<article class="invoice-card" data-section="invoice" data-bill-number="INV-20261030-0002" data-order-number="ORD-20261030-0002" data-status="paid">
              <header class="invoice-header">
                <div class="invoice-store">
                  <div class="invoice-store-name">FloCafe Store</div>
                  <div class="invoice-store-meta">
                    <span>Tax ID: N/A</span>
                    
                  </div>
                </div>
                <div class="invoice-title-right">
                  <span class="status-badge status-paid" data-status-badge="[PAID]">[PAID]</span>
                  <div class="invoice-number mono">Invoice #INV-20261030-0002</div>
                </div>
              </header>

              <div class="invoice-meta-grid">
                <div><strong>Order:</strong> <span class="mono">ORD-20261030-0002</span> (takeaway)</div>
                <div><strong>Paid:</strong> <span class="mono">2026-10-30 06:38:35</span> (Created: <span class="mono">2026-10-30 06:38:35</span>)</div>
                <div><strong>Staff:</strong> E2E owner</div>
                <div><strong>Customer:</strong> Walk-in</div>
              </div>

              <table class="invoice-items-table">
                <thead>
                  <tr>
                    <th class="num" style="width: 8%;">Qty</th>
                    <th style="width: 44%;">Item</th>
                    <th class="num" style="width: 16%;">Unit (THB)</th>
                    <th class="num" style="width: 16%;">Subtotal (THB)</th>
                    <th class="num" style="width: 16%;">Total (THB)</th>
                  </tr>
                </thead>
                <tbody>
                  <tr class="invoice-item-row" data-item-subtotal="60" data-item-total="64.2">
                  <td class="num">1</td>
                  <td>
                    <span class="item-name">E2E Coffee</span>
                    
                  </td>
                  <td class="num">฿60.00</td>
                  <td class="num" data-col="item-subtotal">฿60.00</td>
                  <td class="num" data-col="item-total">฿64.20</td>
                </tr>
                </tbody>
              </table>

              <div class="invoice-footer-grid">
                <div class="invoice-payments">
                  <div class="payments-label"><strong>Payments:</strong> <span class="payment-pill">cash: ฿74.20</span></div>
                </div>
                <div class="invoice-totals">
                  <div class="totals-line"><span>Subtotal</span><span class="mono">฿60.00</span></div>
                  
                  <div class="totals-line"><span>Eco Fee</span><span class="mono">฿10.00</span></div>
                  <div class="totals-line tax-component-line"><span>Tax (7.00%)</span><span class="mono">฿4.20</span></div>
                  
                  
                  <div class="totals-line totals-grand"><span>Total (THB)</span><span class="mono">฿74.20</span></div>
                </div>
              </div>

              
            </article>
  </section>

  <section class="appendix-section" data-section="daily-tax-appendix">
    <h2>Daily Tax Breakdown Appendix</h2>
    <table data-section="taxes">
      <thead>
        <tr>
          <th>Date</th>
          <th>Tax Name</th>
          <th class="num">Rate</th>
          <th class="num">Taxable Base</th>
          <th class="num">Tax Amount</th>
          <th class="num">Gross Amount</th>
        </tr>
      </thead>
      <tbody>
        <tr data-section="tax" data-date="2026-10-30" data-name="Tax" data-rate="7" data-base="120" data-tax="8.4" data-gross="128.4" data-shared-base="false" data-base-additive="true" data-incomplete-detail="false">
            <td class="mono">2026-10-30</td>
            <td>Tax</td>
            <td class="num">7.00%</td>
            <td class="num">฿120.00</td>
            <td class="num">฿8.40</td>
            <td class="num">฿128.40</td>
          </tr>
      </tbody>
    </table>
    
  </section>
</body>
</html>
Evidence: Scenario 1 Idle Lock Policy JSON Evidence

Log verifying authenticated server staff reading idle lock policy (200 OK) while unauthenticated requests return 401.

{
  "serverReadStatus": 200,
  "serverResponseBody": {
    "setting": {
      "key": "auto_lock_minutes",
      "value": "0",
      "updated_at": null
    }
  },
  "unauthStatus": 401,
  "unauthResponseBody": {
    "error": "Authentication required"
  },
  "ghostUserStatus": 401,
  "ghostUserResponseBody": {
    "error": "Invalid or expired token"
  }
}
Evidence: Scenario 2 Payment Correction Authorization JSON Evidence

Log demonstrating manager payment correction succeeding with payments.take (200 OK) and rejecting with 403 when payments.take is revoked.

{
  "successfulCorrection": {
    "bill": {
      "id": 1,
      "bill_number": "INV-20261030-0001",
      "order_id": 1,
      "customer_id": null,
      "subtotal": 60,
      "tax_amount": 4.2,
      "tax_breakdown": [
        {
          "title": "Tax",
          "rate": 7,
          "amount": 4.2
        }
      ],
      "tax_snapshot": [
        {
          "packId": "test-th-pack",
          "packVersion": "1.0.0",
          "effectiveFrom": "2026-01-01",
          "taxRounding": {
            "scope": "line",
            "method": "half_up",
            "decimalPlaces": 2,
            "remainderAllocation": "largest_remainder"
          },
          "payableRounding": {
            "increment": "0.01",
            "method": "half_up"
          },
          "appliedRuleIds": [
            "tax"
          ],
          "lines": [
            {
              "lineId": "line-0001:e2e-product",
              "categoryId": "standard",
              "categorySource": "explicit",
              "taxBehavior": "exclusive",
              "grossAmount": "60.00",
              "taxableBase": "60.00",
              "taxAmount": "4.20",
              "components": [
                {
                  "ruleId": "tax",
                  "label": "Tax",
                  "type": "percent",
                  "rate": "7",
                  "baseRuleIds": [],
                  "amount": "4.20",
                  "roundingRemainder": "0"
                }
              ]
            }
          ],
          "country": "TH",
          "currency": "THB",
          "merchantOverridesApplied": []
        }
      ],
      "discount_amount": 0,
      "discount_type": null,
      "discount_value": null,
      "discount_reason": null,
      "delivery_charge": 0,
      "packaging_charge": 0,
      "service_charge": 0,
      "charges_breakdown": null,
      "round_off": 0,
      "total": 64.2,
      "paid_amount": 64.2,
      "balance": 0,
      "payment_status": "paid",
      "payment_details": [
        {
          "method": "card",
          "cash_session_id": 1,
          "amount": 64.2,
          "requested_amount": 64.2,
          "amount_omitted": false,
          "timestamp": "2026-10-29 23:38:34",
          "transaction_id": "TX-LIVE-001"
        }
      ],
      "paid_at": "2026-10-29 23:38:34",
      "printed_at": null,
      "created_at": "2026-10-29 23:38:34",
      "updated_at": "2026-10-29 23:38:34",
      "split_group_id": null,
      "split_label": null
    },
    "audit": {
      "id": 1,
      "actor_user_id": "e2e-manager",
      "action": "bill.payment_method.corrected",
      "bill_id": 1,
      "bill_number": "INV-20261030-0001",
      "order_id": 1,
      "payment_index": 0,
      "amount": 64.2,
      "old_method": "cash",
      "old_payment_method_id": null,
      "old_transaction_id": null,
      "new_method": "card",
      "new_payment_method_id": null,
      "new_transaction_id": "TX-LIVE-001",
      "actor_id": "e2e-manager",
      "approver_id": "e2e-manager",
      "approved_by": "e2e-manager",
      "reason": "Customer switched to card",
      "cash_session_id": 1,
      "idempotency_key": "idem-corr-1793317114903",
      "timestamp": "2026-10-29 23:38:34",
      "created_at": "2026-10-29 23:38:34"
    }
  },
  "deniedAttemptStatus": 403,
  "deniedAttemptBody": {
    "error": "Insufficient permissions",
    "code": "permission_denied",
    "permission": "payments.take"
  },
  "persistedBillPaymentMethod": "card"
}

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 4 of 4 scenarios driven live against the product
Scenario Result Live Evidence
Authenticated staff reads idle-lock policy while unauthenticated or deactivated requests are rejected ✅ pass live ~/.no-mistakes/evidence/01M64ZSQNNR1Q74M6KMYT8RX7B/scenario-1-idle-lock-policy.json
Payment method correction transaction rechecks payments.take permission ✅ pass live ~/.no-mistakes/evidence/01M64ZSQNNR1Q74M6KMYT8RX7B/scenario-2-payment-correction-permission.json
Daily Invoices HTML export includes custom charges in totals breakdown ✅ pass live ~/.no-mistakes/evidence/01M64ZSQNNR1Q74M6KMYT8RX7B/daily-invoices-export.html
POS session switch isolates checkout continuations and permits subsequent identical orders ✅ pass live ~/.no-mistakes/evidence/01M64ZSQNNR1Q74M6KMYT8RX7B/03-subsequent-identical-order-modal.png
  • npm run test:payment-method-correction
  • npm run test:daily-invoices-export
  • npm run test:pin-quick-switch
  • cd frontend && npx playwright test e2e/pin-quick-switch.spec.ts --project=chromium
  • cd frontend && npx playwright test e2e/live-product-validation.spec.ts --project=chromium
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@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 checkout session races, payment authorization, and invoice charges

🐞 Bug fix 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Stop stale POS checkout work after session changes while preserving the original user's recovery
 records.
• Recheck payment permissions during correction and expose idle-lock policy to all active staff.
• Include custom charges in archived invoices, with regression tests and updated documentation.
Diagram

graph TD
  A["Auth session"] --> B["POS checkout"] -->|pinned token| C["Order API"] --> D["Recovery record"] --> E{"Session current?"}
  E -->|Yes| F["Billing and payment"] --> G["Cart completion"]
  E -->|No| H["Stop checkout"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Centralize session-aware checkout orchestration
  • ➕ Could enforce continuation checks consistently across checkout flows.
  • ➖ Requires a broader checkout refactor and still needs durable, user-scoped recovery for server-committed work.

Recommendation: Keep the targeted session guards and pinned credentials for this defect fix: they protect existing checkout flows without restructuring financial operations. Consider a shared orchestration layer only if more checkout paths need the same lifecycle controls.

Files changed (10) +1013 / -174

Bug fix (5) +422 / -161
page.tsxBind checkout continuations to their originating session +356/-145

Bind checkout continuations to their originating session

• Guards asynchronous order, billing, payment, printing, and cart continuations against session changes. Pins request credentials and keeps retry records scoped to the originating user, including after a server-committed response.

frontend/src/app/(dashboard)/pos/page.tsx

api.tsPreserve explicitly supplied authorization headers +9/-1

Preserve explicitly supplied authorization headers

• Stops the request interceptor from replacing an originating checkout token with the token currently in local storage.

frontend/src/lib/api.ts

bills.tsRecheck payment permission inside correction transaction +2/-1

Recheck payment permission inside correction transaction

• Requires the live caller to retain 'payments.take' when a payment-method correction transaction runs, closing the permission-revocation window during PIN verification.

main/routes/bills.ts

settings.tsMake idle-lock policy readable by active staff +22/-11

Make idle-lock policy readable by active staff

• Replaces feature-permission gating on 'GET /auto_lock_minutes' with an active authenticated-user check, so reports-only staff receive the configured timeout.

main/routes/settings.ts

daily-invoices-export.tsRender persisted custom charges on archived invoices +33/-3

Render persisted custom charges on archived invoices

• Carries charge breakdowns into invoice cards and renders their receipt charge lines. Retains standard-charge fallbacks for legacy bills without a breakdown.

main/services/daily-invoices-export.ts

Tests (3) +577 / -3
daily-invoices-export.test.tsCover custom and legacy invoice charge lines +107/-0

Cover custom and legacy invoice charge lines

• Adds an archived Eco Fee invoice and a legacy invoice to verify both custom-charge rendering and standard-charge fallbacks.

tests/daily-invoices-export.test.ts

payment-method-correction.test.tsCover permission revocation during PIN verification +53/-0

Cover permission revocation during PIN verification

• Revokes 'payments.take' while PIN comparison is in flight and verifies that correction returns 403 without changing the payment method.

tests/payment-method-correction.test.ts

pin-quick-switch.test.tsCover idle-lock access and interrupted POS checkout +417/-3

Cover idle-lock access and interrupted POS checkout

• Verifies reports-only staff can read a nonzero idle-lock timeout. Adds a deferred checkout test showing that a user switch prevents billing and payment, preserves the next user's cart, and retains the original user's order recovery record.

tests/pin-quick-switch.test.ts

Documentation (2) +14 / -10
authentication-and-authorization.mdDocument checkout and authorization boundaries +9/-6

Document checkout and authorization boundaries

• Clarifies transactional payment permission requirements, staff access to the idle-lock policy, and how session changes halt checkout while retaining recovery state.

docs/architecture/authentication-and-authorization.md

api.mdUpdate payment, settings, and invoice API behavior +5/-4

Update payment, settings, and invoice API behavior

• Documents revoked payment permissions, universal authenticated-staff access to the idle-lock setting, and itemized custom charges in archived invoices.

docs/reference/api.md

@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. A switched user's checkout can reopen ✓ Resolved
Description
The new onPayment wrapper checks the checkoutTable value captured when the old modal rendered,
rather than whether that modal still belongs to the current session. If bill generation finishes
after a user switch, its callback can set paymentBill for the old bill, which the new user can
then pay with their current token.
Code

frontend/src/app/(dashboard)/pos/page.tsx[2994]

+            if (!checkoutTable) return;
Relevance

●●● Strong

Recent checkout precedent accepts preventing stale asynchronous callbacks from reopening or paying
the wrong bill.

PR-#990

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The modal awaits /bills/generate before calling its captured callback; the new wrapper's closure
retains the old truthy checkoutTable even after the session effect closes that modal. PaymentModal
subsequently posts payment for the accepted bill without a pinned originating-session token.

frontend/src/components/pos/TableCheckoutModal.tsx[103-116]
frontend/src/app/(dashboard)/pos/page.tsx[330-349]
frontend/src/app/(dashboard)/pos/page.tsx[2970-3002]
frontend/src/components/pos/PaymentModal.tsx[1103-1112]

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 in-flight table checkout can invoke its old payment callback after a session switch; the captured `checkoutTable` check does not reject it.
## Fix Focus Areas
- frontend/src/app/(dashboard)/pos/page.tsx[2970-3002]
- frontend/src/components/pos/TableCheckoutModal.tsx[103-116]
## Recommended Fix
Bind callbacks to the modal's originating session and table instance. Check that identity against current session state before accepting a generated bill, and stop stale modal continuations after their awaits.

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



Remediation recommended

2. An already-paid cart can be charged again after a user switch ✓ Resolved
Description
handlePlaceOrder can clear a confirmed postpaid attempt before awaiting held-order cleanup, then
return at isSessionCurrent() without clearing the cart; the prepaid checkout can likewise return
after a confirmed payment without clearing it. If the session switches after either server commit,
the shared cart retains already-ordered or paid items, while the originating user's retry record is
unavailable to the new user, whose next checkout can mint fresh idempotency keys and submit the cart
again.
Code

frontend/src/app/(dashboard)/pos/page.tsx[R1999-2002]

+      if (!isSessionCurrent()) {
+        savePrepaidAttempt({ ...attempt, order, bill: paidBill }, originUserId);
+        return;
+      }
Relevance

●●● Strong

Recent POS precedents accept stale-session protections; this directly targets committed checkout
recovery and shared-cart isolation.

PR-#990
PR-#1326

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The postpaid path saves the successful order and clears its attempt before awaiting
removeHeldOrder; a session change during that await reaches the early return before
cart.clearCart() (1536–1613). Other stale branches likewise return after a committed order or
confirmed payment without clearing the cart (1861–1865, 1999–2002, 2117). The cart survives user
switches, but readPostpaidAttempt(originUserId) and readPrepaidAttempt(originUserId) use
user-keyed storage, so a different user finds no matching attempt and constructs one with
newIdempotencyKey() (1494–1498, 1789–1797).

frontend/src/app/(dashboard)/pos/page.tsx[1536-1541]
frontend/src/app/(dashboard)/pos/page.tsx[1999-2002]
frontend/src/app/(dashboard)/pos/page.tsx[511-524]
frontend/src/app/(dashboard)/pos/page.tsx[1786-1797]
frontend/src/app/(dashboard)/pos/page.tsx[1515-1555]
frontend/src/app/(dashboard)/pos/page.tsx[1567-1574]
frontend/src/app/(dashboard)/pos/page.tsx[1611-1613]
frontend/src/app/(dashboard)/pos/page.tsx[1466-1498]

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

## Issue description
A session switch after an order or payment commits can leave the shared cart intact. In the postpaid path, this can happen during held-order cleanup after the confirmed attempt has been cleared; in other stale paths, the attempt remains scoped to the originating user. A different user can therefore submit the same cart with fresh idempotency keys.

## Fix Focus Areas
- frontend/src/app/(dashboard)/pos/page.tsx[1536-1613]
- frontend/src/app/(dashboard)/pos/page.tsx[1861-1865]
- frontend/src/app/(dashboard)/pos/page.tsx[1960-1963]
- frontend/src/app/(dashboard)/pos/page.tsx[1999-2002]

## Recommended Fix
When a stale continuation observes a committed order or paid bill, resolve the submitted cart safely: clear it if its fingerprint still matches, or record a terminal-level committed marker keyed by that fingerprint. Make checkout under any user detect the marker and show a recovery notice identifying the already placed or paid order instead of minting new idempotency keys. Keep the confirmed postpaid attempt available for the originating user's recovery until its cart has been safely resolved, including when the session changes during held-order cleanup.

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


3. Archived invoices can show wrong fees ✓ Resolved
Description
buildDailyInvoicesExportDataset uses the order's charges_breakdown whenever the archived bill's
breakdown is null, even though it takes the invoice amounts and total from the bill. When those rows
have different charge snapshots, the serializer prints the order's fees beside the bill's historical
total; its legacy-column fallback cannot correct the mismatch.
Code

main/services/daily-invoices-export.ts[638]

+        COALESCE(b.charges_breakdown, o.charges_breakdown) AS charges_breakdown,
Relevance

●● Moderate

Recent export correctness fixes were accepted, but fallback semantics for mismatched archived
snapshots remain uncertain.

PR-#1393

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The query mixes an order-sourced breakdown with bill-sourced totals, and the serializer renders its
amounts directly. Bill synchronization updates unpaid bills only, so an archived bill need not
acquire a later order breakdown.

main/services/daily-invoices-export.ts[631-641]
main/services/daily-invoices-export.ts[1228-1251]
main/routes/bills.ts[2274-2295]

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

## Issue description
A null archived-bill breakdown causes the exporter to display fees from the order row, which need not match the bill's recorded totals.
## Fix Focus Areas
- main/services/daily-invoices-export.ts[635-640]
- main/services/daily-invoices-export.ts[1228-1251]
## Recommended Fix
Use only the bill's persisted breakdown for itemised invoice charges. If it is absent, render the bill's legacy charge columns; add a test where the order has a breakdown but the archived bill does not.

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


Grey Divider

Context sources
✅ Linked PRs:
  🌐 PR #1392
  🌐 PR #1394
  🌐 PR #1400
❌ Tickets: failed to load — Ticket collection could not establish complete evidence
✅ Compliance rules (platform): 5 rules
✅ Cross-repo context — repo relationships
Review mode: Auto: 🧠 Deep: Broad, security-sensitive changes span multiple independent paths with dense asynchronous logic.

Grey Divider

Tip of the day
💡 Did you know, you can route each severity your way: inline, summary, both, or drop

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread main/services/daily-invoices-export.ts Resolved
@khaira777 khaira777 changed the title fix: resolve post-merge defects across checkout sessions, payment corrections, and invoice exports fix(pos,billing): close post-merge review gaps Oct 29, 2026
@khaira777
khaira777 force-pushed the fm/flocafe-fix-post-merge-review-1392-1394-1400 branch from 3efec60 to 224d902 Compare October 29, 2026 23:25
- Guard async checkout continuations with session epoch and user identity, scoped retry records, and preserved caller auth headers
- Recheck payments.take permission inside transactional payment-method correction
- Include custom charge breakdown lines in archived daily invoice export
- Allow all authenticated staff to read auto_lock_minutes idle-lock policy
…ard 2/4 (`e2e/order-attempt-storage.spec.ts:496` - "prepaid: losing the bill retry snapshot stops payment and reuses the original order key"). ### Invariant Analysis - **Invariant:** When local persistence of an in-flight checkout attempt fails after an upstream mutation has succeeded, the failure must be attributed to the corresponding stage (`retry_state`) and operation (`bill.generate` when persisting the generated bill, `payment.create` when persisting payment request changes on an existing bill), capturing the client diagnostic and safely halting checkout before payment. - **Root Cause:** In `frontend/src/app/(dashboard)/pos/page.tsx`, `requirePrepaidAttemptSaved` was called immediately following `POST /bills/generate` while `failureStage` was still `'bill_generation'`. When the test simulated storage quota exhaustion for records carrying a bill, `requirePrepaidAttemptSaved` threw while `failureStage === 'bill_generation'`, bypassing the error-handler branch that emits the `fc.client.bill.generate.retry_state_update` diagnostic capture (which requires `failureStage === 'retry_state'`). Furthermore, mutating `attempt.bill` before the retry-state block caused subsequent logic to classify the operation as `payment.create` instead of `bill.generate`. - **Fix:** In `frontend/src/app/(dashboard)/pos/page.tsx`, removed the premature `requirePrepaidAttemptSaved` call after bill generation. If the session changed during the request (`!isSessionCurrent()`), the attempt carrying the newly generated bill is saved under `originUserId` via `savePrepaidAttempt` before halting. On the normal active session continuation path, the retry-state update block executes with `attempt.bill` unset, correctly setting `failureOperation = 'bill.generate'` and `failureStage = 'retry_state'` before calling `requirePrepaidAttemptSaved(attempt, originUserId)`. - **Sibling Call Paths & Consumers:** - `POST /orders` (order creation): preserves recovery record under `originUserId` and halts if session changed; if storage fails, captures `fc.client.order.create.retry_state_update`. - `POST /bills/generate` (bill generation): preserves recovery record under `originUserId` and halts if session changed; if storage fails, captures `fc.client.bill.generate.retry_state_update`. - `POST /bills/:id/payments` (payment): preserves paid bill recovery record under `originUserId` and halts if session changed. ### Verification Run - `npm run clean`: Passed (ports 3001, 3002, 3003 free). - `node tests/run-electron-node-test.cjs tests/pin-quick-switch.test.ts`: Passed (all 8 sections, 100% green). - `node tests/run-electron-node-test.cjs tests/daily-invoices-export.test.ts`: Passed. - `node tests/run-electron-node-test.cjs tests/payment-method-correction.test.ts`: Passed. - `npm run build`: Passed. - `npm run build:frontend`: Passed. - `cd frontend && npx playwright test e2e/order-attempt-storage.spec.ts`: All 8 tests passed, including test at line 496. - `npm run lint`: Passed (backend 0 errors, frontend 0 errors). - `npm run format:check:changed -- --base b27799e`: Passed (10 files clean). - `npm run docs:check`: Passed. - `npm run i18n:check`: Passed
@khaira777
khaira777 force-pushed the fm/flocafe-fix-post-merge-review-1392-1394-1400 branch from 224d902 to 8463b5b Compare October 29, 2026 23:43
@khaira777 khaira777 changed the title fix(pos,billing): close post-merge review gaps fix: isolate POS checkout across sessions and resolve post-merge review defects Oct 29, 2026
…e '@/lib/cart-price'` by replacing the frontend-only path alias `@/` with relative sibling imports `./` in `frontend/src/lib/cart-order-item.ts` and `frontend/src/lib/cart-price.ts`. Verified with `npm run test:diagnostics`, `npm run test:issue-788-order-attempt-storage`, `npm run test:cart-variant-identity`, `npm run test:pin-quick-switch`, `npm run test:cart-price-override`, `npm run test:daily-invoices-export`, `npm run test:payment-method-correction`, `npm run lint`, `npm run build:frontend`, and `npm run build`
…Test Suite (Shard 2/4)` (`test:whatsapp-service`): ### 1. Invariant Quiet hours configured through the end of the day (`quietHoursEnd: '23:59'`) or across the entire day (`00:00` to `23:59`) must include the final minute of the day (`23:59`), ensuring automated WhatsApp messages and retries are consistently blocked with reason `quiet_hours` across all 1,440 minutes of the day. Every place where this invariant must hold: - Window check function: `isTimeWithinQuietHours` in [shared/whatsapp-automation.ts](file:///Users/gurkiratkhaira/.no-mistakes/worktrees/de2296f2e6f8/01M64ZSQNNR1Q74M6KMYT8RX7B/shared/whatsapp-automation.ts#L569-L584), which is the single shared boundary evaluating whether an HH:MM timestamp falls within quiet hours. - Consumer: `isCurrentlyQuietHours` in [main/services/whatsapp.ts](file:///Users/gurkiratkhaira/.no-mistakes/worktrees/de2296f2e6f8/01M64ZSQNNR1Q74M6KMYT8RX7B/main/services/whatsapp.ts#L606-L620), which formats the current tenant time and delegates to `isTimeWithinQuietHours`. - Sibling call path 1: `sendMessageInternal` in [main/services/whatsapp.ts](file:///Users/gurkiratkhaira/.no-mistakes/worktrees/de2296f2e6f8/01M64ZSQNNR1Q74M6KMYT8RX7B/main/services/whatsapp.ts#L2367-L2377), enforcing quiet hours on automated message sends. - Sibling call path 2: `retryMessage` in [main/services/whatsapp.ts](file:///Users/gurkiratkhaira/.no-mistakes/worktrees/de2296f2e6f8/01M64ZSQNNR1Q74M6KMYT8RX7B/main/services/whatsapp.ts#L2540-L2560), which routes retried automated messages through `sendMessageInternal`. - Test suites: [tests/whatsapp-service.test.ts](file:///Users/gurkiratkhaira/.no-mistakes/worktrees/de2296f2e6f8/01M64ZSQNNR1Q74M6KMYT8RX7B/tests/whatsapp-service.test.ts#L464-L476) and [tests/reports-whatsapp.test.ts](file:///Users/gurkiratkhaira/.no-mistakes/worktrees/de2296f2e6f8/01M64ZSQNNR1Q74M6KMYT8RX7B/tests/reports-whatsapp.test.ts#L1400-L1425). ### 2. Root Cause In `shared/whatsapp-automation.ts`, `isTimeWithinQuietHours` evaluated same-day windows (`start < end`) as a strict half-open interval `currentHhMm >= start && currentHhMm < end`. Because standard 24-hour time pickers only go up to `23:59`, configuring full-day quiet hours (`quietHoursStart: '00:00'` to `quietHoursEnd: '23:59'`) or quiet hours through midnight (`22:00` to `23:59`) excluded minute `23:59`. When CI Shard 2/4 executed `tests/whatsapp-service.test.ts` at `2026-10-29T23:59:03Z`, `isCurrentlyQuietHours` returned `false`, allowing `whatsapp.retryMessage(autoFailedId)` to succeed instead of failing with `quiet_hours`. ### 3. Fix Applied - In [shared/whatsapp-automation.ts](file:///Users/gurkiratkhaira/.no-mistakes/worktrees/de2296f2e6f8/01M64ZSQNNR1Q74M6KMYT8RX7B/shared/whatsapp-automation.ts#L579): updated same-day window condition to `currentHhMm >= start && (end === '23:59' || currentHhMm < end)`, ensuring windows terminating at `23:59` include the entire last minute of the day. - In [tests/whatsapp-service.test.ts](file:///Users/gurkiratkhaira/.no-mistakes/worktrees/de2296f2e6f8/01M64ZSQNNR1Q74M6KMYT8RX7B/tests/whatsapp-service.test.ts#L470-L476): added regression assertions verifying `isTimeWithinQuietHours` evaluates to `true` for all 23:59 edge cases. ### 4. Verification - `npm run test:whatsapp-service`: Passed (all WhatsApp service API assertions passed). - `env SHARD_TOTAL=4 SHARD_INDEX=1 node scripts/ci/run-test-shard.cjs`: Passed (all 57 test suites in Shard 2/4 passed). - `npm run lint`: Passed (0 errors). - `npm run build`: Passed (TypeScript build completed cleanly). - `npm run docs:check`: Passed (markdownlint and doc integrity verified). - `npm run i18n:check`: Passed (translation integrity verified). - `git diff --check`: Passed (no whitespace errors)
@khaira777 khaira777 changed the title fix: isolate POS checkout across sessions and resolve post-merge review defects fix(pos,billing): close post-merge review gaps Oct 30, 2026
@khaira777
khaira777 merged commit 53b1f6b into main Oct 30, 2026
23 checks passed
@khaira777
khaira777 deleted the fm/flocafe-fix-post-merge-review-1392-1394-1400 branch October 30, 2026 00:21
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(pos,billing): close post-merge review gaps

1 participant