Merge pull request #1612 from useautumn/fix/dashboard-customer-cache

fix: dashboard customer cache
This commit is contained in:
John Yeo
2026-05-19 09:52:03 +01:00
committed by GitHub
5 changed files with 91 additions and 4 deletions

2
ai

Submodule ai updated: a6d3ad0403...b1efb8d303

View File

@@ -14,7 +14,10 @@ import {
} from "@autumn/shared";
import * as Sentry from "@sentry/bun";
import type { AutumnContext, RequestContext } from "@/honoUtils/HonoEnv.js";
import { getOrgCusProductLimit } from "../misc/edgeConfig/orgLimitsStore.js";
import {
getOrgCusProductLimit,
getOrgEntitiesLimit,
} from "../misc/edgeConfig/orgLimitsStore.js";
import { triggerBatchResetCustomerEntitlements } from "./actions/resetCustomerEntitlements/triggerBatchResetCustomerEntitlements.js";
import { CusSearchService } from "./CusSearchService.js";
import { getCursorPaginatedFullCusQuery } from "./cursorPaginatedFullCusQuery.js";
@@ -324,6 +327,10 @@ export class CusBatchService {
orgId: ctx.org.id,
orgSlug: ctx.org.slug,
});
const entitiesLimit = getOrgEntitiesLimit({
orgId: ctx.org.id,
orgSlug: ctx.org.slug,
});
const statusFilters = (filters?.status ?? []).filter(
(s): s is DashboardStatusFilter =>
@@ -373,6 +380,9 @@ export class CusBatchService {
env: ctx.env,
inStatuses: RELEVANT_STATUSES,
withSubs: true,
withEntities: true,
includeInvoices: true,
entitiesLimit,
limit: internalIds ? internalIds.length : limit,
cursor:
!requiresResolveStep && cursor
@@ -403,6 +413,8 @@ export class CusBatchService {
replaceables: [],
free_trials: [],
subscriptions: [],
entities: [],
invoices: [],
}) as unknown as FlattenedCustomerRow;
const allCustomers = reassembleFlattenedCustomer(flat);

View File

@@ -16,6 +16,10 @@ export type CursorPaginatedFullCusQueryArgs = {
env: AppEnv;
inStatuses?: CusProductStatus[];
withSubs?: boolean;
withEntities?: boolean;
includeInvoices?: boolean;
entitiesLimit?: number;
invoicesLimit?: number;
limit: number;
cursor?: StandardCursorFields;
internalCustomerIds?: string[];
@@ -38,6 +42,10 @@ export const getCursorPaginatedFullCusQuery = ({
env,
inStatuses,
withSubs = true,
withEntities = false,
includeInvoices = false,
entitiesLimit = 300,
invoicesLimit = 10,
limit,
cursor,
internalCustomerIds,
@@ -86,6 +94,42 @@ export const getCursorPaginatedFullCusQuery = ({
) AS subscriptions`
: sql`'[]'::json AS subscriptions`;
const entitiesCte = withEntities
? sql`, entities_all AS MATERIALIZED (
SELECT e.internal_customer_id, row_to_json(e) AS row_json
FROM cr
JOIN LATERAL (
SELECT e.*
FROM entities e
WHERE e.internal_customer_id = cr.internal_id
ORDER BY e.internal_id DESC
LIMIT ${entitiesLimit}
) e ON true
)`
: sql``;
const invoicesCte = includeInvoices
? sql`, invoices_all AS MATERIALIZED (
SELECT i.internal_customer_id, row_to_json(i) AS row_json
FROM cr
JOIN LATERAL (
SELECT i.*
FROM invoices i
WHERE i.internal_customer_id = cr.internal_id
ORDER BY i.created_at DESC, i.id DESC
LIMIT ${invoicesLimit}
) i ON true
)`
: sql``;
const entitiesSelect = withEntities
? sql`, (SELECT COALESCE(json_agg(row_json), '[]'::json) FROM entities_all) AS entities`
: sql``;
const invoicesSelect = includeInvoices
? sql`, (SELECT COALESCE(json_agg(row_json), '[]'::json) FROM invoices_all) AS invoices`
: sql``;
return sql`
WITH cr AS MATERIALIZED (
SELECT
@@ -155,6 +199,8 @@ export const getCursorPaginatedFullCusQuery = ({
UNION ALL
SELECT id, entitlement_id FROM ces_loose
)
${entitiesCte}
${invoicesCte}
SELECT
(SELECT COALESCE(json_agg(row_json), '[]'::json) FROM cr) AS customers,
(SELECT COALESCE(json_agg(row_json), '[]'::json) FROM cps_ranked) AS customer_products,
@@ -184,5 +230,7 @@ export const getCursorPaginatedFullCusQuery = ({
JOIN free_trials ft ON ft.id = cps.free_trial_id
) AS free_trials,
${subscriptionsSelect}
${entitiesSelect}
${invoicesSelect}
`;
};

View File

@@ -49,16 +49,41 @@ export const reassembleFlattenedCustomer = (
subscriptionByStripeId: maps.subscriptionByStripeId,
});
const entitiesByCusId = groupByInternalCustomerId(flat.entities);
const invoicesByCusId = groupByInternalCustomerId(flat.invoices);
const out: FullCustomer[] = [];
for (const c of flat.customers) {
const internalId = c.internal_id as string;
out.push({
const hydrated: Record<string, unknown> = {
...c,
created_at: toTimestamp(c.created_at),
customer_products: cpsByCusId.get(internalId) ?? [],
extra_customer_entitlements: looseCesByCusId.get(internalId) ?? [],
subscriptions: subsByCusId.get(internalId) ?? [],
} as unknown as FullCustomer);
};
if (flat.entities !== undefined) {
hydrated.entities = entitiesByCusId.get(internalId) ?? [];
}
if (flat.invoices !== undefined) {
hydrated.invoices = invoicesByCusId.get(internalId) ?? [];
}
out.push(hydrated as unknown as FullCustomer);
}
return out;
};
const groupByInternalCustomerId = (
rows: { internal_customer_id?: string; [k: string]: unknown }[] | undefined,
): Map<string, unknown[]> => {
const out = new Map<string, unknown[]>();
if (!rows) return out;
for (const row of rows) {
const cusId = row.internal_customer_id;
if (!cusId) continue;
const list = out.get(cusId);
if (list) list.push(row);
else out.set(cusId, [row]);
}
return out;
};

View File

@@ -9,6 +9,8 @@ export type FlattenedCustomerRow = {
replaceables: any[];
free_trials: any[];
subscriptions: any[];
entities?: any[];
invoices?: any[];
};
export type FlatCustomerEntitlement = {