Skip to content

fix(expenses): guard aggregate amount ranges - #1160

Merged
khaira777 merged 9 commits into
mainfrom
codex/expense-aggregate-bounds
Oct 15, 2026
Merged

khaira777 merged 9 commits into
mainfrom
codex/expense-aggregate-bounds

Conversation

@khaira777

@khaira777 khaira777 commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Intent

PR 8 - Bound expense aggregates safely (Fixes #1059)
Branch: codex/expense-aggregate-bounds
Title: fix(expenses): guard aggregate amount ranges

Preserve the public numeric minor-unit fields; do not invent a merchant monetary cap.

Requirements:

  • Read integer aggregates exactly using the installed SQLite driver's BigInt support.
  • Before create/replace commits, ensure the resulting active expense total for that currency does not exceed Number.MAX_SAFE_INTEGER.
  • For replacement, subtract the replaced active amount before testing the new total.
  • Keep checks inside the transaction and retain idempotent replay behaviour.
  • Convert to JavaScript numbers only after safe-range validation.
  • Handle legacy oversized aggregates and SQLite integer overflow as a controlled 422 supported-range error, not 500.
  • Return no rounded, saturated or silently omitted totals.
  • Keep list/detail and recovery actions usable when a legacy summary is out of range. Existing records may be voided/replaced through normal audited operations, including voiding an oversized legacy row directly without precision-losing rounding.
  • Do not truncate, delete or migrate expense data.

What Changed

  • Read expense and payment aggregates using better-sqlite3 safeIntegers(true) (BigInt), validating that totals fit within Number.MAX_SAFE_INTEGER before converting to JavaScript numbers and returning 422 amount_range_exceeded on out-of-range totals or SQLite integer overflow.
  • Enforce per-currency active expense total bounds inside createExpense and replaceExpense transactions (subtracting the replaced active amount before validating replacement totals) while preserving idempotent replay behavior.
  • Project amount_minor and due_minor as null when a legacy row's stored amount exceeds Number.MAX_SAFE_INTEGER so list, detail, void, and replace recovery operations remain usable without precision loss, and expand unit/API test coverage and API reference documentation.

Risk Assessment

✅ Low: The change cleanly enforces BigInt-backed safe-integer aggregate bounds on expense totals, handles SQLite int64 overflow and legacy oversized rows with controlled 422 responses, and projects oversized individual row amounts as null so list, detail, payment reversal, void, and replacement recovery paths remain usable without precision loss.

Testing

Ran the targeted expense test suites (npm run test:expenses, covering expense-attempt.test.ts, expense-records.test.ts, expense-api.test.ts, and expense-payments.test.ts) and stood up an isolated live Express + SQLite server to drive all five boundary, adversarial overflow, replacement, and legacy recovery scenarios end-to-end over HTTP, recording the responses and persisted SQLite state in ~/.no-mistakes/evidence/01M4YJM5EGVR5ANVQWZ26DAF3Q/expense-aggregate-bounds-live-verification.json; all checks and live scenarios passed. This is a backend service and HTTP API change with no UI/visual surface change, so HTTP response and database state artifacts were captured instead of screenshots.

  • Live validation: ✅ go - 5 of 5 scenarios driven live against the product
Scenario Result Live Evidence
Create expenses up to exact Number.MAX_SAFE_INTEGER per currency, replay the boundary create idempotently without re-triggering the bound guard, and read exact reconciled summary totals ✅ pass live ~/.no-mistakes/evidence/01M4YJM5EGVR5ANVQWZ26DAF3Q/expense-aggregate-bounds-live-verification.json (scenario_1_boundary_and_replay) and npm run test:expenses
Adversarially attempt to create an expense that pushes active currency incurred total past Number.MAX_SAFE_INTEGER (+1 and +100 minor units) and confirm atomic 422 amount_range_exceeded rejection with… ✅ pass live ~/.no-mistakes/evidence/01M4YJM5EGVR5ANVQWZ26DAF3Q/expense-aggregate-bounds-live-verification.json (scenario_2_adversarial_create_overflow) and npm run test:expenses
Replace an active expense at the Number.MAX_SAFE_INTEGER boundary: subtract the replaced active amount so an in-range replacement succeeds (and replays idempotently) while an over-boundary replacement… ✅ pass live ~/.no-mistakes/evidence/01M4YJM5EGVR5ANVQWZ26DAF3Q/expense-aggregate-bounds-live-verification.json (scenario_3_replacement_bounds) and npm run test:expenses
Query summary when legacy active rows exceed Number.MAX_SAFE_INTEGER or overflow SQLite signed 64-bit SUM(): confirm controlled 422 amount_range_exceeded instead of 500, while list, detail, void, and… ✅ pass live ~/.no-mistakes/evidence/01M4YJM5EGVR5ANVQWZ26DAF3Q/expense-aggregate-bounds-live-verification.json (scenario_4_legacy_aggregate_and_recovery) and npm run test:expenses
Recover a single oversized legacy expense row (INT64_MAX = 9223372036854775807) by reading list/detail with null amount_minor/due_minor (no precision-losing rounding), reversing its payment, voiding i… ✅ pass live ~/.no-mistakes/evidence/01M4YJM5EGVR5ANVQWZ26DAF3Q/expense-aggregate-bounds-live-verification.json (scenario_5_single_int64_max_row_recovery) and npm run test:expenses
Evidence: Live HTTP & SQLite Expense Aggregate Bounds Verification Evidence

{ "scenario_1_boundary_and_replay": { "expBoundary": { "status": 201, "amount_minor": 9007199254690991 }, "expBoundaryReplay": { "status": 201, "idempotentReplay": "true" }, "summaryAtBoundary": { "status": 200, "totals": [{ "currency_code": "INR", "expense_count": 2, "incurred_minor": 9007199254740991, "net_paid_minor": 20000, "due_minor": 9007199254720991 }] } }, "scenario_2_adversarial_create_overflow": { "over1": { "status": 422, "body": { "error": "Expense incurred total exceeds the supported amount range", "code": "amount_range_exceeded" } }, "over100": { "status": 422, "body": { "error": "Expense incurred total exceeds the supported amount range", "code": "amount_range_exceeded" } }, "rowsCommitted": 0, "receiptsCommitted": 0 }, "scenario_3_replacement_bounds": { "replaceOver": { "status": 422, "body": { "error": "Expense incurred total exceeds the supported amount range", "code": "amount_range_exceeded" } }, "replaceExact": { "status": 201 }, "replaceExactReplay": { "status": 201, "idempotentReplay": "true" } }, "scenario_4_legacy_aggregate_and_recovery": { "summaryLegacyOver": { "status": 422, "body": { "error": "Expense incurred total exceeds the supported amount range", "code": "amount_range_exceeded" } }, "listWhileOverStatus": 200, "detailWhileOverStatus": 200, "voidLegacy100Status": 200, "summaryAfterVoid100": { "status": 200, "totals": [{ "currency_code": "INR", "incurred_minor": 9007199254740991, "net_paid_minor": 20000, "due_minor": 9007199254720991 }] } }, "scenario_5_single_int64_max_row_recovery": { "summaryInt64Overflow": { "status": 422, "body": { "error": "Expense incurred total exceeds the supported amount range", "code": "amount_range_exceeded" } }, "detailInt64Active": { "status": 200, "amount_minor": null, "paid_minor": 500, "due_minor": null }, "reverseInt64Pay": { "status": 201, "paid_minor": 0 }, "voidInt64": { "status": 200, "status_field": "voided", "amount_minor": null, "due_minor": null }, "summaryAfterInt64Void": { "status": 200, "totals": [{ "currency_code": "INR", "incurred_minor": 9007199254740991 }] }, "persistedSqliteRow": { "amount_minor": "9223372036854775807", "void_reason": "Direct void of INT64_MAX legacy row" } } }

{
  "scenario_1_boundary_and_replay": {
    "expBoundary": {
      "status": 201,
      "id": "exp_b330cbbe-7cee-4878-a9f4-b6798b8c45af",
      "amount_minor": 9007199254690991
    },
    "expBoundaryReplay": {
      "status": 201,
      "idempotentReplay": "true",
      "id": "exp_b330cbbe-7cee-4878-a9f4-b6798b8c45af"
    },
    "summaryAtBoundary": {
      "status": 200,
      "totals": [
        {
          "currency_code": "INR",
          "expense_count": 2,
          "incurred_minor": 9007199254740991,
          "net_paid_minor": 20000,
          "due_minor": 9007199254720991
        }
      ]
    }
  },
  "scenario_2_adversarial_create_overflow": {
    "over1": {
      "status": 422,
      "idempotentReplay": null,
      "body": {
        "error": "Expense incurred total exceeds the supported amount range",
        "code": "amount_range_exceeded"
      }
    },
    "over100": {
      "status": 422,
      "idempotentReplay": null,
      "body": {
        "error": "Expense incurred total exceeds the supported amount range",
        "code": "amount_range_exceeded"
      }
    },
    "rowsCommitted": 0,
    "receiptsCommitted": 0
  },
  "scenario_3_replacement_bounds": {
    "replaceOver": {
      "status": 422,
      "idempotentReplay": null,
      "body": {
        "error": "Expense incurred total exceeds the supported amount range",
        "code": "amount_range_exceeded"
      }
    },
    "replaceExact": {
      "status": 201,
      "id": "exp_0ce9ff2f-7bfa-4290-9c13-c2c169acdefb",
      "replaced_expense_id": "exp_b330cbbe-7cee-4878-a9f4-b6798b8c45af"
    },
    "replaceExactReplay": {
      "status": 201,
      "idempotentReplay": "true"
    }
  },
  "scenario_4_legacy_aggregate_and_recovery": {
    "summaryLegacyOver": {
      "status": 422,
      "idempotentReplay": null,
      "body": {
        "error": "Expense incurred total exceeds the supported amount range",
        "code": "amount_range_exceeded"
      }
    },
    "listWhileOverStatus": 200,
    "detailWhileOverStatus": 200,
    "voidLegacy100Status": 200,
    "summaryAfterVoid100": {
      "status": 200,
      "totals": [
        {
          "currency_code": "INR",
          "expense_count": 2,
          "incurred_minor": 9007199254740991,
          "net_paid_minor": 20000,
          "due_minor": 9007199254720991
        }
      ]
    }
  },
  "scenario_5_single_int64_max_row_recovery": {
    "summaryInt64Overflow": {
      "status": 422,
      "idempotentReplay": null,
      "body": {
        "error": "Expense incurred total exceeds the supported amount range",
        "code": "amount_range_exceeded"
      }
    },
    "detailInt64Active": {
      "status": 200,
      "amount_minor": null,
      "paid_minor": 500,
      "due_minor": null
    },
    "reverseInt64Pay": {
      "status": 201,
      "paid_minor": 0
    },
    "voidInt64": {
      "status": 200,
      "status_field": "voided",
      "amount_minor": null,
      "due_minor": null
    },
    "summaryAfterInt64Void": {
      "status": 200,
      "totals": [
        {
          "currency_code": "INR",
          "expense_count": 2,
          "incurred_minor": 9007199254740991,
          "net_paid_minor": 20000,
          "due_minor": 9007199254720991
        }
      ]
    },
    "persistedSqliteRow": {
      "amount_minor": "9223372036854775807",
      "voided_at": "2026-10-15 01:45:33",
      "void_reason": "Direct void of INT64_MAX legacy row"
    }
  }
}

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • 🚨 main/services/expenses.ts:514 - projectExpenseRow only maps out-of-range amountRaw and dueRaw to null when row.voided_at !== null (allowOversizedInactive), and otherwise calls requireSafeTotal which throws 422 amount_range_exceeded. While an oversized legacy expense row (amount_minor > Number.MAX_SAFE_INTEGER) is still active (voided_at === null), every read and pre-void recovery path that projects the row fails with 422 instead of staying usable: listExpenses (main/services/expenses.ts:645) fails for the entire page whenever the active oversized row is in the page results; getExpense (main/services/expenses.ts:661) fails on the active row; reverseExpensePayment (main/services/expenses.ts:1347, main/services/expenses.ts:1390 via requireActiveExpense at main/services/expenses.ts:948) fails with 422 so any recorded payment on an oversized legacy row cannot be reversed to satisfy requireUnpaidOpenExpense (main/services/expenses.ts:984) before voiding or replacing; and recordExpensePayment (main/services/expenses.ts:1264) throws 422 in getExpense before runExpenseMutation, blocking idempotent replay and making the expense.due_minor === null check at main/services/expenses.ts:1281 unreachable. Map out-of-range amountRaw and dueRaw to null in projectExpenseRow regardless of row.voided_at (new and replaced rows are already constrained to safe integers on write, and summarizeExpenses still enforces requireSafeTotal on active aggregates).

🔧 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
Create expenses up to exact Number.MAX_SAFE_INTEGER per currency, replay the boundary create idempotently without re-triggering the bound guard, and read exact reconciled summary totals ✅ pass live ~/.no-mistakes/evidence/01M4YJM5EGVR5ANVQWZ26DAF3Q/expense-aggregate-bounds-live-verification.json (scenario_1_boundary_and_replay) and npm run test:expenses
Adversarially attempt to create an expense that pushes active currency incurred total past Number.MAX_SAFE_INTEGER (+1 and +100 minor units) and confirm atomic 422 amount_range_exceeded rejection with… ✅ pass live ~/.no-mistakes/evidence/01M4YJM5EGVR5ANVQWZ26DAF3Q/expense-aggregate-bounds-live-verification.json (scenario_2_adversarial_create_overflow) and npm run test:expenses
Replace an active expense at the Number.MAX_SAFE_INTEGER boundary: subtract the replaced active amount so an in-range replacement succeeds (and replays idempotently) while an over-boundary replacement… ✅ pass live ~/.no-mistakes/evidence/01M4YJM5EGVR5ANVQWZ26DAF3Q/expense-aggregate-bounds-live-verification.json (scenario_3_replacement_bounds) and npm run test:expenses
Query summary when legacy active rows exceed Number.MAX_SAFE_INTEGER or overflow SQLite signed 64-bit SUM(): confirm controlled 422 amount_range_exceeded instead of 500, while list, detail, void, and… ✅ pass live ~/.no-mistakes/evidence/01M4YJM5EGVR5ANVQWZ26DAF3Q/expense-aggregate-bounds-live-verification.json (scenario_4_legacy_aggregate_and_recovery) and npm run test:expenses
Recover a single oversized legacy expense row (INT64_MAX = 9223372036854775807) by reading list/detail with null amount_minor/due_minor (no precision-losing rounding), reversing its payment, voiding i… ✅ pass live ~/.no-mistakes/evidence/01M4YJM5EGVR5ANVQWZ26DAF3Q/expense-aggregate-bounds-live-verification.json (scenario_5_single_int64_max_row_recovery) and npm run test:expenses
  • npm run test:expenses
  • node tests/run-electron-node-test.cjs tests/expense-records.test.ts
  • node tests/run-electron-node-test.cjs tests/expense-api.test.ts
  • Live HTTP scenario verification against an isolated Express + SQLite server instance recording ~/.no-mistakes/evidence/01M4YJM5EGVR5ANVQWZ26DAF3Q/expense-aggregate-bounds-live-verification.json
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@coderabbitai

coderabbitai Bot commented Oct 15, 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
  • Configuration used: Repository: FreeOpenSourcePOS/FloCafe/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 724ce713-bc70-491f-a11f-8154ac0a83f7

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.

@khaira777

Copy link
Copy Markdown
Contributor Author

@greptile review

@greptile-apps

greptile-apps Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 4/5

[Critical impact] Changes expense amount validation and data model.

The frontend must handle the new null amounts before this is safe to merge.

Findings

  1. P1 Oversized expenses display as zero ▶

Summary

Adds exact expense aggregate reads and checks active totals before writes.

  • Expense totals stay exact up to JavaScript’s safe-integer limit.
  • Oversized legacy expenses stay readable and recoverable.

Review used static code and test inspection; no tests or live UI checks were run.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Create or replace expense] --> B[Begin transaction]
  B --> C{Stored response exists?}
  C -->|Yes| D[Replay response]
  C -->|No| E[Read exact active currency total]
  E --> F[Subtract replaced amount if applicable]
  F --> G{New total fits safe range?}
  G -->|No| H[Return 422 and roll back]
  G -->|Yes| I[Write expense and response receipt]
  I --> J[Commit]
Loading

Reviews (4) · Last reviewed commit: "Merge branch 'main' into codex/expense-a..." · Reviewed by Greptile

Comment thread main/services/expenses.ts
@khaira777

Copy link
Copy Markdown
Contributor Author

@greptileai review

@khaira777

Copy link
Copy Markdown
Contributor Author

@greptileai review

@khaira777
khaira777 force-pushed the codex/expense-aggregate-bounds branch from b978f7c to 2ec1348 Compare October 15, 2026 01:59
@khaira777

Copy link
Copy Markdown
Contributor Author

@greptileai review

1 similar comment
@khaira777

Copy link
Copy Markdown
Contributor Author

@greptileai review

Comment thread main/services/expenses.ts
Backend projects oversized legacy amounts as null; type the frontend
fields as number | null and render common.notAvailable instead of
formatting null as $0.00.
@khaira777

Copy link
Copy Markdown
Contributor Author

Greptile P1 addressed in d0b370e: amount_minor/due_minor are now typed number | null in frontend/src/lib/types.ts, and both the expenses list and ExpenseWorkflow render common.notAvailable (already translated in all locales) instead of formatting null as $0.00. The payment form opens with an empty amount for null records and the disable check treats null due as no-payable. Marking the thread resolved.

@khaira777
khaira777 merged commit da0512c into main Oct 15, 2026
23 checks passed
@khaira777
khaira777 deleted the codex/expense-aggregate-bounds branch October 15, 2026 05:18
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(expenses): prevent 500 error on expense period summary when amounts approach safe integer limit

1 participant