diff --git a/.changeset/fix-analytics-zeroes.md b/.changeset/fix-analytics-zeroes.md new file mode 100644 index 0000000000..b52b6e6cf3 --- /dev/null +++ b/.changeset/fix-analytics-zeroes.md @@ -0,0 +1,4 @@ +--- +--- + +Fix analytics dashboard showing zeroes by syncing subscription data during backfill (internal admin change) diff --git a/server/public/admin-analytics.html b/server/public/admin-analytics.html index 352b6eb95a..80716c905e 100644 --- a/server/public/admin-analytics.html +++ b/server/public/admin-analytics.html @@ -406,7 +406,9 @@

Revenue by Product

const result = await response.json(); - messageEl.textContent = `Sync complete: Found ${result.invoices_found} invoices and ${result.refunds_found} refunds. Imported ${result.imported} new records, ${result.skipped} already existed.`; + const subsSyncedMsg = result.subscriptions_synced ? ` Synced ${result.subscriptions_synced} subscriptions.` : ''; + const subsFailedMsg = result.subscriptions_failed ? ` (${result.subscriptions_failed} failed)` : ''; + messageEl.textContent = `Sync complete: Found ${result.invoices_found} invoices and ${result.refunds_found} refunds. Imported ${result.imported} new records, ${result.skipped} already existed.${subsSyncedMsg}${subsFailedMsg}`; messageEl.style.display = 'block'; messageEl.className = 'success-message'; diff --git a/server/src/http.ts b/server/src/http.ts index 685a575155..dc43d4c1e0 100644 --- a/server/src/http.ts +++ b/server/src/http.ts @@ -2652,11 +2652,87 @@ export class HTTPServer { } } + // Sync subscription data to organizations for MRR calculation + // This populates subscription_amount, subscription_interval, subscription_current_period_end + let subscriptionsSynced = 0; + let subscriptionsFailed = 0; + if (stripe) { + for (const [customerId, workosOrgId] of customerOrgMap) { + try { + // Get customer with subscriptions and expanded price/product data in single API call + const customer = await stripe.customers.retrieve(customerId, { + expand: ['subscriptions.data.items.data.price.product'], + }); + + if ('deleted' in customer && customer.deleted) { + continue; + } + + const subscriptions = (customer as Stripe.Customer).subscriptions; + if (!subscriptions || subscriptions.data.length === 0) { + continue; + } + + // Get the first active subscription (already has expanded items) + const subscription = subscriptions.data[0]; + if (!subscription || !['active', 'trialing', 'past_due'].includes(subscription.status)) { + continue; + } + + // Get primary subscription item directly from expanded data + const primaryItem = subscription.items.data[0]; + if (!primaryItem) { + continue; + } + + const price = primaryItem.price; + const product = price?.product as Stripe.Product | undefined; + const amount = price?.unit_amount ?? 0; + const interval = price?.recurring?.interval ?? null; + + // Update organization with subscription details + await pool.query( + `UPDATE organizations + SET subscription_amount = $1, + subscription_interval = $2, + subscription_currency = $3, + subscription_current_period_end = $4, + subscription_canceled_at = $5, + subscription_product_id = $6, + subscription_product_name = $7, + subscription_price_id = $8, + updated_at = NOW() + WHERE workos_organization_id = $9`, + [ + amount, + interval, + price?.currency || 'usd', + subscription.current_period_end ? new Date(subscription.current_period_end * 1000) : null, + subscription.canceled_at ? new Date(subscription.canceled_at * 1000) : null, + product?.id || null, + product?.name || null, + price?.id || null, + workosOrgId, + ] + ); + + subscriptionsSynced++; + logger.debug({ workosOrgId, customerId, amount, interval }, 'Synced subscription data'); + } catch (subError) { + subscriptionsFailed++; + logger.error({ err: subError, customerId, workosOrgId }, 'Failed to sync subscription for customer'); + // Continue with other customers + } + } + } + logger.info({ invoices: invoices.length, refunds: refunds.length, imported, skipped, + subscriptionsSynced, + subscriptionsFailed, }, 'Revenue backfill completed'); res.json({ @@ -2666,6 +2742,8 @@ export class HTTPServer { refunds_found: refunds.length, imported, skipped, + subscriptions_synced: subscriptionsSynced, + subscriptions_failed: subscriptionsFailed, }); } catch (error) { logger.error({ err: error }, 'Error during revenue backfill');