Fix prepaid credit mint via Paddle custom_data #340

Merged
vkobel merged 6 commits from refs/pull/340/head into main 2026-07-16 22:04:52 +00:00
vkobel commented 2026-06-29 11:09:41 +00:00 (Migrated from codeberg.org)

The Paddle credit flow credited an amount taken from client transaction metadata, never tied to what was actually paid. Move the decision server-side: the API records a credit_purchase_intents row (server-computed amount, keyed by transaction id) at creation; the webhook and callback credit that amount and refuse when no row exists. Metadata is display-only now.

Also adds UNIQUE on credit_ledger.paddle_transaction_id + ON CONFLICT DO NOTHING on every grant path, replacing a racy SELECT-then-INSERT.

Two unrelated bugs found while testing, fixed in their own commits: metering's get_user_usage query (syntax + GROUP BY + NUMERIC-as-f64), and a stale usage_records ref in the e2e test (now usage_ledger).

Reviewer:

migration 044 adds the table + constraint; the UNIQUE fails to apply if the DB holds duplicate paddle_transaction_id rows from the old race — run the dupe-check first. In-flight pre-deploy transactions have no intent row and won't auto-credit (reconcile manually).

What I tested already:

make test-e2e-billing against a real stack — 37/37, including four new cases: credits the intent amount and ignores a forged custom_data value, refuses with no intent row, credits once on duplicate delivery, and the UNIQUE/ON CONFLICT no-op.

The Paddle credit flow credited an amount taken from client transaction metadata, never tied to what was actually paid. Move the decision server-side: the API records a credit_purchase_intents row (server-computed amount, keyed by transaction id) at creation; the webhook and callback credit that amount and refuse when no row exists. Metadata is display-only now. Also adds UNIQUE on credit_ledger.paddle_transaction_id + ON CONFLICT DO NOTHING on every grant path, replacing a racy SELECT-then-INSERT. Two unrelated bugs found while testing, fixed in their own commits: metering's get_user_usage query (syntax + GROUP BY + NUMERIC-as-f64), and a stale usage_records ref in the e2e test (now usage_ledger). ### Reviewer: migration 044 adds the table + constraint; the UNIQUE fails to apply if the DB holds duplicate paddle_transaction_id rows from the old race — run the dupe-check first. In-flight pre-deploy transactions have no intent row and won't auto-credit (reconcile manually). ### What I tested already: make test-e2e-billing against a real stack — 37/37, including four new cases: credits the intent amount and ignores a forged custom_data value, refuses with no intent row, credits once on duplicate delivery, and the UNIQUE/ON CONFLICT no-op.
anton commented 2026-07-13 04:42:02 +00:00 (Migrated from codeberg.org)

@vkobel I just made sure your PR is up to date with main to make further work / merging easier.

@vkobel I just made sure your PR is up to date with main to make further work / merging easier.
Sign in to join this conversation.
No reviewers
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
public/platform!340
No description provided.