Skip to content

feat(kilo-pass): disallow duplicate card fingerprints across Kilo Pass subscriptions#3309

Open
jrf0110 wants to merge 11 commits into
mainfrom
gt/toast/d0f06aec
Open

feat(kilo-pass): disallow duplicate card fingerprints across Kilo Pass subscriptions#3309
jrf0110 wants to merge 11 commits into
mainfrom
gt/toast/d0f06aec

Conversation

@jrf0110
Copy link
Copy Markdown
Contributor

@jrf0110 jrf0110 commented May 18, 2026

Summary

Enforce that a single credit card fingerprint can be attached to at most one active Kilo Pass subscription across all Kilo users at any time. This blocks a fraud vector where multiple accounts purchase Kilo Pass using the same physical card.

What's new

  • findActiveKiloPassByCardFingerprint helper in apps/web/src/lib/stripe.ts — queries payment methods for a matching fingerprint on a different user, then checks whether that user has an active Kilo Pass subscription.
  • checkDuplicateCardFingerprintGate in apps/web/src/lib/kilo-pass/card-fingerprint-gate.ts — runs in the invoice.paid webhook handler after the Stripe charge succeeds. If a duplicate is found: cancels the subscription on Stripe, refunds the invoice, writes an audit log, and sends a customer-facing email notification.
  • DuplicateCardSubscriptionCanceled audit log action — added to the KiloPassAuditLogAction enum for tracking duplicate-card cancellations.
  • Email notification — new kiloPassDuplicateCardCanceled template and sendKiloPassDuplicateCardCanceledEmail sender function, using transactional_email_log for idempotent dedup.
  • App Store / Google Play path skipped — the gate only runs in the Stripe invoice.paid path. Store purchases have their own subscription model (appAccountToken) and don't go through Stripe invoices. A code comment documents this.

Edge cases handled

  • Same user re-subscribing with the same card → NOT blocked (excluded by excludingUserId).
  • Other user has ended Kilo Pass with same fingerprint → NOT blocked (query pivots off active subscription status).
  • No fingerprint available → gate passes open (fails open rather than blocking legitimate purchases).
  • Race condition / webhook replay → email dedup via transactional_email_log unique index; audit logs are idempotent via the existing Stripe event ID tracking.

Verification

Manual verification only. Automated tests exist but require a running Postgres instance.

Visual Changes

N/A

Reviewer Notes

  • The gate fires in stripe-handlers-invoice-paid.ts after the subscription upsert but before credit issuance — if blocked, the subscription row is marked canceled with ended_at set, and no credits are issued.
  • The findActiveKiloPassByCardFingerprint query joins payment_methods to kilo_pass_subscriptions, filtering for active subscriptions (not in canceled/unpaid/incomplete_expired, and ended_at IS NULL).
  • Migration 0135_duplicate_card_gate.sql adds the DuplicateCardSubscriptionCanceled enum value to the check constraint.

Comment thread apps/web/src/lib/kilo-pass/card-fingerprint-gate.ts Outdated
Comment thread apps/web/src/lib/stripe.ts Outdated
Comment thread apps/web/src/lib/kilo-pass/card-fingerprint-gate.ts
@kilo-code-bot
Copy link
Copy Markdown
Contributor

kilo-code-bot Bot commented May 18, 2026

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

Incremental review of the single commit pushed since cd3f998: only the migration was renumbered (01380139) because an unrelated migration 0138_brief_valkyrie was merged to main. The migration content is unchanged and correct. All previously-resolved issues remain closed; no new issues found.

Resolved Issues (all closed)
  • appendKiloPassAuditLog correctly passes dbOrTx — audit row is inside the outer transaction boundary
  • maybeSendDuplicateCardCanceledEmail is called after the transaction commits — the transactional_email_log idempotency row is guaranteed to persist before the email fires
  • ✅ Raw sql\NOT IN`replaced with Drizzle'snotInArray(...)` — type-safe query
  • blockedEmailParams uses the transaction return value instead of outer-variable mutation — correct narrowing
  • findActiveKiloPassByCardFingerprint signature simplified back to use db directly — the TOCTOU race window is documented and acknowledged as an acceptable trade-off
  • ✅ Stale stripe.ts comment cleaned up
  • ✅ Migration renumbered from 01380139 cleanly — content identical, journal consistent
Files Reviewed (9 files)
  • apps/web/src/emails/kiloPassDuplicateCardCanceled.html
  • apps/web/src/lib/email.ts
  • apps/web/src/lib/kilo-pass/card-fingerprint-gate.test.ts
  • apps/web/src/lib/kilo-pass/card-fingerprint-gate.ts ✅ all issues resolved
  • apps/web/src/lib/kilo-pass/stripe-handlers-invoice-paid.ts ✅ all issues resolved
  • packages/db/src/migrations/0139_duplicate_card_gate.sql ✅ renumbered, content unchanged
  • packages/db/src/migrations/meta/_journal.json
  • packages/db/src/schema-types.ts
  • packages/db/src/schema.test.ts

Reviewed by claude-4.6-sonnet-20260217 · 812,537 tokens

Review guidance: REVIEW.md from base branch main

Comment thread apps/web/src/lib/kilo-pass/card-fingerprint-gate.ts Outdated
@jrf0110 jrf0110 force-pushed the gt/toast/d0f06aec branch 3 times, most recently from dd45044 to cd3f998 Compare May 20, 2026 09:05
@jrf0110 jrf0110 force-pushed the gt/toast/d0f06aec branch from cd3f998 to 6954391 Compare May 20, 2026 15:58
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.

1 participant