Skip to content

fix(purchase): save purchase history once per commitment - #776

Merged
cristim merged 3 commits into
mainfrom
fix/704-history-dedupe
Oct 9, 2026
Merged

cristim merged 3 commits into
mainfrom
fix/704-history-dedupe

Conversation

@cristim

@cristim cristim commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

What

Closes #704 (MON-06) and #774: a re-drive or root retry that adopts a commitment the first attempt already bought saved the same (provider, account_id, purchase_id) again, so spend, savings and YTD were double counted.

SavePurchaseHistory (internal/config/store_postgres.go) now inserts only WHERE NOT EXISTS a row for that key, with a bare ON CONFLICT DO NOTHING. Migration 000108 adds a partial unique index on (provider, account_id, purchase_id) WHERE created_at > <migration time>. A skipped save is logged; the first row wins.

Nothing is deleted. Existing duplicate rows stay (still double counted in old data). Removing them deletes financial history and is a separate owner decision. The partial predicate cannot say "no earlier twin", so the index covers rows created after the migration (the cutoff is now() evaluated once in the migration and baked into the predicate), and the NOT EXISTS in the insert covers older rows.

Proofs requested during planning

  1. Conflict form. store_postgres.go:2089: bare ON CONFLICT DO NOTHING, no conflict target, so no index inference and no "no unique or exclusion constraint matching the ON CONFLICT specification" error. The only unique constraints on purchase_history are the id primary key (a fresh UUID per row) and the new index, so nothing unexpected is swallowed.
  2. created_at. It comes from the column default (created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), migration 000001) and is not in the INSERT column list; this is the only non-test INSERT INTO purchase_history. TestSavePurchaseHistory_CreatedAtIgnoresPurchaseTimestamp saves a record whose purchase Timestamp is a year old and asserts created_at is within the last minute.

Tests (-tags integration, real Postgres via testcontainers; CI runs this tag)

  • On origin/main a repeated save leaves 2 rows (and the older-row case 2); with the fix 1.
  • ConcurrentSaveWaitsForUncommittedTwin: another transaction inserts the key but has not committed; the save blocks on the unique index and no-ops after the commit. Without the index it returns at once and the test fails.
  • A row older than the index still blocks a repeat (fails without NOT EXISTS); same purchase id in another account or provider is a different purchase.
  • Migration108_AppliesOverExistingDuplicates: the migration applies with pre-existing duplicates, deletes nothing, and the index rejects a duplicate among new rows.
  • Short tests for internal/config, internal/database/postgres/migrations and internal/purchase pass.

Real Postgres 16 harness, not the deployed stack.

Notes

  • Not changed: internal/purchase/execution.go, manager.go, reaper.go, handler_purchases.go (MON-02 work by another agent). The issue's third item, refusing a root retry when completed children exist, is left to that work.
  • Rolling deploy: old code still running after the migration gets a unique violation on a re-drive, which becomes the existing audit-gap marker instead of a duplicate row.
  • Reviewer item R1 (coordinator decision A): SavePurchaseHistory returns ErrEmptyPurchaseID for an empty purchase id (otherwise it would be dropped silently after the first). The caller already turns a history error into the history_write_failed audit-gap note and the execution stays completed; TestManager_ExecuteAndFinalize_EmptyCommitmentID_CompletesWithAuditGap runs the real Validate() through that path. Both new tests fail with the guard disabled.

Closes #774

Local independent review (2 passes): no actionable findings after the log line and extra subtests.

🤖 Generated with Claude Code

A re-drive or root retry that adopts a commitment the first attempt already
bought saved the same (provider, account_id, purchase_id) again, so spend,
savings and YTD were double counted.

SavePurchaseHistory now inserts only when no row exists for the key and
ignores a conflict. Migration 000108 adds a partial unique index on
(provider, account_id, purchase_id) for rows created after it runs, so two
concurrent saves cannot both insert. Rows that exist today, duplicates
included, are left alone: removing them deletes financial history and is a
separate decision, and the cutoff keeps them from blocking the index.

Integration tests (real Postgres) cover a repeated save, a row older than the
index, an uncommitted twin in another transaction, created_at coming from the
column default, and the migration applying over existing duplicates.

Refs #704
@cristim cristim added urgency/this-sprint Within the current sprint priority/p1 Next up; this sprint severity/high Significant harm impact/many Affects most users effort/m Days type/bug Defect triaged Item has been triaged labels Oct 9, 2026
@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

This review includes 8 billable files and costs up to $2.00.

  • Ask an admin to make reviews automatic

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Or wait 50 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available. Your 86 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: LeanerCloud/cloud-commitments-platform/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 2fad03a6-006f-48d0-9f1e-eff3f23e1851

📥 Commits

Reviewing files that changed from the base of the PR and between 225ad35 and 316c83d.


📒 Files selected for processing (8)
  • internal/config/errors.go
  • internal/config/store_postgres.go
  • internal/config/store_postgres_history_dedupe_integration_test.go
  • internal/config/store_postgres_history_validate_test.go
  • internal/config/types.go
  • internal/database/postgres/migrations/000108_purchase_history_unique_purchase.down.sql
  • internal/database/postgres/migrations/000108_purchase_history_unique_purchase.up.sql
  • internal/purchase/execution_test.go


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

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

SavePurchaseHistory saves once per (provider, account_id, purchase_id), so a
successful purchase with an empty CommitmentID would be dropped silently
after the first one. Return ErrEmptyPurchaseID instead; the caller already
turns a history error into the audit-gap note on the execution, which stays
completed.

Closes #774
@cristim
cristim merged commit 57f42e0 into main Oct 9, 2026
25 checks passed
@cristim

cristim commented Oct 9, 2026

Copy link
Copy Markdown
Member Author

Merged as 57f42e0 (squash, match-head 316c83d). Reviewed SHAs: full review APPROVE at c48022f (archera-quotes-codex); Opus delta APPROVE at 316c83d (cc-coordinator-opus). CI 25/25 pass, state CLEAN, 0 unresolved threads. Local at 316c83d (git archive, Go 1.26.9): go build ./... exit 0; go test ./internal/config/... ./internal/purchase/... ./internal/api/... exit 0 (Postgres integration tests not run).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/m Days impact/many Affects most users priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/bug Defect urgency/this-sprint Within the current sprint

Projects

None yet

1 participant