fix: get full customer
This commit is contained in:
@@ -1,17 +0,0 @@
|
||||
import { CusEntService } from "../src/internal/customers/cusProducts/cusEnts/CusEntitlementService";
|
||||
import { initDrizzle } from "../src/db/initDrizzle";
|
||||
import { clearCusEntsFromCache } from "../src/cron/resetCron/clearCusEntsFromCache";
|
||||
|
||||
const main = async () => {
|
||||
const { db } = initDrizzle();
|
||||
const cusEnts = await CusEntService.getActiveResetPassed({
|
||||
db,
|
||||
batchSize: 500,
|
||||
});
|
||||
|
||||
|
||||
await clearCusEntsFromCache({ cusEnts });
|
||||
};
|
||||
|
||||
await main();
|
||||
process.exit(0);
|
||||
21
server/experiments/experimentEnv.ts
Normal file
21
server/experiments/experimentEnv.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import "dotenv/config";
|
||||
import { loadLocalEnv } from "../src/utils/envUtils";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
const requireEnv = ({ key }: { key: string }) => {
|
||||
const value = process.env[key];
|
||||
|
||||
if (!value) {
|
||||
throw new Error(`${key} env var is required`);
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
export const prodTestOrgId = requireEnv({ key: "PROD_TEST_ORG_ID" });
|
||||
export const prodTestCustomerId = requireEnv({
|
||||
key: "PROD_TEST_CUSTOMER_ID",
|
||||
});
|
||||
|
||||
export const { initDrizzle } = await import("../src/db/initDrizzle");
|
||||
60
server/experiments/explainGetFull.ts
Normal file
60
server/experiments/explainGetFull.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { AppEnv, CusProductStatus } from "@autumn/shared";
|
||||
import { sql } from "drizzle-orm";
|
||||
import {
|
||||
initDrizzle,
|
||||
prodTestCustomerId,
|
||||
prodTestOrgId,
|
||||
} from "./experimentEnv";
|
||||
const { getFullCusQuery } = await import(
|
||||
"../src/internal/customers/getFullCusQuery"
|
||||
);
|
||||
|
||||
const RELEVANT_STATUSES = [
|
||||
CusProductStatus.Active,
|
||||
CusProductStatus.PastDue,
|
||||
CusProductStatus.Scheduled,
|
||||
];
|
||||
|
||||
// Run with `bun run experiments/explainGetFull.ts`
|
||||
|
||||
const main = async () => {
|
||||
const orgId = prodTestOrgId;
|
||||
const env = AppEnv.Live;
|
||||
const customerId = prodTestCustomerId;
|
||||
|
||||
const { db } = initDrizzle();
|
||||
|
||||
const query = getFullCusQuery(
|
||||
customerId,
|
||||
orgId,
|
||||
env,
|
||||
RELEVANT_STATUSES,
|
||||
true, // includeInvoices
|
||||
true, // withEntities
|
||||
false, // withTrialsUsed
|
||||
true, // withSubs
|
||||
false, // withEvents
|
||||
);
|
||||
|
||||
// Run the actual query to measure wall-clock time
|
||||
console.log("--- Running query ---");
|
||||
const start = performance.now();
|
||||
const result = await db.execute(query);
|
||||
const elapsed = performance.now() - start;
|
||||
console.log(`Rows returned: ${result.length}`);
|
||||
console.log(`Wall-clock time: ${elapsed.toFixed(2)}ms\n`);
|
||||
|
||||
// Run EXPLAIN ANALYZE
|
||||
console.log("--- EXPLAIN (ANALYZE, BUFFERS) ---\n");
|
||||
const explainQuery = sql`EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) ${query}`;
|
||||
const explainResult = await db.execute(explainQuery);
|
||||
|
||||
for (const row of explainResult) {
|
||||
const line = (row as Record<string, unknown>)["QUERY PLAN"];
|
||||
console.log(line);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
await main();
|
||||
164
server/experiments/explainListFullProducts.ts
Normal file
164
server/experiments/explainListFullProducts.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { AppEnv, entitlements, freeTrials, prices, products } from "@autumn/shared";
|
||||
import { and, eq, exists, sql } from "drizzle-orm";
|
||||
import { initDrizzle, prodTestOrgId } from "./experimentEnv";
|
||||
|
||||
const main = async () => {
|
||||
const orgId = prodTestOrgId;
|
||||
const env = AppEnv.Live;
|
||||
|
||||
const { db } = initDrizzle();
|
||||
|
||||
// Reproduce the latestVersionsSubquery from ProductService._listFullQuery
|
||||
const latestVersionsSubquery = db
|
||||
.select({
|
||||
id: products.id,
|
||||
maxVersion: sql<number>`MAX(${products.version})`.as("max_version"),
|
||||
})
|
||||
.from(products)
|
||||
.where(and(eq(products.org_id, orgId), eq(products.env, env)))
|
||||
.groupBy(products.id)
|
||||
.as("latest_versions");
|
||||
|
||||
// 1. Run the Drizzle relational query for wall-clock time
|
||||
console.log("--- Running ProductService.listFull query ---");
|
||||
const start = performance.now();
|
||||
const data = await db.query.products.findMany({
|
||||
where: and(
|
||||
eq(products.org_id, orgId),
|
||||
eq(products.env, env),
|
||||
exists(
|
||||
db
|
||||
.select()
|
||||
.from(latestVersionsSubquery)
|
||||
.where(
|
||||
and(
|
||||
eq(latestVersionsSubquery.id, products.id),
|
||||
eq(latestVersionsSubquery.maxVersion, products.version),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
with: {
|
||||
entitlements: {
|
||||
with: { feature: true },
|
||||
where: eq(entitlements.is_custom, false),
|
||||
},
|
||||
prices: { where: eq(prices.is_custom, false) },
|
||||
free_trials: { where: eq(freeTrials.is_custom, false) },
|
||||
},
|
||||
});
|
||||
const elapsed = performance.now() - start;
|
||||
console.log(`Products returned: ${data.length}`);
|
||||
console.log(`Wall-clock time: ${elapsed.toFixed(2)}ms\n`);
|
||||
|
||||
// 2. Build equivalent raw SQL so we can wrap with EXPLAIN ANALYZE
|
||||
const rawQuery = sql`
|
||||
SELECT
|
||||
p.internal_id, p.id, p.name, p.description, p.org_id,
|
||||
p.created_at, p.env, p.is_add_on, p.is_default, p."group",
|
||||
p.version, p.processor, p.base_variant_id, p.archived,
|
||||
ent_data.data AS entitlements,
|
||||
price_data.data AS prices,
|
||||
ft_data.data AS free_trials
|
||||
FROM products p
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT COALESCE(
|
||||
json_agg(json_build_object(
|
||||
'id', e.id,
|
||||
'created_at', e.created_at,
|
||||
'internal_feature_id', e.internal_feature_id,
|
||||
'internal_product_id', e.internal_product_id,
|
||||
'is_custom', e.is_custom,
|
||||
'allowance_type', e.allowance_type,
|
||||
'allowance', e.allowance,
|
||||
'interval', e."interval",
|
||||
'interval_count', e.interval_count,
|
||||
'carry_from_previous', e.carry_from_previous,
|
||||
'entity_feature_id', e.entity_feature_id,
|
||||
'org_id', e.org_id,
|
||||
'feature_id', e.feature_id,
|
||||
'usage_limit', e.usage_limit,
|
||||
'rollover', e.rollover,
|
||||
'feature', row_to_json(f)
|
||||
)),
|
||||
'[]'::json
|
||||
) AS data
|
||||
FROM entitlements e
|
||||
LEFT JOIN features f ON f.internal_id = e.internal_feature_id
|
||||
WHERE e.internal_product_id = p.internal_id
|
||||
AND e.is_custom = false
|
||||
) ent_data ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT COALESCE(
|
||||
json_agg(json_build_object(
|
||||
'id', pr.id,
|
||||
'org_id', pr.org_id,
|
||||
'internal_product_id', pr.internal_product_id,
|
||||
'config', pr.config,
|
||||
'created_at', pr.created_at,
|
||||
'billing_type', pr.billing_type,
|
||||
'tier_behavior', pr.tier_behavior,
|
||||
'is_custom', pr.is_custom,
|
||||
'entitlement_id', pr.entitlement_id,
|
||||
'proration_config', pr.proration_config
|
||||
)),
|
||||
'[]'::json
|
||||
) AS data
|
||||
FROM prices pr
|
||||
WHERE pr.internal_product_id = p.internal_id
|
||||
AND pr.is_custom = false
|
||||
) price_data ON true
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT COALESCE(
|
||||
json_agg(json_build_object(
|
||||
'id', ft.id,
|
||||
'created_at', ft.created_at,
|
||||
'internal_product_id', ft.internal_product_id,
|
||||
'duration', ft.duration,
|
||||
'length', ft.length,
|
||||
'unique_fingerprint', ft.unique_fingerprint,
|
||||
'is_custom', ft.is_custom,
|
||||
'card_required', ft.card_required
|
||||
)),
|
||||
'[]'::json
|
||||
) AS data
|
||||
FROM free_trials ft
|
||||
WHERE ft.internal_product_id = p.internal_id
|
||||
AND ft.is_custom = false
|
||||
) ft_data ON true
|
||||
WHERE p.org_id = ${orgId}
|
||||
AND p.env = ${env}
|
||||
AND EXISTS (
|
||||
SELECT id, max_version FROM (
|
||||
SELECT id, MAX(version) AS max_version
|
||||
FROM products
|
||||
WHERE org_id = ${orgId} AND env = ${env}
|
||||
GROUP BY id
|
||||
) latest_versions
|
||||
WHERE latest_versions.id = p.id
|
||||
AND max_version = p.version
|
||||
)
|
||||
`;
|
||||
|
||||
// Run raw query for wall-clock time
|
||||
console.log("--- Running raw SQL query ---");
|
||||
const start2 = performance.now();
|
||||
const rawResult = await db.execute(rawQuery);
|
||||
const elapsed2 = performance.now() - start2;
|
||||
console.log(`Rows returned: ${rawResult.length}`);
|
||||
console.log(`Wall-clock time: ${elapsed2.toFixed(2)}ms\n`);
|
||||
|
||||
// Run EXPLAIN ANALYZE
|
||||
console.log("--- EXPLAIN (ANALYZE, BUFFERS) ---\n");
|
||||
const explainQuery = sql`EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) ${rawQuery}`;
|
||||
const explainResult = await db.execute(explainQuery);
|
||||
|
||||
for (const row of explainResult) {
|
||||
const line = (row as Record<string, unknown>)["QUERY PLAN"];
|
||||
console.log(line);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
await main();
|
||||
@@ -1,122 +0,0 @@
|
||||
import { ApiVersion } from "@autumn/shared";
|
||||
import AutumnError, { AutumnInt } from "../src/external/autumn/autumnCli";
|
||||
|
||||
export const main = async () => {
|
||||
console.log("🚀 Starting rate limit test...\n");
|
||||
|
||||
// Initialize client
|
||||
const autumn = new AutumnInt({version: ApiVersion.V1_2});
|
||||
const numRequests = 100;
|
||||
|
||||
const customerId1 = "trial1";
|
||||
const customerId2 = "temp";
|
||||
|
||||
console.log(`Testing with ${numRequests} concurrent requests to /track endpoint`);
|
||||
console.log(`Using base URL: ${autumn.baseUrl}\n`);
|
||||
|
||||
// Create all track requests
|
||||
const cusId1Promises = [];
|
||||
const cusId2Promises = [];
|
||||
for (let i = 0; i < numRequests; i++) {
|
||||
cusId1Promises.push(
|
||||
autumn.entities.get(customerId1, "1"),
|
||||
);
|
||||
cusId2Promises.push(
|
||||
autumn.entities.get(customerId2, "1"),
|
||||
);
|
||||
}
|
||||
|
||||
// Execute all requests concurrently for both customers
|
||||
const startTime = Date.now();
|
||||
const [cusId1Results, cusId2Results] = await Promise.all([
|
||||
Promise.allSettled(cusId1Promises),
|
||||
Promise.allSettled(cusId2Promises),
|
||||
]);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
// Helper function to analyze results
|
||||
const analyzeResults = (results: PromiseSettledResult<unknown>[]) => {
|
||||
const succeeded = results.filter((r) => r.status === "fulfilled").length;
|
||||
const rateLimited = results.filter(
|
||||
(r) =>
|
||||
r.status === "rejected" &&
|
||||
r.reason instanceof AutumnError &&
|
||||
r.reason.code === "rate_limit_exceeded",
|
||||
).length;
|
||||
const otherErrors = results.filter(
|
||||
(r) =>
|
||||
r.status === "rejected" &&
|
||||
!(
|
||||
r.reason instanceof AutumnError &&
|
||||
r.reason.code === "rate_limit_exceeded"
|
||||
),
|
||||
).length;
|
||||
return { succeeded, rateLimited, otherErrors };
|
||||
};
|
||||
|
||||
|
||||
|
||||
const cus1Stats = analyzeResults(cusId1Results);
|
||||
const cus2Stats = analyzeResults(cusId2Results);
|
||||
const totalRequests = numRequests * 2;
|
||||
|
||||
// Display results
|
||||
console.log("📊 Results:");
|
||||
console.log("═".repeat(60));
|
||||
console.log(`Total requests: ${totalRequests} (${numRequests} per customer)`);
|
||||
console.log(`⏱️ Duration: ${duration}ms`);
|
||||
console.log(`📈 Throughput: ${Math.round(totalRequests / (duration / 1000))} req/s`);
|
||||
console.log("═".repeat(60));
|
||||
|
||||
console.log(`\n👤 Customer 1 (${customerId1}):`);
|
||||
console.log("─".repeat(60));
|
||||
console.log(` Total: ${numRequests}`);
|
||||
console.log(` ✅ Succeeded: ${cus1Stats.succeeded}`);
|
||||
console.log(` ⛔ Rate limited: ${cus1Stats.rateLimited}`);
|
||||
console.log(` ❌ Other errors: ${cus1Stats.otherErrors}`);
|
||||
|
||||
console.log(`\n👤 Customer 2 (${customerId2}):`);
|
||||
console.log("─".repeat(60));
|
||||
console.log(` Total: ${numRequests}`);
|
||||
console.log(` ✅ Succeeded: ${cus2Stats.succeeded}`);
|
||||
console.log(` ⛔ Rate limited: ${cus2Stats.rateLimited}`);
|
||||
console.log(` ❌ Other errors: ${cus2Stats.otherErrors}`);
|
||||
|
||||
console.log("\n📈 Combined Stats:");
|
||||
console.log("─".repeat(60));
|
||||
console.log(
|
||||
` ✅ Total succeeded: ${cus1Stats.succeeded + cus2Stats.succeeded}`,
|
||||
);
|
||||
console.log(
|
||||
` ⛔ Total rate limited: ${cus1Stats.rateLimited + cus2Stats.rateLimited}`,
|
||||
);
|
||||
console.log(
|
||||
` ❌ Total errors: ${cus1Stats.otherErrors + cus2Stats.otherErrors}`,
|
||||
);
|
||||
|
||||
// Show sample errors if any
|
||||
const totalErrors = cus1Stats.otherErrors + cus2Stats.otherErrors;
|
||||
if (totalErrors > 0) {
|
||||
console.log("\n⚠️ Sample of other errors:");
|
||||
const errorSamples = [...cusId1Results, ...cusId2Results]
|
||||
.filter(
|
||||
(r) =>
|
||||
r.status === "rejected" &&
|
||||
!(
|
||||
r.reason instanceof AutumnError &&
|
||||
r.reason.code === "rate_limit_exceeded"
|
||||
),
|
||||
)
|
||||
.slice(0, 3);
|
||||
|
||||
for (const sample of errorSamples) {
|
||||
if (sample.status === "rejected") {
|
||||
console.log(` - ${sample.reason}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n✨ Test complete!");
|
||||
};
|
||||
|
||||
await main();
|
||||
@@ -37,7 +37,8 @@ const extractCustomerIdFromBody = ({
|
||||
method: string;
|
||||
}): string | undefined => {
|
||||
const isCreateCustomerPath =
|
||||
path.startsWith("/v1/customers") && method === "POST";
|
||||
(path.startsWith("/v1/customers") && method === "POST") ||
|
||||
path.includes("customers.get_or_create");
|
||||
return (isCreateCustomerPath ? body?.id : body?.customer_id) as
|
||||
| string
|
||||
| undefined;
|
||||
@@ -114,11 +115,14 @@ const logResponse = async ({
|
||||
const log = c.res.status === 200 ? ctx.logger.info : ctx.logger.warn;
|
||||
const statusColor = c.res.status === 200 ? chalk.green : chalk.yellow;
|
||||
|
||||
log(`[${statusColor(c.res.status)}] ${c.req.path} (${ctx.org?.slug}) ${durationMs}ms`, {
|
||||
statusCode: c.res.status,
|
||||
durationMs,
|
||||
res: responseBody,
|
||||
});
|
||||
log(
|
||||
`[${statusColor(c.res.status)}] ${c.req.path} (${ctx.org?.slug}) ${durationMs}ms`,
|
||||
{
|
||||
statusCode: c.res.status,
|
||||
durationMs,
|
||||
res: responseBody,
|
||||
},
|
||||
);
|
||||
|
||||
if (
|
||||
Object.keys(ctx.extraLogs).length > 0 &&
|
||||
|
||||
@@ -59,6 +59,12 @@ export const buildStripeSubscriptionUpdateAction = ({
|
||||
typeof subscriptionCancelAt === "number" &&
|
||||
subscriptionCancelAt !== currentCancelAt;
|
||||
|
||||
// Configure trial settings off
|
||||
const shouldUpdateEndBehavior =
|
||||
shouldUnsetTrialEnd &&
|
||||
stripeSubscription.trial_settings?.end_behavior.missing_payment_method !==
|
||||
"create_invoice";
|
||||
|
||||
const params: Stripe.SubscriptionUpdateParams = {
|
||||
items: subItemsUpdate.length > 0 ? subItemsUpdate : undefined,
|
||||
trial_end: shouldSetTrialEnd
|
||||
@@ -76,6 +82,14 @@ export const buildStripeSubscriptionUpdateAction = ({
|
||||
...(stripeDiscounts?.length && {
|
||||
discounts: stripeDiscountsToParams({ stripeDiscounts }),
|
||||
}),
|
||||
|
||||
...(shouldUpdateEndBehavior && {
|
||||
trial_settings: {
|
||||
end_behavior: {
|
||||
missing_payment_method: "create_invoice",
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const hasNoUpdates = [
|
||||
|
||||
@@ -44,6 +44,7 @@ export class CusService {
|
||||
withSubs = false,
|
||||
allowNotFound = false,
|
||||
withEvents = false,
|
||||
explain = false,
|
||||
}: {
|
||||
ctx: AutumnContext;
|
||||
idOrInternalId: string;
|
||||
@@ -54,6 +55,7 @@ export class CusService {
|
||||
withSubs?: boolean;
|
||||
allowNotFound?: boolean;
|
||||
withEvents?: boolean;
|
||||
explain?: boolean;
|
||||
}): Promise<FullCustomer> {
|
||||
const { db, org, env } = ctx;
|
||||
const orgId = org.id;
|
||||
@@ -86,6 +88,12 @@ export class CusService {
|
||||
entityId,
|
||||
);
|
||||
|
||||
if (explain) {
|
||||
const explainQuery = sql`EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) ${query}`;
|
||||
const result = await db.execute(explainQuery);
|
||||
return result as unknown as FullCustomer;
|
||||
}
|
||||
|
||||
const result = await db.execute(query);
|
||||
|
||||
if (!result || result.length === 0) {
|
||||
@@ -452,13 +460,15 @@ export class CusService {
|
||||
|
||||
const ids = batch.map((r) => r.internal_id);
|
||||
|
||||
await db.delete(customers).where(
|
||||
and(
|
||||
inArray(customers.internal_id, ids),
|
||||
eq(customers.org_id, orgId),
|
||||
eq(customers.env, env),
|
||||
),
|
||||
);
|
||||
await db
|
||||
.delete(customers)
|
||||
.where(
|
||||
and(
|
||||
inArray(customers.internal_id, ids),
|
||||
eq(customers.org_id, orgId),
|
||||
eq(customers.env, env),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ const buildOptimizedCusProductsCTE = (inStatuses?: CusProductStatus[]) => {
|
||||
FROM free_trials ft
|
||||
WHERE ft.id = cp.free_trial_id
|
||||
) ft_data ON true
|
||||
WHERE cp.internal_customer_id = (SELECT internal_id FROM customer_record)
|
||||
WHERE cp.internal_customer_id = (SELECT internal_id COLLATE "default" FROM customer_record)
|
||||
${withStatusFilter()}
|
||||
)
|
||||
`;
|
||||
@@ -217,7 +217,7 @@ const buildExtraEntitlementsCTE = () => {
|
||||
'[]'::json
|
||||
) AS extra_customer_entitlements
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.internal_customer_id = (SELECT internal_id FROM customer_record)
|
||||
WHERE ce.internal_customer_id = (SELECT internal_id COLLATE "default" FROM customer_record)
|
||||
AND ce.customer_product_id IS NULL
|
||||
AND (ce.expires_at IS NULL OR ce.expires_at > EXTRACT(EPOCH FROM now()) * 1000)
|
||||
)
|
||||
@@ -619,7 +619,7 @@ export const getPaginatedFullCusQuery = ({
|
||||
|
||||
${
|
||||
withSubs
|
||||
? sql`, customer_subscriptions AS (
|
||||
? sql`, customer_subscriptions AS (
|
||||
SELECT
|
||||
s.internal_customer_id,
|
||||
COALESCE(
|
||||
@@ -683,7 +683,7 @@ export const getPaginatedFullCusQuery = ({
|
||||
|
||||
${
|
||||
includeInvoices
|
||||
? sql`, customer_invoices AS (
|
||||
? sql`, customer_invoices AS (
|
||||
SELECT
|
||||
cr.internal_id AS internal_customer_id,
|
||||
COALESCE(
|
||||
|
||||
@@ -1,43 +1,68 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { type ApiCustomerV3, CustomerExpand } from "@autumn/shared";
|
||||
import { test } from "bun:test";
|
||||
import { type ApiCustomerV3, tryCatch } from "@autumn/shared";
|
||||
import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect";
|
||||
import {
|
||||
calculateTrialEndMs,
|
||||
expectProductTrialing,
|
||||
} from "@tests/integration/billing/utils/expectCustomerProductTrialing";
|
||||
import { TestFeature } from "@tests/setup/v2Features";
|
||||
import { items } from "@tests/utils/fixtures/items";
|
||||
import { products } from "@tests/utils/fixtures/products";
|
||||
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario";
|
||||
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils";
|
||||
|
||||
test("temp: create entity updates cached customer", async () => {
|
||||
const customerId = `temp-cached-customer-${Date.now()}`;
|
||||
const entityId = `${customerId}-entity-1`;
|
||||
test("temp: paid default trial customer can upgrade to premium", async () => {
|
||||
const customerId = `temp-default-trial-upgrade`;
|
||||
|
||||
const { autumnV1 } = await initScenario({
|
||||
const defaultTrial = products.defaultTrial({
|
||||
id: "default-trial",
|
||||
items: [items.monthlyMessages({ includedUsage: 500 })],
|
||||
trialDays: 7,
|
||||
cardRequired: false,
|
||||
});
|
||||
|
||||
const premium = products.premium({
|
||||
id: "premium",
|
||||
items: [items.monthlyMessages({ includedUsage: 1000 })],
|
||||
});
|
||||
|
||||
const { autumnV1, ctx, testClockId } = await initScenario({
|
||||
customerId,
|
||||
setup: [s.customer({ testClock: false })],
|
||||
setup: [
|
||||
s.customer({ testClock: true, withDefault: true }),
|
||||
s.products({ list: [defaultTrial, premium] }),
|
||||
],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
await autumnV1.customers.get<ApiCustomerV3>(customerId); // set customer in the cache
|
||||
const customerBeforeUpgrade =
|
||||
await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||
|
||||
await autumnV1.entitiesV2.create({
|
||||
customer_id: customerId,
|
||||
entity_id: entityId,
|
||||
name: "Temp Entity",
|
||||
feature_id: TestFeature.Users,
|
||||
await expectProductTrialing({
|
||||
customer: customerBeforeUpgrade,
|
||||
productId: defaultTrial.id,
|
||||
trialEndsAt: calculateTrialEndMs({ trialDays: 7 }),
|
||||
});
|
||||
|
||||
const customerFromCache = await autumnV1.customers.get<ApiCustomerV3>(
|
||||
customerId,
|
||||
{
|
||||
expand: [CustomerExpand.Entities],
|
||||
},
|
||||
);
|
||||
expectCustomerFeatureCorrect({
|
||||
customer: customerBeforeUpgrade,
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 500,
|
||||
balance: 500,
|
||||
usage: 0,
|
||||
});
|
||||
|
||||
expect(customerFromCache.entities).toBeDefined();
|
||||
try {
|
||||
await autumnV1.billing.attach({
|
||||
customer_id: customerId,
|
||||
product_id: premium.id,
|
||||
// redirect_mode: "redirect_mode",
|
||||
});
|
||||
} catch (error) {}
|
||||
|
||||
const createdEntity = customerFromCache.entities?.find(
|
||||
(entity) => entity.id === entityId,
|
||||
);
|
||||
|
||||
expect(createdEntity).toBeDefined();
|
||||
expect(createdEntity).toMatchObject({
|
||||
id: entityId,
|
||||
name: "Temp Entity",
|
||||
await advanceTestClock({
|
||||
stripeCli: ctx.stripeCli,
|
||||
testClockId: testClockId!,
|
||||
numberOfDays: 12,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
boolean,
|
||||
foreignKey,
|
||||
index,
|
||||
numeric,
|
||||
pgTable,
|
||||
text,
|
||||
@@ -25,5 +26,6 @@ export const freeTrials = pgTable(
|
||||
foreignColumns: [products.internal_id],
|
||||
name: "free_trials_internal_product_id_fkey",
|
||||
}).onDelete("cascade"),
|
||||
index("idx_free_trials_internal_product_id").on(table.internal_product_id),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
text,
|
||||
unique,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { collatePgColumn, sqlNow } from "../../db/utils";
|
||||
import { sqlNow } from "../../db/utils";
|
||||
import { organizations } from "../orgModels/orgTable";
|
||||
|
||||
type ProductProcessor = {
|
||||
@@ -48,5 +48,3 @@ export const products = pgTable(
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
collatePgColumn(products.internal_id, "C");
|
||||
|
||||
Reference in New Issue
Block a user