How We Fixed the Silent Payment Bug That Cost Us Real Money
This morning I was reviewing Stripe logs when I noticed something odd—successfully paid "Premium Listing" purchases where the posts never actually got premium status. Our users were paying, but the promised visibility boost wasn't happening. Oops.
The Bug That Lived in Production for Approximately 13-19 Days
The issue was straightforward, yet nasty:
- User pays for a premium listing package via Stripe Checkout
- Client-side completes the payment flow
- Backend marks payment as completed in our database
- Critical step missing: The actual package benefits (featuredUntil/highlightedUntil timestamps) never got applied to the post
We discovered this when a user emailed saying "I paid £4.99 but my post looks the same."
The Anatomy of the Failure
Our payment flow had two parallel paths:
Admin Bypass Path (Working)
// Working admin flow
await markPaymentCompleted(paymentId);
await applyPackageToPost(postId, packageType); // ✅
Stripe Checkout Path (Broken)
// Broken production flow until v1.4.4
await markPaymentCompleted(paymentId); // ❌ Stopped here
// applyPackageToPost() was never called
The missing line was a classic copy-paste omission—we'd abstracted the package application logic but forgot to call it in the Stripe verification path.
The Fix (v1.4.4)
We patched it with surgical precision:
- Added
applyPackageToPost()to the checkout verification flow - Made the admin bypass use the same unified function
- Added transaction wrapping to ensure atomic updates
// Fixed in v1.4.4
const verified = await verifyCheckoutSession(sessionId);
if (verified) {
await db.transaction(async (tx) => {
await markPaymentCompleted(paymentId, tx);
await applyPackageToPost(postId, packageType, tx); // ✅ Now called
});
}
Key details:
- Uses Drizzle ORM transactions
- Same function handles both Stripe and admin flows
- Logs package application separately for auditing
Why This Hurt More Than Typical Bugs
- Silent failure - No errors in logs, payments showed as "completed"
- Delayed detection - Users assumed premium features were subtle
- Reputation risk - Paying customers weren't getting what they paid for
Lessons Learned
-
Payment flows need dual verification
We now:- Log package application separately from payment completion
- Have a nightly job checking for paid-but-unapplied packages
Transactional boundaries matter
The fix wrapped everything in a DB transaction—no more partial updates.Admin tools should use the same paths as users
The admin bypass worked because it called the full flow. The Stripe path didn't.-
Monitor what you monetize
We added Stripe webhook logging and dashboard monitoring for:- Payments completed vs packages applied (tracked via checkout session IDs)
- Time delta between payment and feature activation (logged in checkout verification flow)
The Stack That Caught It
Our monitoring eventually flagged the issue through user reports and internal checks.
The Stripe webhook handler is a stub. Package activation happens in the verify-checkout-session flow.
Current Architecture (Post-Fix)
graph TD
A[Stripe Checkout] -->|Session ID| B(verifyCheckoutSession)
B --> C{Valid?}
C -->|Yes| D[markPaymentCompleted]
D --> E[applyPackageToPost]
E --> F[Update post.featuredUntil]
C -->|No| G[Log failed verification]
H[Admin Bypass] --> E
Key improvements:
- Single code path for both flows
- Transactional integrity
- Audit logging at each step
If We Had to Do It Again
We'd:
- Write the failing test first - No test covered the post-payment state
- Monitor payment-to-feature latency - Would've caught it within hours
- Add feature toggle checks - Verify premium posts actually appear differently
The fix shipped in v1.4.4. Since then, zero reported issues—and our premium purchase conversion rate increased. Turns out people buy more when the features actually work. Who knew?
Top comments (0)