Skip to content

Splitting $10.00 three ways loses a cent #52

Closed

Description

@Param-Harrison

What happened

Add an expense of 1000 cents ($10.00) split among 3 people. The three
shares are 333, 333, 333 cents - they sum to 999, not 1000. A cent
disappears.

Expected

Shares should always sum to exactly the original amount. One (or more)
member should get the leftover cent(s), e.g. 334, 333, 333.

Steps to reproduce

bun run .claude/skills/handling-money/scripts/check-cents.ts

The split(1000, 3 members) case prints FAIL ... sum 999 (expected 1000).

Or via the API, with any Goa trip token:

curl -s -X POST http://localhost:3200/api/groups/goa-trip/expenses \
  -H "Authorization: Bearer goa-asha" -H "Content-Type: application/json" \
  -d '{"payerId":"goa-asha-id","amountCents":1000,"description":"test","splitAmong":["goa-asha-id","goa-ben-id","goa-chetan-id"]}'

Then check the expense's shares sum to 1000 - they sum to 999.

Area

money

Severity

medium

Notes for whoever picks this up

See .claude/skills/handling-money - the remainder rule is documented in
references/rules.md. The fix is in src/money/cents.ts's splitCents.

Activity

  1. added
    bugSomething behaves incorrectly
    factory:readyHuman marked this ready for the loop to pick up
    factory:triagingfactory-triage is classifying this issue
    and removed
    factory:readyHuman marked this ready for the loop to pick up
    on Sep 24, 2026
  2. Param-Harrison commented on Sep 24, 2026

    @Param-Harrison
    ContributorAuthor

    Triage

    Disposition: proceed
    Type: bug
    Risk: high
    Confidence: 0.9

    Done when: splitCents(1000, [a, b, c]) returns shares that sum to exactly 1000 (e.g. 334/333/333, remainder to the first members per .claude/skills/handling-money/references/rules.md), proven by a new bun:test in tests/money.test.ts that fails on the current code, with make check green.

    Files expected: src/money/cents.ts, tests/money.test.ts

    Gate level: make check

    Risk is high because the change touches money (the split that every expense's shares come from), even though it is confined to one non-protected module. No protected path is involved: the fix goes in src/money/cents.ts, and the .claude/skills/handling-money skill is only read, not edited.

  3. added
    factory:planningfactory-plan is writing the plan comment
    and removed
    factory:triagingfactory-triage is classifying this issue
    on Sep 24, 2026
  4. Param-Harrison commented on Sep 24, 2026

    @Param-Harrison
    ContributorAuthor

    Plan (rev 1)

    Goal: Make splitCents give the leftover cents to the first members, so shares always add up to the total (1000 split 3 ways → 334/333/333).

    Acceptance criteria

    • AC-1: splitCents(1000, ["a","b","c"]) returns {a:334, b:333, c:333}, and the test fails on the current code. Checked by bun test tests/money.test.ts -t "gives the remainder cents to the first members"
    • AC-2: For several totals and member counts that don't divide evenly (for example 101 across 5 and 1 across 3), the shares add up to exactly totalCents. Each share is floor(total/n) or floor(total/n)+1, and the +1 shares go to the first remainder members in the order they're passed in. Checked by bun test tests/money.test.ts -t "shares always sum to the total"
    • AC-3: The existing splitCents tests (even splits, 900/3 and 2000/2) and the zero-members throw still pass without changes. Checked by bun test tests/money.test.ts
    • AC-4: The handling-money sum check reports every case as ok. Checked by bun run .claude/skills/handling-money/scripts/check-cents.ts
    • AC-5: The full gate is green. Checked by make check

    Non-goals (binding — a diff that crosses one fails verify)

    • NG-1: No edits outside src/money/cents.ts and tests/money.test.ts. That rules out src/expenses.ts, src/seed.ts, other tests, and anything under .claude/**, .factory/**, src/auth/** or .github/**.
    • NG-2: No change to splitCents's signature or return type, and no change to formatCents, parseCents or sumCents.
    • NG-3: No floats in the split. Use only integer-cents arithmetic, per the handling-money skill.
    • NG-4: No migration or backfill of expense shares already stored with the floored values. This fix covers new splits only.
    • NG-5: No change to how the split handles negative totals or totals that aren't integers (no new validation).

    Files to touch

    • src/money/cents.ts: fix splitCents (lines 39–49) and replace the "SEEDED DEFECT" doc comment (lines 29–38) with a description of the remainder rule
    • tests/money.test.ts: add the remainder tests to the existing splitCents describe block

    Tests to write first

    • tests/money.test.ts › splitCents › gives the remainder cents to the first members
    • tests/money.test.ts › splitCents › shares always sum to the total

    Repo skills to apply

    handling-money (references/rules.md remainder rule: base = floor(total/n), the first total - base*n members get base + 1)

    Risk: low

    Charter tier low: "a change confined to a single non-protected module with no schema or API-shape change". Only src/money/ changes. The function's signature, the DB schema and the route shapes all stay the same, and no protected path is touched. The only caller, src/expenses.ts:28, inserts whatever shares splitCents returns and doesn't depend on their order. No existing test asserts the floored output: tests/routes.test.ts uses fixtures that divide evenly.

    Triage suggested high because this is money. The charter's high tier covers only dependency majors, src/auth/**, or plans with low confidence, and none of those apply here. Still, every expense's shares come from this function, so I've marked it not eligible for auto-approve. A human should look at it even though the tier is low.

    Gate level: make check

    This needs a human: reply /factory approve to proceed, or
    /factory revise <what to change>.

  5. added and removed
    factory:planningfactory-plan is writing the plan comment
    on Sep 24, 2026
  6. Param-Harrison commented on Sep 24, 2026

    @Param-Harrison
    ContributorAuthor

    /factory approve

  7. added
    factory:buildingfactory-build is making the change on factory/issue-N
    and removed on Sep 24, 2026
  8. Param-Harrison commented on Sep 24, 2026

    @Param-Harrison
    ContributorAuthor

    Status

    Stage: build — finished
    Progress: 3/5
    Elapsed: ~3m
    Gate line: FACTORY_GATES: status=GREEN passed=3 failed=0 skipped=1 failed_gates=audit

    Round 2 fixes the blocking NG-5 finding from the last verdict. The remainder rule now applies only to totals that are whole numbers of zero or more. Negative and non-integer totals still get the old floored shares (-1000 split 3 ways → -334 each, 10.5 split 2 ways → 5 each). A new test, leaves negative and non-integer totals floored as before, locks that in. It failed on the round-1 code and passes now. bun test passes (29 pass, 0 fail), and check-cents.ts reports every case ok. The optional audit gate still fails on hono advisories that were already there, which is out of scope. The two non-blocking findings (duplicate member ids, the doc-comment wording) predate this change and are left for a follow-up.

  9. added a commit that references this issue on Sep 24, 2026
  10. added
    factory:verifyingfactory-verifier and factory-reviewer are checking the diff
    factory:needs-humanRefused, or unresolved after two rounds; a human decides
    and removed
    factory:buildingfactory-build is making the change on factory/issue-N
    factory:verifyingfactory-verifier and factory-reviewer are checking the diff
    on Sep 24, 2026
  11. Param-Harrison commented on Sep 24, 2026

    @Param-Harrison
    ContributorAuthor

    /factory retry

  12. added
    factory:buildingfactory-build is making the change on factory/issue-N
    and removed
    factory:needs-humanRefused, or unresolved after two rounds; a human decides
    on Sep 24, 2026
  13. added a commit that references this issue on Sep 24, 2026
  14. added
    factory:verifyingfactory-verifier and factory-reviewer are checking the diff
    and removed
    factory:buildingfactory-build is making the change on factory/issue-N
    on Sep 24, 2026
  15. Param-Harrison commented on Sep 24, 2026

    @Param-Harrison
    ContributorAuthor

    Verdict: pass

    Acceptance criteria evidence

    • AC-1: pass — bun test tests/money.test.ts -t "gives the remainder cents to the first members" → 1 pass, 0 fail. With the pre-fix src/money/cents.ts it fails: Expected: {a:334,...} Received: {a:333,...}.
    • AC-2: pass — bun test tests/money.test.ts -t "shares always sum to the total" → 1 pass, 0 fail, 31 expect() calls. Covers 1000/3, 101/5, 1/3, 2/3, 999/7 and 1003/4. With the pre-fix code it fails: Expected: 1000, Received: 999.
    • AC-3: pass — bun test tests/money.test.ts → 9 pass, 0 fail, 45 expect() calls. git diff --numstat 934ab00 HEAD -- tests/money.test.ts shows 30 insertions and 0 deletions, so the existing tests are unchanged.
    • AC-4: pass — bun run .claude/skills/handling-money/scripts/check-cents.ts → all four cases (900/3, 2000/2, 1000/3, 101/5) print [ok ], then "all splits sum to their total."
    • AC-5: pass — make check → tsc --noEmit clean, 29 pass, 0 fail, 81 expect() calls across 4 files, and all 9 skills valid.

    The test that bites

    splitCents › gives the remainder cents to the first members fails on the pre-build code (Expected: {a:334,...} Received: {a:333,...}) and passes here
    (bun test tests/money.test.ts -t "gives the remainder cents to the first members" → 1 pass, 0 fail).
    splitCents › shares always sum to the total also fails before the change (Expected: 1000, Received: 999) and passes after it.

    Reviewer findings

    • Blocking anomaly — working tree does not match reviewed commit: git status in this workspace currently shows a staged, uncommitted modification to src/money/cents.ts that reverts the file back to the pre-fix "SEEDED DEFECT" version (confirmed via git diff --cached -- src/money/cents.ts): it restores const share = Math.floor(totalCents / memberIds.length) applied uniformly to all members, undoing the remainder fix that's actually committed at 93f3f44. Running bun test tests/money.test.ts against the current on-disk state fails 2 of the 2 new tests (gives the remainder cents to the first members, shares always sum to the total) because the working file doesn't match HEAD. This is not a defect in the reviewed diff — git show 93f3f44:src/money/cents.ts confirms the committed content is correct. It is a blocking concern for merge readiness … it should be investigated and the staged change discarded/reset to match HEAD before this is considered done.

      Disposition: This was a transient state, not a problem with the build. The reviewer ran at the same time as the verifier. The verifier's revert step (git checkout 934ab00 -- src/money/cents.ts) stages the old file on purpose, and the verifier then restored it with git checkout HEAD -- src/money/cents.ts. After both finished, git status --short printed nothing, bun test tests/money.test.ts gave 9 pass, 0 fail, and make check was green. The finding no longer applies.

    The reviewer found no other issues. The diff follows the remainder rule, uses integer-only arithmetic, and has no security surface.

    Non-goals respected

    • NG-1: git diff --name-only 934ab00 HEAD lists only src/money/cents.ts and tests/money.test.ts. The DEMO.md difference from the local main comes from a54c5b1 (DEMO.md: reset needs force pushes allowed on main #43), which predates the build.
    • NG-2: splitCents(totalCents: number, memberIds: string[]): Record<string, number> is unchanged. formatCents, parseCents and sumCents are byte-identical.
    • NG-3: The split uses only Math.floor and integer arithmetic, and no fractional value is assigned to a share.
    • NG-4: No migration or backfill was added.
    • NG-5: For negative or non-integer totals, remainder is forced to 0, so every share is Math.floor(total/n), exactly as before. The test leaves negative and non-integer totals floored as before locks this in.
  16. added
    factory:in-reviewDraft PR open, waiting on human review and merge
    and removed
    factory:verifyingfactory-verifier and factory-reviewer are checking the diff
    on Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething behaves incorrectlyfactory:in-reviewDraft PR open, waiting on human review and merge

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions