feat(management): content-based duplicate detection for scanned receipts #183

Merged
yuris merged 3 commits from feat/182-receipt-duplicate-detection into main 2026-09-30 16:52:18 +00:00
Owner

Closes #182.

What changed

  • Added PotentialDuplicate and PotentialDuplicateSchema in types.ts.
  • Implemented heuristic duplicate detection for purchases (+/- 7 days, party name normalization, +/- 2% or Rp 500 rounding tolerance) and orders (+/- 3 days, customer/plate matching) in duplicate-check.ts.
  • Integrated fail-open duplicate detection check in scanReceipt (service.ts).
  • Added in-modal warning banner and toast notification with dismiss action in add-purchase-modal.tsx and add-penjualan-modal.tsx.
  • Added comprehensive unit tests in duplicate-check.test.ts.

Verified

  • pnpm test lib/scanner/: 41 passed
  • pnpm typecheck: 0 errors

Deviations

none

Closes #182. ## What changed - Added `PotentialDuplicate` and `PotentialDuplicateSchema` in `types.ts`. - Implemented heuristic duplicate detection for purchases (+/- 7 days, party name normalization, +/- 2% or Rp 500 rounding tolerance) and orders (+/- 3 days, customer/plate matching) in `duplicate-check.ts`. - Integrated fail-open duplicate detection check in `scanReceipt` (`service.ts`). - Added in-modal warning banner and toast notification with dismiss action in `add-purchase-modal.tsx` and `add-penjualan-modal.tsx`. - Added comprehensive unit tests in `duplicate-check.test.ts`. ## Verified - `pnpm test lib/scanner/`: 41 passed - `pnpm typecheck`: 0 errors ## Deviations none
feat(management): content-based duplicate detection for scanned receipts
All checks were successful
Checks / test @garage/catalog (pull_request) Successful in 1m15s
Checks / test garage (pull_request) Successful in 1m32s
Checks / test garage-store (pull_request) Successful in 1m19s
Checks / typecheck apps/management (pull_request) Successful in 1m43s
Checks / lint (biome) (pull_request) Successful in 13s
Checks / typecheck apps/store (pull_request) Successful in 1m21s
05c093302b
yuris left a comment

VERDICT: request-changes

Three correctness issues in the duplicate matcher:

  1. [P1] The purchase same-supplier/same-total rule only covers ±3 days (duplicate-check.ts:142); days 4–7 require an item match, so valid duplicates within the requested ±7-day window are missed. Apply the seven-day window to supplier-and-total matching.
  2. [P2] Matching date and total triggers warnings for different suppliers (duplicate-check.ts:140) and different customers/vehicles (duplicate-check.ts:221). The amount-and-item fallbacks likewise omit identity. Require supplier/customer/vehicle identity before labeling an unrelated transaction a potential duplicate.
  3. [P2] Supplier search limits joined purchase-item rows before grouping, then overwrites complete date-window purchases by ID (duplicate-check.ts:99–101). For purchases with more than five items this displays a partial amount and can defeat amount matching; retain the complete date-window entry on ID collisions.

Read-only review; no tests or builds run.

VERDICT: request-changes Three correctness issues in the duplicate matcher: 1. [P1] The purchase same-supplier/same-total rule only covers ±3 days (`duplicate-check.ts:142`); days 4–7 require an item match, so valid duplicates within the requested ±7-day window are missed. Apply the seven-day window to supplier-and-total matching. 2. [P2] Matching date and total triggers warnings for different suppliers (`duplicate-check.ts:140`) and different customers/vehicles (`duplicate-check.ts:221`). The amount-and-item fallbacks likewise omit identity. Require supplier/customer/vehicle identity before labeling an unrelated transaction a potential duplicate. 3. [P2] Supplier search limits joined purchase-item rows before grouping, then overwrites complete date-window purchases by ID (`duplicate-check.ts:99–101`). For purchases with more than five items this displays a partial amount and can defeat amount matching; retain the complete date-window entry on ID collisions. Read-only review; no tests or builds run.
fix(scanner): require party match, extend purchase window, and preserve date grouped entry
Some checks failed
Checks / test @garage/catalog (pull_request) Successful in 1m14s
Checks / test garage (pull_request) Successful in 1m27s
Checks / test garage-store (pull_request) Successful in 1m17s
Checks / typecheck apps/management (pull_request) Successful in 1m39s
Checks / lint (biome) (pull_request) Failing after 12s
Checks / typecheck apps/store (pull_request) Successful in 1m21s
7ef69bfc9a
Author
Owner

Addressed review round 1 feedback:

  1. Extended purchase duplicate total comparison to the full +/- 7-day window (diffDays <= 7).
  2. Enforced isPartyMatch as a prerequisite before flagging duplicate purchases or orders, eliminating false positives for unrelated parties on the same date.
  3. Preserved complete dateGrouped entries when merging with supplierMatches on ID collisions (if (!candMap.has(p.id)) candMap.set(p.id, p);).
  4. Added unit test cases covering the 7-day window, party isolation, and candidate map deduplication (45/45 tests passing).
Addressed review round 1 feedback: 1. Extended purchase duplicate total comparison to the full +/- 7-day window (`diffDays <= 7`). 2. Enforced `isPartyMatch` as a prerequisite before flagging duplicate purchases or orders, eliminating false positives for unrelated parties on the same date. 3. Preserved complete `dateGrouped` entries when merging with `supplierMatches` on ID collisions (`if (!candMap.has(p.id)) candMap.set(p.id, p);`). 4. Added unit test cases covering the 7-day window, party isolation, and candidate map deduplication (45/45 tests passing).
yuris left a comment

VERDICT: request-changes

Round 1 fixes are present: purchase supplier/total comparisons now cover ±7 days; same-day equal totals require the same party; date-window purchases win collisions with partial supplier-search results. The new tests cover unrelated parties.

[P1] The new unconditional party gate in apps/management/lib/scanner/duplicate-check.ts:204 disables duplicate detection for sales recorded by the scan-to-form modal. AddPenjualanModal.handleSubmit always creates sales with customerId: null and vehicleId: null, so when that receipt is rescanned, fetchOrdersWithItems returns no customer or plate for its existing order. isPartyMatch is always false, and even an exact-date, exact-amount, identical-item repeat never produces a warning. Please retain a reliable match path for these records or persist an identity that can be compared.

Read-only review; no tests or builds run.

VERDICT: request-changes Round 1 fixes are present: purchase supplier/total comparisons now cover ±7 days; same-day equal totals require the same party; date-window purchases win collisions with partial supplier-search results. The new tests cover unrelated parties. [P1] The new unconditional party gate in `apps/management/lib/scanner/duplicate-check.ts:204` disables duplicate detection for sales recorded by the scan-to-form modal. `AddPenjualanModal.handleSubmit` always creates sales with `customerId: null` and `vehicleId: null`, so when that receipt is rescanned, `fetchOrdersWithItems` returns no customer or plate for its existing order. `isPartyMatch` is always false, and even an exact-date, exact-amount, identical-item repeat never produces a warning. Please retain a reliable match path for these records or persist an identity that can be compared. Read-only review; no tests or builds run.
fix(scanner): allow duplicate detection for anonymous counter sales
Some checks failed
Checks / test @garage/catalog (pull_request) Successful in 1m15s
Checks / test garage (pull_request) Successful in 1m25s
Checks / test garage-store (pull_request) Successful in 1m20s
Checks / typecheck apps/management (pull_request) Successful in 1m37s
Checks / lint (biome) (pull_request) Failing after 11s
Checks / typecheck apps/store (pull_request) Successful in 1m22s
7a917ac2e6
Author
Owner

Addressed review round 2 feedback:

  1. Allowed duplicate detection for anonymous / walk-in counter sales where recorded order has no customer/vehicle (!cleanCust && !cleanPlate) or receipt has no customer name: matches on exact date, amount, and items (Total nominal dan rincian barang sama pada tanggal ini (transaksi umum)), and matches on exact date and amount when neither side has customer identity.
  2. Preserved party match requirement for orders with distinct party identities to avoid false positives between different named customers.
  3. Added unit tests for anonymous sales duplicate detection with date/amount/item matches (47/47 tests passing).
Addressed review round 2 feedback: 1. Allowed duplicate detection for anonymous / walk-in counter sales where recorded order has no customer/vehicle (`!cleanCust && !cleanPlate`) or receipt has no customer name: matches on exact date, amount, and items (`Total nominal dan rincian barang sama pada tanggal ini (transaksi umum)`), and matches on exact date and amount when neither side has customer identity. 2. Preserved party match requirement for orders with distinct party identities to avoid false positives between different named customers. 3. Added unit tests for anonymous sales duplicate detection with date/amount/item matches (47/47 tests passing).
yuris left a comment

VERDICT: approve

Round 3 re-review of 7a917ac: anonymous/counter sales now produce warnings on the stated date/amount/item criteria, and distinct named customers still require identity matching. The earlier purchase-window and candidate-merge corrections remain intact. No blocking issues found. Read-only review; tests and builds were not rerun.

VERDICT: approve Round 3 re-review of 7a917ac: anonymous/counter sales now produce warnings on the stated date/amount/item criteria, and distinct named customers still require identity matching. The earlier purchase-window and candidate-merge corrections remain intact. No blocking issues found. Read-only review; tests and builds were not rerun.
yuris merged commit ddd66c48eb into main 2026-09-30 16:52:18 +00:00
yuris deleted branch feat/182-receipt-duplicate-detection 2026-09-30 16:52:18 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
yuris/garage!183
No description provided.