Files
cfw-autumn/server/src/internal/customers/getFullCusQuery.ts
2025-07-24 13:20:29 +01:00

304 lines
8.1 KiB
TypeScript

import { AppEnv } from "@autumn/shared";
import { CusProductStatus } from "@autumn/shared";
import { sql, SQL } from "drizzle-orm";
const buildOptimizedCusProductsCTE = (inStatuses?: CusProductStatus[]) => {
const withStatusFilter = () => {
return inStatuses
? sql`AND cp.status = ANY(ARRAY[${sql.join(
inStatuses.map((status) => sql`${status}`),
sql`, `,
)}])`
: sql``;
};
return sql`
customer_products_with_prices AS (
SELECT
cp.*,
row_to_json(prod) AS product,
-- Spread customer_prices fields + add price field
COALESCE(
json_agg(DISTINCT (
to_jsonb(cpr.*) || jsonb_build_object('price', to_jsonb(p.*))
)) FILTER (WHERE cpr.id IS NOT NULL),
'[]'::json
) AS customer_prices,
-- Spread customer_entitlements fields + add entitlement, replaceables, and rollovers
COALESCE(
json_agg(DISTINCT (
to_jsonb(ce.*) || jsonb_build_object(
'entitlement', (
SELECT row_to_json(ent_with_feature)
FROM (
SELECT e.*, row_to_json(f) AS feature
FROM entitlements e
JOIN features f ON e.internal_feature_id = f.internal_id
WHERE e.id = ce.entitlement_id
) AS ent_with_feature
),
'replaceables', (
SELECT COALESCE(
json_agg(row_to_json(r)) FILTER (WHERE r.id IS NOT NULL),
'[]'::json
)
FROM replaceables r
WHERE r.cus_ent_id = ce.id
),
'rollover', (
SELECT COALESCE(
json_agg(row_to_json(ro)) FILTER (WHERE ro.expires_at > EXTRACT(EPOCH FROM now()) * 1000),
'[]'::json
)
FROM rollovers ro
WHERE ro.cus_ent_id = ce.id
)
)
)) FILTER (WHERE ce.id IS NOT NULL),
'[]'::json
) AS customer_entitlements,
-- free_trial
(
SELECT row_to_json(ft)
FROM free_trials ft
WHERE ft.id = cp.free_trial_id
) AS free_trial
FROM customer_products cp
JOIN products prod ON cp.internal_product_id = prod.internal_id
LEFT JOIN customer_prices cpr ON cpr.customer_product_id = cp.id
LEFT JOIN prices p ON cpr.price_id = p.id
LEFT JOIN customer_entitlements ce ON ce.customer_product_id = cp.id
WHERE cp.internal_customer_id = (SELECT internal_id FROM customer_record)
${withStatusFilter()}
GROUP BY cp.id, prod.*
)
`;
};
const buildEntitiesCTE = (withEntities: boolean) => {
if (!withEntities) {
return sql``;
}
return sql`
customer_entities AS (
SELECT
COALESCE(
json_agg(row_to_json(e) ORDER BY e.internal_id DESC),
'[]'::json
) AS entities
FROM entities e
WHERE e.internal_customer_id = (SELECT internal_id FROM customer_record)
LIMIT 100
)
`;
};
const buildEntityCTE = (entityId?: string) => {
if (!entityId) {
return sql``;
}
return sql`
entity_record AS (
SELECT * FROM entities e
WHERE e.internal_customer_id = (SELECT internal_id FROM customer_record)
AND (
e.id = ${entityId} OR e.internal_id = ${entityId}
)
LIMIT 1
)
`;
};
const buildTrialsUsedCTE = (
withTrialsUsed: boolean,
orgId: string,
env: AppEnv,
) => {
if (!withTrialsUsed) {
return sql``;
}
return sql`
customer_trials_used AS (
SELECT
COALESCE(
json_agg(json_build_object(
'product_id', p.id,
'fingerprint', c.fingerprint,
'customer_id', c.id
)) FILTER (WHERE p.id IS NOT NULL),
'[]'::json
) AS trials_used
FROM customer_products cp
JOIN products p ON cp.internal_product_id = p.internal_id
JOIN customers c ON cp.internal_customer_id = c.internal_id
WHERE (c.id = (SELECT id FROM customer_record) OR (c.fingerprint IS NOT NULL AND c.fingerprint = (SELECT fingerprint FROM customer_record)))
AND p.org_id = ${orgId}
AND p.env = ${env}
AND cp.free_trial_id IS NOT NULL
)
`;
};
const buildSubscriptionsCTE = (
withSubs: boolean,
inStatuses?: CusProductStatus[],
) => {
if (!withSubs) {
return sql``;
}
return sql`
customer_subscriptions AS (
SELECT
COALESCE(
json_agg(row_to_json(s)) FILTER (WHERE s.stripe_id IS NOT NULL),
'[]'::json
) AS subscriptions
FROM subscriptions s
WHERE EXISTS (
SELECT 1 FROM customer_products_with_prices cpwp
WHERE cpwp.subscription_ids @> ARRAY[s.stripe_id]
)
)
`;
};
const buildInvoicesCTE = (hasEntityCTE: boolean) => {
let entityFilter = hasEntityCTE
? sql`AND (
NOT EXISTS (SELECT 1 FROM entity_record)
OR i.internal_entity_id = (SELECT internal_id FROM entity_record LIMIT 1)
)`
: sql``;
return sql`
customer_invoices AS (
SELECT
COALESCE(
json_agg(row_to_json(i) ORDER BY i.created_at DESC, i.id DESC) FILTER (WHERE i.id IS NOT NULL),
'[]'::json
) AS invoices
FROM invoices i
WHERE i.internal_customer_id = (SELECT internal_id FROM customer_record)
${entityFilter}
LIMIT 10
)
`;
};
export const getFullCusQuery = (
idOrInternalId: string,
orgId: string,
env: AppEnv,
inStatuses: CusProductStatus[],
includeInvoices: boolean,
withEntities: boolean,
withTrialsUsed: boolean,
withSubs: boolean,
entityId?: string,
) => {
const sqlChunks: SQL[] = [];
// Step 1: Get customer record
sqlChunks.push(sql`
WITH customer_record AS (
SELECT * FROM customers c
WHERE (
c.id = ${idOrInternalId} OR c.internal_id = ${idOrInternalId}
)
AND c.org_id = ${orgId}
AND c.env = ${env}
ORDER BY (c.id = ${idOrInternalId}) DESC
LIMIT 1
)
`);
// Step 2: Get entities
if (withEntities) {
sqlChunks.push(sql`, `);
sqlChunks.push(buildEntitiesCTE(withEntities));
}
// Step 3: Get entity
if (entityId) {
sqlChunks.push(sql`, `);
sqlChunks.push(buildEntityCTE(entityId));
}
// Add customer products CTE
sqlChunks.push(sql`, `);
// sqlChunks.push(buildCusProductsCTE(inStatuses));
sqlChunks.push(buildOptimizedCusProductsCTE(inStatuses));
// Conditionally add trials used CTE
if (withTrialsUsed) {
sqlChunks.push(sql`, `);
sqlChunks.push(buildTrialsUsedCTE(withTrialsUsed, orgId, env));
}
// Conditionally add subscriptions CTE
if (withSubs) {
sqlChunks.push(sql`, `);
sqlChunks.push(buildSubscriptionsCTE(withSubs, inStatuses));
}
// Conditionally add invoices CTE
if (includeInvoices) {
sqlChunks.push(sql`, `);
sqlChunks.push(buildInvoicesCTE(!!entityId));
}
// Build final SELECT
const selectFieldsChunks: SQL[] = [];
selectFieldsChunks.push(sql`
cr.*,
COALESCE(
(SELECT json_agg(cpwp) FROM customer_products_with_prices cpwp),
'[]'::json
) AS customer_products
`);
// Add entities to SELECT if withEntities is true
if (withEntities) {
selectFieldsChunks.push(sql`,
(SELECT entities FROM customer_entities) AS entities`);
}
// Add entity to SELECT if entityId is provided
if (entityId) {
selectFieldsChunks.push(sql`,
(SELECT row_to_json(er) FROM entity_record er LIMIT 1) AS entity`);
}
// Add trials used to SELECT if withTrialsUsed is true
if (withTrialsUsed) {
selectFieldsChunks.push(sql`,
(SELECT trials_used FROM customer_trials_used) AS trials_used`);
}
// Add subscriptions to SELECT if withSubs is true
if (withSubs) {
selectFieldsChunks.push(sql`,
(SELECT subscriptions FROM customer_subscriptions) AS subscriptions`);
}
if (includeInvoices) {
selectFieldsChunks.push(sql`,
(SELECT invoices FROM customer_invoices) AS invoices`);
}
sqlChunks.push(sql`
SELECT ${sql.join(selectFieldsChunks, sql``)}
FROM customer_record cr
`);
return sql.join(sqlChunks, sql``);
};