Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changeset/fix-analytics-zeroes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

Fix analytics dashboard showing zeroes by syncing subscription data during backfill (internal admin change)
4 changes: 3 additions & 1 deletion server/public/admin-analytics.html
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,9 @@ <h2>Revenue by Product</h2>

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';

Expand Down
78 changes: 78 additions & 0 deletions server/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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');
Expand Down