Skip to content

Commit 7303831

Browse files
bokelleyclaude
andauthored
Make Stripe sync idempotent - update existing records (#450)
* Make Stripe sync idempotent - update existing records Changed ON CONFLICT DO NOTHING to DO UPDATE so re-running "Sync from Stripe" will refresh existing records with current data (e.g., fix missing product info without manual truncation). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> * Fix response field inconsistency in backfill early return Aligned field names in the early return path (when no Stripe customers are linked) with the success path: invoices_found, refunds_found, processed, subscriptions_synced, subscriptions_failed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]> --------- Co-authored-by: Claude Opus 4.5 <[email protected]>
1 parent 9831423 commit 7303831

File tree

2 files changed

+67
-60
lines changed

2 files changed

+67
-60
lines changed

.changeset/sweet-taxis-act.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

server/src/http.ts

Lines changed: 65 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -3231,10 +3231,12 @@ export class HTTPServer {
32313231
if (customerOrgMap.size === 0) {
32323232
return res.json({
32333233
success: true,
3234-
message: 'No organizations with Stripe customers found',
3235-
invoices_imported: 0,
3236-
refunds_imported: 0,
3237-
skipped: 0,
3234+
message: 'No organizations with Stripe customers found. Link customers to orgs first.',
3235+
invoices_found: 0,
3236+
refunds_found: 0,
3237+
processed: 0,
3238+
subscriptions_synced: 0,
3239+
subscriptions_failed: 0,
32383240
});
32393241
}
32403242

@@ -3246,60 +3248,65 @@ export class HTTPServer {
32463248

32473249
const allEvents = [...invoices, ...refunds];
32483250

3249-
// Import events, skipping duplicates
3251+
// Import events, updating existing records with fresh data from Stripe
32503252
let imported = 0;
3251-
let skipped = 0;
32523253

32533254
for (const event of allEvents) {
3254-
try {
3255-
await pool.query(
3256-
`INSERT INTO revenue_events (
3257-
workos_organization_id,
3258-
stripe_invoice_id,
3259-
stripe_subscription_id,
3260-
stripe_payment_intent_id,
3261-
stripe_charge_id,
3262-
amount_paid,
3263-
currency,
3264-
revenue_type,
3265-
billing_reason,
3266-
product_id,
3267-
product_name,
3268-
price_id,
3269-
billing_interval,
3270-
paid_at,
3271-
period_start,
3272-
period_end
3273-
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
3274-
ON CONFLICT (stripe_invoice_id) DO NOTHING`,
3275-
[
3276-
event.workos_organization_id,
3277-
event.stripe_invoice_id,
3278-
event.stripe_subscription_id,
3279-
event.stripe_payment_intent_id,
3280-
event.stripe_charge_id,
3281-
event.amount_paid,
3282-
event.currency,
3283-
event.revenue_type,
3284-
event.billing_reason,
3285-
event.product_id,
3286-
event.product_name,
3287-
event.price_id,
3288-
event.billing_interval,
3289-
event.paid_at,
3290-
event.period_start,
3291-
event.period_end,
3292-
]
3293-
);
3294-
imported++;
3295-
} catch (err: unknown) {
3296-
// Check for unique constraint violation
3297-
if ((err as { code?: string }).code === '23505') {
3298-
skipped++;
3299-
} else {
3300-
throw err;
3301-
}
3302-
}
3255+
await pool.query(
3256+
`INSERT INTO revenue_events (
3257+
workos_organization_id,
3258+
stripe_invoice_id,
3259+
stripe_subscription_id,
3260+
stripe_payment_intent_id,
3261+
stripe_charge_id,
3262+
amount_paid,
3263+
currency,
3264+
revenue_type,
3265+
billing_reason,
3266+
product_id,
3267+
product_name,
3268+
price_id,
3269+
billing_interval,
3270+
paid_at,
3271+
period_start,
3272+
period_end
3273+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
3274+
ON CONFLICT (stripe_invoice_id) DO UPDATE SET
3275+
workos_organization_id = EXCLUDED.workos_organization_id,
3276+
stripe_subscription_id = EXCLUDED.stripe_subscription_id,
3277+
stripe_payment_intent_id = EXCLUDED.stripe_payment_intent_id,
3278+
stripe_charge_id = EXCLUDED.stripe_charge_id,
3279+
amount_paid = EXCLUDED.amount_paid,
3280+
currency = EXCLUDED.currency,
3281+
revenue_type = EXCLUDED.revenue_type,
3282+
billing_reason = EXCLUDED.billing_reason,
3283+
product_id = EXCLUDED.product_id,
3284+
product_name = EXCLUDED.product_name,
3285+
price_id = EXCLUDED.price_id,
3286+
billing_interval = EXCLUDED.billing_interval,
3287+
paid_at = EXCLUDED.paid_at,
3288+
period_start = EXCLUDED.period_start,
3289+
period_end = EXCLUDED.period_end`,
3290+
[
3291+
event.workos_organization_id,
3292+
event.stripe_invoice_id,
3293+
event.stripe_subscription_id,
3294+
event.stripe_payment_intent_id,
3295+
event.stripe_charge_id,
3296+
event.amount_paid,
3297+
event.currency,
3298+
event.revenue_type,
3299+
event.billing_reason,
3300+
event.product_id,
3301+
event.product_name,
3302+
event.price_id,
3303+
event.billing_interval,
3304+
event.paid_at,
3305+
event.period_start,
3306+
event.period_end,
3307+
]
3308+
);
3309+
imported++;
33033310
}
33043311

33053312
// Sync subscription data to organizations for MRR calculation
@@ -3379,19 +3386,17 @@ export class HTTPServer {
33793386
logger.info({
33803387
invoices: invoices.length,
33813388
refunds: refunds.length,
3382-
imported,
3383-
skipped,
3389+
processed: imported,
33843390
subscriptionsSynced,
33853391
subscriptionsFailed,
33863392
}, 'Revenue backfill completed');
33873393

33883394
res.json({
33893395
success: true,
3390-
message: `Backfill completed`,
3396+
message: `Sync completed: ${imported} records processed`,
33913397
invoices_found: invoices.length,
33923398
refunds_found: refunds.length,
3393-
imported,
3394-
skipped,
3399+
processed: imported,
33953400
subscriptions_synced: subscriptionsSynced,
33963401
subscriptions_failed: subscriptionsFailed,
33973402
});

0 commit comments

Comments
 (0)