feat: new versioning system
This commit is contained in:
14
bun.lock
14
bun.lock
@@ -47,6 +47,7 @@
|
|||||||
"@react-email/components": "^0.0.42",
|
"@react-email/components": "^0.0.42",
|
||||||
"@sentry/node": "^9.30.0",
|
"@sentry/node": "^9.30.0",
|
||||||
"@supabase/supabase-js": "^2.46.2",
|
"@supabase/supabase-js": "^2.46.2",
|
||||||
|
"@types/qs": "^6.14.0",
|
||||||
"@types/semver": "^7.7.1",
|
"@types/semver": "^7.7.1",
|
||||||
"@upstash/redis": "^1.35.1",
|
"@upstash/redis": "^1.35.1",
|
||||||
"ai": "^4.3.10",
|
"ai": "^4.3.10",
|
||||||
@@ -86,6 +87,7 @@
|
|||||||
"postgres": "^3.4.7",
|
"postgres": "^3.4.7",
|
||||||
"posthog-node": "^4.17.2",
|
"posthog-node": "^4.17.2",
|
||||||
"puppeteer-core": "^24.14.0",
|
"puppeteer-core": "^24.14.0",
|
||||||
|
"qs": "^6.14.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"recaseai": "^0.0.37",
|
"recaseai": "^0.0.37",
|
||||||
"resend": "^4.1.1",
|
"resend": "^4.1.1",
|
||||||
@@ -2287,7 +2289,7 @@
|
|||||||
|
|
||||||
"pvutils": ["pvutils@1.1.3", "", {}, "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ=="],
|
"pvutils": ["pvutils@1.1.3", "", {}, "sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ=="],
|
||||||
|
|
||||||
"qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
|
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
|
||||||
|
|
||||||
"querystringify": ["querystringify@2.2.0", "", {}, "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="],
|
"querystringify": ["querystringify@2.2.0", "", {}, "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="],
|
||||||
|
|
||||||
@@ -3077,6 +3079,10 @@
|
|||||||
|
|
||||||
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
"body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||||
|
|
||||||
|
"body-parser/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
|
||||||
|
|
||||||
|
"chai-http/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
|
||||||
|
|
||||||
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
@@ -3097,6 +3103,8 @@
|
|||||||
|
|
||||||
"express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
"express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||||
|
|
||||||
|
"express/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
|
||||||
|
|
||||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||||
|
|
||||||
"figures/is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
|
"figures/is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="],
|
||||||
@@ -3171,6 +3179,10 @@
|
|||||||
|
|
||||||
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
|
"stripe/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
|
||||||
|
|
||||||
|
"superagent/qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
|
||||||
|
|
||||||
"svix/@types/node": ["@types/node@22.17.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w=="],
|
"svix/@types/node": ["@types/node@22.17.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w=="],
|
||||||
|
|
||||||
"svix/uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
|
"svix/uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ async function startDev() {
|
|||||||
"bunx",
|
"bunx",
|
||||||
[
|
[
|
||||||
"concurrently",
|
"concurrently",
|
||||||
`"cd shared && bun dev"`,
|
`"cd shared && bun run build && bun tsc"`,
|
||||||
`"cd server && SERVER_PORT=${serverPort} bun dev"`,
|
`"cd server && SERVER_PORT=${serverPort} bun dev"`,
|
||||||
`"cd server && bun workers:dev"`,
|
`"cd server && bun workers:dev"`,
|
||||||
`"cd vite && VITE_PORT=${vitePort} bun dev"`,
|
`"cd vite && VITE_PORT=${vitePort} bun dev"`,
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
"@react-email/components": "^0.0.42",
|
"@react-email/components": "^0.0.42",
|
||||||
"@sentry/node": "^9.30.0",
|
"@sentry/node": "^9.30.0",
|
||||||
"@supabase/supabase-js": "^2.46.2",
|
"@supabase/supabase-js": "^2.46.2",
|
||||||
|
"@types/qs": "^6.14.0",
|
||||||
"@types/semver": "^7.7.1",
|
"@types/semver": "^7.7.1",
|
||||||
"@upstash/redis": "^1.35.1",
|
"@upstash/redis": "^1.35.1",
|
||||||
"ai": "^4.3.10",
|
"ai": "^4.3.10",
|
||||||
@@ -88,6 +89,7 @@
|
|||||||
"postgres": "^3.4.7",
|
"postgres": "^3.4.7",
|
||||||
"posthog-node": "^4.17.2",
|
"posthog-node": "^4.17.2",
|
||||||
"puppeteer-core": "^24.14.0",
|
"puppeteer-core": "^24.14.0",
|
||||||
|
"qs": "^6.14.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"recaseai": "^0.0.37",
|
"recaseai": "^0.0.37",
|
||||||
"resend": "^4.1.1",
|
"resend": "^4.1.1",
|
||||||
|
|||||||
@@ -7,20 +7,20 @@ source "$(dirname "$0")/config.sh"
|
|||||||
if [[ "$1" == *"setup"* ]]; then
|
if [[ "$1" == *"setup"* ]]; then
|
||||||
MOCHA_PARALLEL=true $MOCHA_SETUP
|
MOCHA_PARALLEL=true $MOCHA_SETUP
|
||||||
fi
|
fi
|
||||||
$MOCHA_CMD 'tests/advanced/referrals/*.ts' 'tests/advanced/coupons/*.ts'
|
# $MOCHA_CMD 'tests/advanced/referrals/*.ts' 'tests/advanced/coupons/*.ts'
|
||||||
|
|
||||||
# $MOCHA_CMD 'tests/advanced/multiFeature/*.ts' \
|
$MOCHA_CMD 'tests/advanced/multiFeature/*.ts' \
|
||||||
# 'tests/advanced/coupons/*.ts' \
|
'tests/advanced/coupons/*.ts' \
|
||||||
# 'tests/attach/updateQuantity/*.ts' \
|
'tests/attach/updateQuantity/*.ts' \
|
||||||
# 'tests/advanced/referrals/*.ts' \
|
'tests/advanced/referrals/*.ts' \
|
||||||
# 'tests/advanced/referrals/paid/*.ts' \
|
'tests/advanced/referrals/paid/*.ts' \
|
||||||
# 'tests/advanced/rollovers/*.ts' \
|
'tests/advanced/rollovers/*.ts' \
|
||||||
# 'tests/advanced/customInterval/*.ts'
|
'tests/advanced/customInterval/*.ts'
|
||||||
|
|
||||||
# $MOCHA_CMD 'tests/attach/multiProduct/*.ts' \
|
$MOCHA_CMD 'tests/attach/multiProduct/*.ts' \
|
||||||
# 'tests/advanced/usageLimit/*.ts'
|
'tests/advanced/usageLimit/*.ts'
|
||||||
|
|
||||||
# $MOCHA_CMD 'tests/advanced/usage/*.ts'
|
$MOCHA_CMD 'tests/advanced/usage/*.ts'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -43,9 +43,9 @@ const skipIds = [
|
|||||||
"DxG668K7uDd0Vahk54YWjvCGVgf2",
|
"DxG668K7uDd0Vahk54YWjvCGVgf2",
|
||||||
];
|
];
|
||||||
|
|
||||||
orgSlugs = ["capy"];
|
orgSlugs = ["feather"];
|
||||||
const customerId = null;
|
let customerId = null;
|
||||||
// customerId = "55765cf6-84af-11f0-a94e-3eef481a796b";
|
customerId = "3aa8ebee-28cb-4293-97e8-becf55bc2159";
|
||||||
|
|
||||||
const getSingleCustomer = async ({
|
const getSingleCustomer = async ({
|
||||||
stripeCli,
|
stripeCli,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
|
ApiVersion,
|
||||||
type Customer,
|
type Customer,
|
||||||
EntInterval,
|
EntInterval,
|
||||||
type FullCusProduct,
|
type FullCusProduct,
|
||||||
type FullCustomerEntitlement,
|
type FullCustomerEntitlement,
|
||||||
LegacyVersion,
|
|
||||||
type Organization,
|
type Organization,
|
||||||
type Price,
|
type Price,
|
||||||
type UsagePriceConfig,
|
type UsagePriceConfig,
|
||||||
@@ -53,8 +53,8 @@ export const handleUsagePrices = async ({
|
|||||||
) < 10;
|
) < 10;
|
||||||
|
|
||||||
const invoiceFromUpgrade =
|
const invoiceFromUpgrade =
|
||||||
invoice.billing_reason == "subscription_update" ||
|
invoice.billing_reason === "subscription_update" ||
|
||||||
invoice.billing_reason == "manual";
|
invoice.billing_reason === "manual";
|
||||||
|
|
||||||
if (invoiceCreatedRecently) {
|
if (invoiceCreatedRecently) {
|
||||||
logger.info("Invoice created recently, skipping");
|
logger.info("Invoice created recently, skipping");
|
||||||
@@ -84,7 +84,7 @@ export const handleUsagePrices = async ({
|
|||||||
|
|
||||||
const isNewUsageMethod =
|
const isNewUsageMethod =
|
||||||
activeProduct.internal_entity_id ||
|
activeProduct.internal_entity_id ||
|
||||||
activeProduct.api_version === LegacyVersion.v1_4;
|
activeProduct.api_semver === ApiVersion.Beta;
|
||||||
|
|
||||||
if (isNewUsageMethod) {
|
if (isNewUsageMethod) {
|
||||||
const invoiceItem = getInvoiceItemForUsage({
|
const invoiceItem = getInvoiceItemForUsage({
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const handleCusProductDeleted = async ({
|
|||||||
stripeId: fullCus.processor?.id,
|
stripeId: fullCus.processor?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isV4Usage = cusProduct.api_version === ApiVersion.Beta;
|
const isV4Usage = cusProduct.api_semver === ApiVersion.Beta;
|
||||||
|
|
||||||
// refer to handleUpgradeFlow.ts, when cancel immediately through API / dashboard, this happens...?
|
// refer to handleUpgradeFlow.ts, when cancel immediately through API / dashboard, this happens...?
|
||||||
const isAutumnCancel =
|
const isAutumnCancel =
|
||||||
|
|||||||
@@ -124,9 +124,15 @@ export const handleSchedulePhaseCompleted = async ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error(
|
if (process.env.NODE_ENV === "development") {
|
||||||
`schedule.phase.completed: failed to cancel schedule ${schedule.id}, error: ${error.message}`,
|
logger.warn(
|
||||||
);
|
`schedule.phase.completed: failed to cancel schedule ${schedule.id}, error: ${error.message}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.error(
|
||||||
|
`schedule.phase.completed: failed to cancel schedule ${schedule.id}, error: ${error.message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ export const apiVersionMiddleware = async (c: Context<HonoEnv>, next: Next) => {
|
|||||||
|
|
||||||
// Store in context - now you can do ctx.apiVersion.gte(ApiVersion.V1_1)
|
// Store in context - now you can do ctx.apiVersion.gte(ApiVersion.V1_1)
|
||||||
ctx.apiVersion = finalVersion;
|
ctx.apiVersion = finalVersion;
|
||||||
console.log(`Autumn Version: ${finalVersion.semver}`);
|
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -58,5 +58,21 @@ export const baseMiddleware = async (c: Context<HonoEnv>, next: Next) => {
|
|||||||
env: AppEnv.Sandbox, // maybe use app_env headers
|
env: AppEnv.Sandbox, // maybe use app_env headers
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const method = c.req.method;
|
||||||
|
const path = c.req.path;
|
||||||
|
|
||||||
|
let body = null;
|
||||||
|
if (method === "POST" || method === "PUT" || method === "PATCH") {
|
||||||
|
body = await c.req.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`${method} ${path}`, {
|
||||||
|
context: {
|
||||||
|
body,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`URL: ${c.req.url}`);
|
||||||
|
|
||||||
await next();
|
await next();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export const betterAuthMiddleware = async (c: Context<HonoEnv>, next: Next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: Fetch org and features from database
|
// Step 4: Fetch org and features from database
|
||||||
|
ctx.env = c.req.header("app_env") as AppEnv;
|
||||||
const data = await OrgService.getWithFeatures({
|
const data = await OrgService.getWithFeatures({
|
||||||
db: ctx.db,
|
db: ctx.db,
|
||||||
orgId: orgId,
|
orgId: orgId,
|
||||||
@@ -76,7 +77,6 @@ export const betterAuthMiddleware = async (c: Context<HonoEnv>, next: Next) => {
|
|||||||
|
|
||||||
// Step 5: Store in context
|
// Step 5: Store in context
|
||||||
ctx.org = org;
|
ctx.org = org;
|
||||||
ctx.env = c.req.header("app_env") as AppEnv;
|
|
||||||
ctx.features = features;
|
ctx.features = features;
|
||||||
ctx.userId = userId;
|
ctx.userId = userId;
|
||||||
ctx.authType = AuthType.Dashboard;
|
ctx.authType = AuthType.Dashboard;
|
||||||
|
|||||||
56
server/src/honoMiddlewares/queryMiddleware.ts
Normal file
56
server/src/honoMiddlewares/queryMiddleware.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import type { MiddlewareHandler } from "hono";
|
||||||
|
import qs from "qs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware to parse query strings using the `qs` library
|
||||||
|
* This handles comma-separated arrays automatically and complex nested objects
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* - ?expand=invoices,rewards → { expand: ['invoices', 'rewards'] }
|
||||||
|
* - ?expand[0]=invoices&expand[1]=rewards → { expand: ['invoices', 'rewards'] }
|
||||||
|
* - ?filter[status]=active → { filter: { status: 'active' } }
|
||||||
|
*
|
||||||
|
* Add this middleware before your route validators:
|
||||||
|
* ```ts
|
||||||
|
* router.use(queryMiddleware());
|
||||||
|
* router.get('/customers/:id', validator('query', GetCustomerQuerySchema), handler);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const queryMiddleware = (): MiddlewareHandler => {
|
||||||
|
return async (c, next) => {
|
||||||
|
const url = new URL(c.req.url);
|
||||||
|
const queryString = url.search.slice(1); // Remove leading '?'
|
||||||
|
|
||||||
|
if (queryString) {
|
||||||
|
// Parse with comma support for arrays
|
||||||
|
const parsed = qs.parse(queryString, {
|
||||||
|
comma: true, // Enable comma-separated array parsing
|
||||||
|
depth: 5, // Limit depth to prevent prototype pollution
|
||||||
|
allowDots: false, // Disable dot notation (filter.status) to prevent confusion
|
||||||
|
decoder: (str, defaultDecoder, charset, type) => {
|
||||||
|
// Use default decoder but handle edge cases
|
||||||
|
if (type === "value") {
|
||||||
|
// Handle boolean strings
|
||||||
|
if (str === "true") return true;
|
||||||
|
if (str === "false") return false;
|
||||||
|
// Handle numbers if needed (optional)
|
||||||
|
// const num = Number(str);
|
||||||
|
// if (!isNaN(num) && str === String(num)) return num;
|
||||||
|
}
|
||||||
|
return defaultDecoder(str, defaultDecoder, charset);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Store parsed query for Hono to use
|
||||||
|
// @ts-expect-error - Hono's query parsing internals
|
||||||
|
c.req.query = (key?: string) => {
|
||||||
|
if (key) {
|
||||||
|
return parsed[key];
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
|
import type { AffectedResource, ApiVersion } from "@autumn/shared";
|
||||||
import type { Context, Env, Handler, MiddlewareHandler } from "hono";
|
import type { Context, Env, Handler, MiddlewareHandler } from "hono";
|
||||||
import type { ZodType, z } from "zod/v4";
|
import type { ZodType, z } from "zod/v4";
|
||||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||||
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
||||||
import { validator } from "./validatorMiddleware.js";
|
import { validator } from "./validatorMiddleware.js";
|
||||||
import { versionedValidator } from "./versionedValidator.js";
|
import { versionedValidator } from "./versionedValidator.js";
|
||||||
import type { AffectedResource, ApiVersion } from "@autumn/shared";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extended context type that includes validated input
|
* Extended context type that includes validated input
|
||||||
@@ -32,7 +32,9 @@ type ValidatedContext<
|
|||||||
/**
|
/**
|
||||||
* Version-specific schemas configuration
|
* Version-specific schemas configuration
|
||||||
*/
|
*/
|
||||||
type VersionedSchemas<T extends ZodType> = Partial<Record<ApiVersion, ZodType>> & {
|
type VersionedSchemas<T extends ZodType> = Partial<
|
||||||
|
Record<ApiVersion, ZodType>
|
||||||
|
> & {
|
||||||
latest: T; // Latest version schema (required)
|
latest: T; // Latest version schema (required)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import type { ZodType } from "zod/v4";
|
|||||||
* This allows the errorMiddleware to handle validation errors consistently
|
* This allows the errorMiddleware to handle validation errors consistently
|
||||||
* Maintains full type inference from zValidator
|
* Maintains full type inference from zValidator
|
||||||
*
|
*
|
||||||
|
* For query validation, this uses the parsed query from queryMiddleware
|
||||||
|
* to ensure boolean/array conversions are applied before validation
|
||||||
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* ```ts
|
* ```ts
|
||||||
* router.post(
|
* router.post(
|
||||||
@@ -22,6 +25,20 @@ export const validator = <T extends ZodType>(
|
|||||||
target: "json" | "query" | "param" | "header" | "form",
|
target: "json" | "query" | "param" | "header" | "form",
|
||||||
schema: T,
|
schema: T,
|
||||||
) => {
|
) => {
|
||||||
|
// For query validation, use the parsed query from queryMiddleware
|
||||||
|
if (target === "query") {
|
||||||
|
return async (c: any, next: any) => {
|
||||||
|
const parsedQuery = c.req.query();
|
||||||
|
const result = schema.safeParse(parsedQuery);
|
||||||
|
if (!result.success) {
|
||||||
|
throw result.error;
|
||||||
|
}
|
||||||
|
// Store validated data for c.req.valid("query")
|
||||||
|
c.req.addValidatedData(target, result.data);
|
||||||
|
await next();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return zValidator(target, schema, (result, _c) => {
|
return zValidator(target, schema, (result, _c) => {
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
throw result.error;
|
throw result.error;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { apiVersionMiddleware } from "./honoMiddlewares/apiVersionMiddleware.js"
|
|||||||
import { baseMiddleware } from "./honoMiddlewares/baseMiddleware.js";
|
import { baseMiddleware } from "./honoMiddlewares/baseMiddleware.js";
|
||||||
import { errorMiddleware } from "./honoMiddlewares/errorMiddleware.js";
|
import { errorMiddleware } from "./honoMiddlewares/errorMiddleware.js";
|
||||||
import { orgConfigMiddleware } from "./honoMiddlewares/orgConfigMiddleware.js";
|
import { orgConfigMiddleware } from "./honoMiddlewares/orgConfigMiddleware.js";
|
||||||
|
import { queryMiddleware } from "./honoMiddlewares/queryMiddleware.js";
|
||||||
import { refreshCacheMiddleware } from "./honoMiddlewares/refreshCacheMiddleware.js";
|
import { refreshCacheMiddleware } from "./honoMiddlewares/refreshCacheMiddleware.js";
|
||||||
import { secretKeyMiddleware } from "./honoMiddlewares/secretKeyMiddleware.js";
|
import { secretKeyMiddleware } from "./honoMiddlewares/secretKeyMiddleware.js";
|
||||||
import { traceMiddleware } from "./honoMiddlewares/traceMiddleware.js";
|
import { traceMiddleware } from "./honoMiddlewares/traceMiddleware.js";
|
||||||
@@ -81,6 +82,8 @@ export const createHonoApp = () => {
|
|||||||
app.use("/v1/*", refreshCacheMiddleware);
|
app.use("/v1/*", refreshCacheMiddleware);
|
||||||
|
|
||||||
// Step 7: Add pricing middleware, analytics middleware, etc.
|
// Step 7: Add pricing middleware, analytics middleware, etc.
|
||||||
|
|
||||||
|
app.use("/v1/*", queryMiddleware());
|
||||||
app.route("v1/customers", cusRouter);
|
app.route("v1/customers", cusRouter);
|
||||||
app.route("v1/products", honoProductRouter);
|
app.route("v1/products", honoProductRouter);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
type APICusProduct,
|
type ApiCusProduct,
|
||||||
ApiVersion,
|
ApiVersion,
|
||||||
ApiVersionClass,
|
ApiVersionClass,
|
||||||
type AppEnv,
|
type AppEnv,
|
||||||
@@ -79,7 +79,7 @@ export const getSingleEntityResponse = async ({
|
|||||||
features,
|
features,
|
||||||
});
|
});
|
||||||
|
|
||||||
const products: APICusProduct[] = [...main, ...addOns];
|
const products: ApiCusProduct[] = [...main, ...addOns];
|
||||||
|
|
||||||
const cusFeatures = await getCusFeaturesResponse({
|
const cusFeatures = await getCusFeaturesResponse({
|
||||||
cusProducts: entityCusProducts,
|
cusProducts: entityCusProducts,
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import {
|
import { ApiEntitySchema, EntityExpand, LegacyVersion } from "@autumn/shared";
|
||||||
EntityExpand,
|
|
||||||
EntityResponseSchema,
|
|
||||||
LegacyVersion,
|
|
||||||
} from "@autumn/shared";
|
|
||||||
import { invoicesToResponse } from "@/internal/invoices/invoiceUtils.js";
|
import { invoicesToResponse } from "@/internal/invoices/invoiceUtils.js";
|
||||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||||
import { routeHandler } from "@/utils/routerUtils.js";
|
import { routeHandler } from "@/utils/routerUtils.js";
|
||||||
@@ -20,7 +16,7 @@ export const handleGetEntity = async (req: any, res: any) =>
|
|||||||
const customerId = req.params.customer_id as string;
|
const customerId = req.params.customer_id as string;
|
||||||
const expand = parseEntityExpand(req.query.expand);
|
const expand = parseEntityExpand(req.query.expand);
|
||||||
|
|
||||||
const { orgId, env, db, logger, features } = req;
|
const { env, db, logger, features } = req;
|
||||||
|
|
||||||
const org = await OrgService.getFromReq(req);
|
const org = await OrgService.getFromReq(req);
|
||||||
const apiVersion = orgToVersion({
|
const apiVersion = orgToVersion({
|
||||||
@@ -50,7 +46,7 @@ export const handleGetEntity = async (req: any, res: any) =>
|
|||||||
const withInvoices = expand.includes(EntityExpand.Invoices);
|
const withInvoices = expand.includes(EntityExpand.Invoices);
|
||||||
|
|
||||||
res.status(200).json(
|
res.status(200).json(
|
||||||
EntityResponseSchema.parse({
|
ApiEntitySchema.parse({
|
||||||
...entity,
|
...entity,
|
||||||
invoices: withInvoices
|
invoices: withInvoices
|
||||||
? invoicesToResponse({
|
? invoicesToResponse({
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export const initCusProduct = ({
|
|||||||
quantity: quantity || 1,
|
quantity: quantity || 1,
|
||||||
internal_entity_id: internalEntityId,
|
internal_entity_id: internalEntityId,
|
||||||
entity_id: entityId,
|
entity_id: entityId,
|
||||||
api_version: apiVersion,
|
api_semver: apiVersion || null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ export const handleUpgradeFlow = async ({
|
|||||||
|
|
||||||
const logger = req.logtail;
|
const logger = req.logtail;
|
||||||
|
|
||||||
if (curCusProduct?.api_version) {
|
if (curCusProduct?.api_semver) {
|
||||||
attachParams.apiVersion = curCusProduct.api_version;
|
attachParams.apiVersion = curCusProduct.api_semver;
|
||||||
}
|
}
|
||||||
|
|
||||||
let sub = curSub;
|
let sub = curSub;
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ export const getPricesAndEnts = async ({
|
|||||||
internalEntityId: customer.entity?.internal_id,
|
internalEntityId: customer.entity?.internal_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const curCusProduct = curSameProduct || curMainProduct;
|
||||||
|
|
||||||
// Not custom
|
// Not custom
|
||||||
if (!is_custom) {
|
if (!is_custom) {
|
||||||
const prices = products.flatMap((p: FullProduct) => p.prices);
|
const prices = products.flatMap((p: FullProduct) => p.prices);
|
||||||
@@ -63,7 +65,7 @@ export const getPricesAndEnts = async ({
|
|||||||
optionsInput: optionsInput || [],
|
optionsInput: optionsInput || [],
|
||||||
features,
|
features,
|
||||||
prices,
|
prices,
|
||||||
curCusProduct: curMainProduct,
|
curCusProduct,
|
||||||
}),
|
}),
|
||||||
prices,
|
prices,
|
||||||
entitlements,
|
entitlements,
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ const handlePrepaidErrors = async ({
|
|||||||
if (billingType === BillingType.UsageInAdvance) {
|
if (billingType === BillingType.UsageInAdvance) {
|
||||||
// Get options for price
|
// Get options for price
|
||||||
const priceEnt = getPriceEntitlement(price, entitlements);
|
const priceEnt = getPriceEntitlement(price, entitlements);
|
||||||
|
|
||||||
const options = getEntOptions(optionsList, priceEnt);
|
const options = getEntOptions(optionsList, priceEnt);
|
||||||
|
|
||||||
// 1. If not checkout, quantity should be defined
|
// 1. If not checkout, quantity should be defined
|
||||||
|
|||||||
@@ -68,10 +68,10 @@ export const mapOptionsList = ({
|
|||||||
if (isOneOff(prices) || isFreeProduct(prices)) return newOptionsList;
|
if (isOneOff(prices) || isFreeProduct(prices)) return newOptionsList;
|
||||||
|
|
||||||
const curOptionsList = curCusProduct?.options || [];
|
const curOptionsList = curCusProduct?.options || [];
|
||||||
|
console.log("Current options list:", curOptionsList);
|
||||||
for (const option of curOptionsList) {
|
for (const option of curOptionsList) {
|
||||||
const inNewOptions = newOptionsList.find(
|
const inNewOptions = newOptionsList.find(
|
||||||
(newOption) =>
|
(newOption) => newOption.feature_id === option.feature_id,
|
||||||
newOption.internal_feature_id === option.internal_feature_id,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const prepaidPriceExists = findPrepaidPrice({
|
const prepaidPriceExists = findPrepaidPrice({
|
||||||
@@ -84,5 +84,7 @@ export const mapOptionsList = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`New options list:`, newOptionsList);
|
||||||
|
|
||||||
return newOptionsList;
|
return newOptionsList;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
ACTIVE_STATUSES,
|
ACTIVE_STATUSES,
|
||||||
APICusProductSchema,
|
ApiCusProductSchema,
|
||||||
ApiVersion,
|
ApiVersion,
|
||||||
type ApiVersionClass,
|
type ApiVersionClass,
|
||||||
type AppEnv,
|
type AppEnv,
|
||||||
@@ -512,7 +512,7 @@ export const processFullCusProduct = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return APICusProductSchema.parse({
|
return ApiCusProductSchema.parse({
|
||||||
id: cusProduct.product.id,
|
id: cusProduct.product.id,
|
||||||
name: cusProduct.product.name,
|
name: cusProduct.product.name,
|
||||||
group: cusProduct.product.group || null,
|
group: cusProduct.product.group || null,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { handleAddCouponToCus } from "./handlers/handleAddCouponToCus.js";
|
|||||||
import { handleCreateBillingPortal } from "./handlers/handleCreateBillingPortal.js";
|
import { handleCreateBillingPortal } from "./handlers/handleCreateBillingPortal.js";
|
||||||
import { handleDeleteCustomer } from "./handlers/handleDeleteCustomer.js";
|
import { handleDeleteCustomer } from "./handlers/handleDeleteCustomer.js";
|
||||||
import { handleGetCustomerV2 } from "./handlers/handleGetCustomerV2.js";
|
import { handleGetCustomerV2 } from "./handlers/handleGetCustomerV2.js";
|
||||||
import { handlePostCustomerRequest } from "./handlers/handlePostCustomer.js";
|
import { handlePostCustomer } from "./handlers/handlePostCustomerV2.js";
|
||||||
import { handleTransferProduct } from "./handlers/handleTransferProduct.js";
|
import { handleTransferProduct } from "./handlers/handleTransferProduct.js";
|
||||||
import { handleUpdateBalances } from "./handlers/handleUpdateBalances.js";
|
import { handleUpdateBalances } from "./handlers/handleUpdateBalances.js";
|
||||||
import { handleUpdateCustomer } from "./handlers/handleUpdateCustomer.js";
|
import { handleUpdateCustomer } from "./handlers/handleUpdateCustomer.js";
|
||||||
@@ -47,7 +47,7 @@ expressCusRouter.post("/all/search", async (req: any, res: any) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
expressCusRouter.post("", handlePostCustomerRequest);
|
// expressCusRouter.post("", handlePostCustomerRequest);
|
||||||
|
|
||||||
// cusRouter.get("/:customer_id", handleGetCustomer);
|
// cusRouter.get("/:customer_id", handleGetCustomer);
|
||||||
|
|
||||||
@@ -150,3 +150,4 @@ expressCusRouter.post("/:customer_id/transfer", handleTransferProduct);
|
|||||||
export const cusRouter = new Hono<HonoEnv>();
|
export const cusRouter = new Hono<HonoEnv>();
|
||||||
|
|
||||||
cusRouter.get("/:customer_id", ...handleGetCustomerV2);
|
cusRouter.get("/:customer_id", ...handleGetCustomerV2);
|
||||||
|
cusRouter.post("", ...handlePostCustomer);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const cusEntsToInterval = ({
|
|||||||
cusEnts: FullCusEntWithFullCusProduct[];
|
cusEnts: FullCusEntWithFullCusProduct[];
|
||||||
}): {
|
}): {
|
||||||
interval: EntInterval | "multiple" | null;
|
interval: EntInterval | "multiple" | null;
|
||||||
interval_count: number;
|
interval_count: number | null;
|
||||||
} => {
|
} => {
|
||||||
const cusEntKeys = cusEnts.map((cusEnt) => cusEntToKey({ cusEnt }));
|
const cusEntKeys = cusEnts.map((cusEnt) => cusEntToKey({ cusEnt }));
|
||||||
const uniqueCusEntKeys = [...new Set(cusEntKeys)];
|
const uniqueCusEntKeys = [...new Set(cusEntKeys)];
|
||||||
@@ -27,7 +27,7 @@ export const cusEntsToInterval = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { interval: "multiple", interval_count: 0 };
|
return { interval: "multiple", interval_count: null };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const cusEntsToNextResetAt = ({
|
export const cusEntsToNextResetAt = ({
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
import {
|
|
||||||
AffectedResource,
|
|
||||||
APICusProductSchema,
|
|
||||||
type ApiVersion,
|
|
||||||
ApiVersionClass,
|
|
||||||
applyResponseVersionChanges,
|
|
||||||
CusProductStatus,
|
|
||||||
type Feature,
|
|
||||||
type FullCusProduct,
|
|
||||||
LATEST_VERSION,
|
|
||||||
type Subscription,
|
|
||||||
} from "@autumn/shared";
|
|
||||||
import { getProductResponse } from "@/internal/products/productUtils/productResponseUtils/getProductResponse.js";
|
|
||||||
import { fullCusProductToProduct } from "../../cusProducts/cusProductUtils.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a customer product in LATEST format, then applies version changes
|
|
||||||
*
|
|
||||||
* Latest format (V1_1+):
|
|
||||||
* - Has `items` field with product features
|
|
||||||
* - Has `started_at` instead of `starts_at`
|
|
||||||
* - Includes current_period_start/end for trials
|
|
||||||
*/
|
|
||||||
export const getApiCusProduct = async ({
|
|
||||||
cusProduct,
|
|
||||||
subs,
|
|
||||||
features,
|
|
||||||
apiVersion,
|
|
||||||
entity,
|
|
||||||
}: {
|
|
||||||
cusProduct: FullCusProduct;
|
|
||||||
subs?: Subscription[];
|
|
||||||
features: Feature[];
|
|
||||||
apiVersion: ApiVersion;
|
|
||||||
entity?: any;
|
|
||||||
}): Promise<any> => {
|
|
||||||
// Determine if trialing
|
|
||||||
const trialing =
|
|
||||||
cusProduct.trial_ends_at && cusProduct.trial_ends_at > Date.now();
|
|
||||||
|
|
||||||
// Build stripe subscription data
|
|
||||||
const subIds = cusProduct.subscription_ids;
|
|
||||||
let stripeSubData = {};
|
|
||||||
|
|
||||||
if ((!subIds || subIds.length === 0) && trialing) {
|
|
||||||
stripeSubData = {
|
|
||||||
current_period_start: cusProduct.starts_at,
|
|
||||||
current_period_end: cusProduct.trial_ends_at,
|
|
||||||
};
|
|
||||||
} else if (subIds && subIds.length > 0 && subs) {
|
|
||||||
const baseSub = subs.find(
|
|
||||||
(s) => s.id === subIds[0] || (s as Subscription).stripe_id === subIds[0],
|
|
||||||
);
|
|
||||||
if (baseSub) {
|
|
||||||
stripeSubData = {
|
|
||||||
current_period_end: baseSub.current_period_end
|
|
||||||
? baseSub.current_period_end * 1000
|
|
||||||
: null,
|
|
||||||
current_period_start: baseSub.current_period_start
|
|
||||||
? baseSub.current_period_start * 1000
|
|
||||||
: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build product in LATEST format (V1_1+)
|
|
||||||
const fullProduct = fullCusProductToProduct(cusProduct);
|
|
||||||
const v2Product = await getProductResponse({
|
|
||||||
product: fullProduct,
|
|
||||||
features,
|
|
||||||
withDisplay: false,
|
|
||||||
options: cusProduct.options,
|
|
||||||
});
|
|
||||||
|
|
||||||
const latestProduct = APICusProductSchema.parse({
|
|
||||||
id: fullProduct.id,
|
|
||||||
name: fullProduct.name,
|
|
||||||
group: fullProduct.group || null,
|
|
||||||
status: trialing ? CusProductStatus.Trialing : cusProduct.status,
|
|
||||||
canceled_at: cusProduct.canceled_at || null,
|
|
||||||
is_default: fullProduct.is_default || false,
|
|
||||||
is_add_on: fullProduct.is_add_on || false,
|
|
||||||
version: fullProduct.version,
|
|
||||||
quantity: cusProduct.quantity,
|
|
||||||
started_at: cusProduct.starts_at,
|
|
||||||
entity_id: entity?.id || cusProduct.entity_id || undefined,
|
|
||||||
...stripeSubData,
|
|
||||||
items: v2Product.items, // V1_1+ has items field
|
|
||||||
});
|
|
||||||
|
|
||||||
// Apply version changes to transform to requested version
|
|
||||||
// This will remove items field for V0_1
|
|
||||||
return applyResponseVersionChanges({
|
|
||||||
input: latestProduct,
|
|
||||||
currentVersion: new ApiVersionClass(LATEST_VERSION),
|
|
||||||
targetVersion: new ApiVersionClass(apiVersion),
|
|
||||||
resource: AffectedResource.CusProduct,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,247 @@
|
|||||||
|
import {
|
||||||
|
ApiCusProductSchema,
|
||||||
|
type CusProductLegacyData,
|
||||||
|
CusProductStatus,
|
||||||
|
cusProductToProduct,
|
||||||
|
type Entity,
|
||||||
|
type FullCusProduct,
|
||||||
|
type FullCustomer,
|
||||||
|
type Subscription,
|
||||||
|
} from "@autumn/shared";
|
||||||
|
import type { RequestContext } from "@/honoUtils/HonoEnv.js";
|
||||||
|
import { getProductResponse } from "@/internal/products/productUtils/productResponseUtils/getProductResponse.js";
|
||||||
|
|
||||||
|
// const prices = cusProduct.customer_prices.map((cp) => {
|
||||||
|
// const price = cp.price;
|
||||||
|
|
||||||
|
// if (price.config?.type === PriceType.Fixed) {
|
||||||
|
// const config = price.config as FixedPriceConfig;
|
||||||
|
// return {
|
||||||
|
// amount: config.amount,
|
||||||
|
// interval: config.interval,
|
||||||
|
// };
|
||||||
|
// } else {
|
||||||
|
// const config = price.config as UsagePriceConfig;
|
||||||
|
// const priceOptions = getPriceOptions(price, cusProduct.options);
|
||||||
|
// const usageTier = getUsageTier(price, priceOptions?.quantity!);
|
||||||
|
// const cusEnt = getRelatedCusEnt({
|
||||||
|
// cusPrice: cp,
|
||||||
|
// cusEnts: cusProduct.customer_entitlements,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const ent = cusEnt?.entitlement;
|
||||||
|
|
||||||
|
// const singleTier =
|
||||||
|
// ent?.allowance === 0 && config.usage_tiers.length === 1;
|
||||||
|
|
||||||
|
// if (singleTier) {
|
||||||
|
// return {
|
||||||
|
// amount: usageTier.amount,
|
||||||
|
// interval: config.interval,
|
||||||
|
// quantity: priceOptions?.quantity,
|
||||||
|
// };
|
||||||
|
// } else {
|
||||||
|
// // Add allowance to tiers
|
||||||
|
// const allowance = ent?.allowance;
|
||||||
|
// let tiers;
|
||||||
|
|
||||||
|
// if (notNullish(allowance) && allowance! > 0) {
|
||||||
|
// tiers = [
|
||||||
|
// {
|
||||||
|
// to: allowance,
|
||||||
|
// amount: 0,
|
||||||
|
// },
|
||||||
|
// ...config.usage_tiers.map((tier) => {
|
||||||
|
// const isLastTier = tier.to === -1 || tier.to === TierInfinite;
|
||||||
|
// return {
|
||||||
|
// to: isLastTier ? tier.to : Number(tier.to) + allowance!,
|
||||||
|
// amount: tier.amount,
|
||||||
|
// };
|
||||||
|
// }),
|
||||||
|
// ];
|
||||||
|
// } else {
|
||||||
|
// tiers = config.usage_tiers.map((tier) => {
|
||||||
|
// const isLastTier = tier.to === -1 || tier.to === TierInfinite;
|
||||||
|
// return {
|
||||||
|
// to: isLastTier ? tier.to : Number(tier.to) + allowance!,
|
||||||
|
// amount: tier.amount,
|
||||||
|
// };
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// tiers: tiers,
|
||||||
|
// name: "",
|
||||||
|
// quantity: priceOptions?.quantity,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
export const getApiCusProduct = async ({
|
||||||
|
ctx,
|
||||||
|
fullCus,
|
||||||
|
cusProduct,
|
||||||
|
entities,
|
||||||
|
// cusProduct,
|
||||||
|
// subs,
|
||||||
|
// org,
|
||||||
|
// apiVersion,
|
||||||
|
// features,
|
||||||
|
// entity,
|
||||||
|
}: {
|
||||||
|
ctx: RequestContext;
|
||||||
|
fullCus: FullCustomer;
|
||||||
|
cusProduct: FullCusProduct;
|
||||||
|
entities: Entity[];
|
||||||
|
// org: Organization;
|
||||||
|
// subs?: Subscription[];
|
||||||
|
// apiVersion: ApiVersionClass;
|
||||||
|
// features: Feature[];
|
||||||
|
// entity?: Entity;
|
||||||
|
}) => {
|
||||||
|
const { org } = ctx;
|
||||||
|
const trialing =
|
||||||
|
cusProduct.trial_ends_at && cusProduct.trial_ends_at > Date.now();
|
||||||
|
|
||||||
|
const fullProduct = cusProductToProduct({ cusProduct });
|
||||||
|
const v2Product = await getProductResponse({
|
||||||
|
product: fullProduct,
|
||||||
|
features: ctx.features,
|
||||||
|
fullCus,
|
||||||
|
currency: org.default_currency || "usd",
|
||||||
|
options: cusProduct.options,
|
||||||
|
withDisplay: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const entity = entities.find(
|
||||||
|
(e) => e.internal_id === cusProduct.internal_entity_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
const subId = cusProduct.subscription_ids?.[0];
|
||||||
|
const autumnSub = fullCus.subscriptions?.find(
|
||||||
|
(s) => s.id === subId || (s as Subscription).stripe_id === subId,
|
||||||
|
);
|
||||||
|
|
||||||
|
let stripeSubData = {};
|
||||||
|
if (autumnSub) {
|
||||||
|
stripeSubData = {
|
||||||
|
current_period_end: autumnSub?.current_period_end
|
||||||
|
? autumnSub.current_period_end * 1000
|
||||||
|
: null,
|
||||||
|
current_period_start: autumnSub?.current_period_start
|
||||||
|
? autumnSub.current_period_start * 1000
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!subId && trialing) {
|
||||||
|
stripeSubData = {
|
||||||
|
current_period_start: cusProduct.starts_at,
|
||||||
|
current_period_end: cusProduct.trial_ends_at,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiCusProduct = ApiCusProductSchema.parse({
|
||||||
|
id: fullProduct.id,
|
||||||
|
name: fullProduct.name,
|
||||||
|
group: fullProduct.group || null,
|
||||||
|
status: trialing ? CusProductStatus.Trialing : cusProduct.status,
|
||||||
|
canceled_at: cusProduct.canceled_at || null,
|
||||||
|
|
||||||
|
is_default: fullProduct.is_default || false,
|
||||||
|
is_add_on: fullProduct.is_add_on || false,
|
||||||
|
version: fullProduct.version,
|
||||||
|
quantity: cusProduct.quantity,
|
||||||
|
|
||||||
|
started_at: cusProduct.starts_at,
|
||||||
|
entity_id: entity?.id || cusProduct.entity_id || undefined,
|
||||||
|
|
||||||
|
...stripeSubData,
|
||||||
|
items: v2Product.items,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: apiCusProduct,
|
||||||
|
legacyData: {
|
||||||
|
subscription_id: subId || undefined,
|
||||||
|
} satisfies CusProductLegacyData,
|
||||||
|
};
|
||||||
|
|
||||||
|
// if (subIds && subIds.length > 0 && apiVersion.gte(ApiVersion.V0_2)) {
|
||||||
|
// const baseSub = subs?.find(
|
||||||
|
// (s) => s.id === subIds[0] || (s as Subscription).stripe_id === subIds[0],
|
||||||
|
// );
|
||||||
|
// stripeSubData = {
|
||||||
|
// current_period_end: baseSub?.current_period_end
|
||||||
|
// ? baseSub.current_period_end * 1000
|
||||||
|
// : null,
|
||||||
|
// current_period_start: baseSub?.current_period_start
|
||||||
|
// ? baseSub.current_period_start * 1000
|
||||||
|
// : null,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (!subIds && trialing) {
|
||||||
|
// stripeSubData = {
|
||||||
|
// current_period_start: cusProduct.starts_at,
|
||||||
|
// current_period_end: cusProduct.trial_ends_at,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (apiVersion.gte(ApiVersion.V1_1)) {
|
||||||
|
// if ((!subIds || subIds.length === 0) && trialing) {
|
||||||
|
// stripeSubData = {
|
||||||
|
// current_period_start: cusProduct.starts_at,
|
||||||
|
// current_period_end: cusProduct.trial_ends_at,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const fullProduct = fullCusProductToProduct(cusProduct);
|
||||||
|
// const v2Product = await getProductResponse({
|
||||||
|
// product: fullProduct,
|
||||||
|
// features,
|
||||||
|
// withDisplay: false,
|
||||||
|
// options: cusProduct.options,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return ApiCusProductSchema.parse({
|
||||||
|
// id: fullProduct.id,
|
||||||
|
// name: fullProduct.name,
|
||||||
|
// group: fullProduct.group || null,
|
||||||
|
// status: trialing ? CusProductStatus.Trialing : cusProduct.status,
|
||||||
|
// canceled_at: cusProduct.canceled_at || null,
|
||||||
|
|
||||||
|
// is_default: fullProduct.is_default || false,
|
||||||
|
// is_add_on: fullProduct.is_add_on || false,
|
||||||
|
// version: fullProduct.version,
|
||||||
|
// quantity: cusProduct.quantity,
|
||||||
|
|
||||||
|
// started_at: cusProduct.starts_at,
|
||||||
|
// entity_id: entity?.id || cusProduct.entity_id || undefined,
|
||||||
|
|
||||||
|
// ...stripeSubData,
|
||||||
|
// items: v2Product.items,
|
||||||
|
// });
|
||||||
|
// } else {
|
||||||
|
// const cusProductResponse = {
|
||||||
|
// id: cusProduct.product.id,
|
||||||
|
// name: cusProduct.product.name,
|
||||||
|
// group: cusProduct.product.group,
|
||||||
|
// status: trialing ? CusProductStatus.Trialing : cusProduct.status,
|
||||||
|
// created_at: cusProduct.created_at,
|
||||||
|
// canceled_at: cusProduct.canceled_at,
|
||||||
|
// processor: {
|
||||||
|
// type: cusProduct.processor?.type,
|
||||||
|
// subscription_id: cusProduct.processor?.subscription_id || null,
|
||||||
|
// },
|
||||||
|
// subscription_ids: cusProduct.subscription_ids || [],
|
||||||
|
// prices: prices,
|
||||||
|
// starts_at: cusProduct.starts_at,
|
||||||
|
|
||||||
|
// ...stripeSubData,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// return cusProductResponse;
|
||||||
|
// }
|
||||||
|
};
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import {
|
||||||
|
ACTIVE_STATUSES,
|
||||||
|
type ApiCusProduct,
|
||||||
|
type CusProductLegacyData,
|
||||||
|
type CusProductStatus,
|
||||||
|
type FullCustomer,
|
||||||
|
} from "@autumn/shared";
|
||||||
|
import type { RequestContext } from "@/honoUtils/HonoEnv.js";
|
||||||
|
import { getApiCusProduct } from "./getApiCusProduct.js";
|
||||||
|
|
||||||
|
const mergeCusProductResponses = ({
|
||||||
|
cusProductResponses,
|
||||||
|
}: {
|
||||||
|
cusProductResponses: ApiCusProduct[];
|
||||||
|
}) => {
|
||||||
|
const getProductKey = (product: ApiCusProduct) => {
|
||||||
|
const status = ACTIVE_STATUSES.includes(product.status as CusProductStatus)
|
||||||
|
? "active"
|
||||||
|
: product.status;
|
||||||
|
return `${product.id}:${status}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const record: Record<string, any> = {};
|
||||||
|
|
||||||
|
for (const curr of cusProductResponses) {
|
||||||
|
const key = getProductKey(curr);
|
||||||
|
const latest = record[key];
|
||||||
|
|
||||||
|
const currStartedAt = curr.started_at;
|
||||||
|
|
||||||
|
record[key] = {
|
||||||
|
...(latest || curr),
|
||||||
|
version: Math.max(latest?.version || 1, curr?.version || 1),
|
||||||
|
canceled_at: curr.canceled_at
|
||||||
|
? curr.canceled_at
|
||||||
|
: latest?.canceled_at || null,
|
||||||
|
started_at: latest?.started_at
|
||||||
|
? Math.min(latest?.started_at, currStartedAt)
|
||||||
|
: currStartedAt,
|
||||||
|
quantity: (latest?.quantity || 0) + (curr?.quantity || 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(record);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getApiCusProducts = async ({
|
||||||
|
ctx,
|
||||||
|
fullCus,
|
||||||
|
}: {
|
||||||
|
ctx: RequestContext;
|
||||||
|
fullCus: FullCustomer;
|
||||||
|
}) => {
|
||||||
|
// Process full cus products
|
||||||
|
const apiCusProducts: ApiCusProduct[] = [];
|
||||||
|
|
||||||
|
// const inStatuses = ctx.org.config.include_past_due
|
||||||
|
// ? [CusProductStatus.Active, CusProductStatus.PastDue]
|
||||||
|
// : [CusProductStatus.Active];
|
||||||
|
|
||||||
|
// const cusProducts = fullCus.customer_products.filter((cp) =>
|
||||||
|
// inStatuses.includes(cp.status),
|
||||||
|
// );
|
||||||
|
const cusProducts = fullCus.customer_products;
|
||||||
|
|
||||||
|
const legacyData: Record<string, CusProductLegacyData> = {};
|
||||||
|
for (const cusProduct of cusProducts) {
|
||||||
|
const processed = await getApiCusProduct({
|
||||||
|
cusProduct,
|
||||||
|
ctx,
|
||||||
|
fullCus,
|
||||||
|
entities: fullCus.entities,
|
||||||
|
});
|
||||||
|
|
||||||
|
apiCusProducts.push(processed.data);
|
||||||
|
legacyData[processed.data.id] = processed.legacyData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const merged = mergeCusProductResponses({
|
||||||
|
cusProductResponses: apiCusProducts,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiCusProducts: merged,
|
||||||
|
legacyData,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,25 +1,63 @@
|
|||||||
import {
|
import {
|
||||||
AffectedResource,
|
AffectedResource,
|
||||||
|
type ApiCustomer,
|
||||||
ApiCustomerSchema,
|
ApiCustomerSchema,
|
||||||
applyResponseVersionChanges,
|
applyResponseVersionChanges,
|
||||||
|
backwardsChangeActive,
|
||||||
|
CusExpand,
|
||||||
|
type CustomerLegacyData,
|
||||||
type FullCustomer,
|
type FullCustomer,
|
||||||
|
V0_2_InvoicesAlwaysExpanded,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
|
import { z } from "zod/v4";
|
||||||
import type { RequestContext } from "@/honoUtils/HonoEnv.js";
|
import type { RequestContext } from "@/honoUtils/HonoEnv.js";
|
||||||
import { getApiCusFeatures } from "./getApiCusFeature/getApiCusFeatures.js";
|
import { getApiCusFeatures } from "./getApiCusFeature/getApiCusFeatures.js";
|
||||||
|
import { getApiCusProducts } from "./getApiCusProduct/getApiCusProducts.js";
|
||||||
|
import { getApiCustomerExpand } from "./getApiCustomerExpand.js";
|
||||||
|
|
||||||
export const getApiCustomer = async ({
|
export const getApiCustomer = async ({
|
||||||
ctx,
|
ctx,
|
||||||
fullCus,
|
fullCus,
|
||||||
|
expand,
|
||||||
|
withAutumnId = false,
|
||||||
}: {
|
}: {
|
||||||
ctx: RequestContext;
|
ctx: RequestContext;
|
||||||
fullCus: FullCustomer;
|
fullCus: FullCustomer;
|
||||||
|
expand: CusExpand[];
|
||||||
|
withAutumnId?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
|
// SIDE EFFECT
|
||||||
|
if (
|
||||||
|
backwardsChangeActive({
|
||||||
|
apiVersion: ctx.apiVersion,
|
||||||
|
versionChange: V0_2_InvoicesAlwaysExpanded,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
expand.push(CusExpand.Invoices);
|
||||||
|
}
|
||||||
|
|
||||||
const apiCusFeatures = await getApiCusFeatures({
|
const apiCusFeatures = await getApiCusFeatures({
|
||||||
ctx,
|
ctx,
|
||||||
fullCus,
|
fullCus,
|
||||||
});
|
});
|
||||||
|
|
||||||
const apiCustomer = ApiCustomerSchema.parse({
|
const { apiCusProducts, legacyData: cusProductLegacyData } =
|
||||||
|
await getApiCusProducts({
|
||||||
|
ctx,
|
||||||
|
fullCus,
|
||||||
|
});
|
||||||
|
|
||||||
|
const apiCusExpand = await getApiCustomerExpand({
|
||||||
|
ctx,
|
||||||
|
fullCus,
|
||||||
|
expand,
|
||||||
|
});
|
||||||
|
|
||||||
|
const apiCustomer = ApiCustomerSchema.extend({
|
||||||
|
autumn_id: z.string().optional(),
|
||||||
|
}).parse({
|
||||||
|
autumn_id: withAutumnId ? fullCus.internal_id : undefined,
|
||||||
|
|
||||||
id: fullCus.id || null,
|
id: fullCus.id || null,
|
||||||
created_at: fullCus.created_at,
|
created_at: fullCus.created_at,
|
||||||
name: fullCus.name || null,
|
name: fullCus.name || null,
|
||||||
@@ -28,23 +66,18 @@ export const getApiCustomer = async ({
|
|||||||
|
|
||||||
stripe_id: fullCus.processor?.id || null,
|
stripe_id: fullCus.processor?.id || null,
|
||||||
env: fullCus.env,
|
env: fullCus.env,
|
||||||
|
metadata: fullCus.metadata,
|
||||||
|
|
||||||
products: [],
|
products: apiCusProducts,
|
||||||
features: apiCusFeatures,
|
features: apiCusFeatures,
|
||||||
|
...apiCusExpand,
|
||||||
// invoices: z.array(APIInvoiceSchema).optional(),
|
|
||||||
// trials_used: z.array(APITrialsUsedSchema).optional(),
|
|
||||||
|
|
||||||
// rewards: APICusRewardsSchema.nullish(),
|
|
||||||
// metadata: z.record(z.any(), z.any()).default({}),
|
|
||||||
// entities: z.array(EntityResponseSchema).optional(),
|
|
||||||
// referrals: z.array(APICusReferralSchema).optional(),
|
|
||||||
// upcoming_invoice: APICusUpcomingInvoiceSchema.nullish(),
|
|
||||||
// payment_method: z.any().nullish(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return applyResponseVersionChanges({
|
return applyResponseVersionChanges<ApiCustomer, CustomerLegacyData>({
|
||||||
input: apiCustomer,
|
input: apiCustomer,
|
||||||
|
legacyData: {
|
||||||
|
cusProductLegacyData,
|
||||||
|
},
|
||||||
targetVersion: ctx.apiVersion,
|
targetVersion: ctx.apiVersion,
|
||||||
resource: AffectedResource.Customer,
|
resource: AffectedResource.Customer,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import {
|
||||||
|
ApiBaseEntitySchema,
|
||||||
|
type ApiCustomerExpand,
|
||||||
|
CusExpand,
|
||||||
|
type FullCusProduct,
|
||||||
|
type FullCustomer,
|
||||||
|
} from "@autumn/shared";
|
||||||
|
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
|
||||||
|
import { invoicesToResponse } from "@/internal/invoices/invoiceUtils.js";
|
||||||
|
import { getCusPaymentMethodRes } from "../cusResponseUtils/getCusPaymentMethodRes.js";
|
||||||
|
import { getCusReferrals } from "../cusResponseUtils/getCusReferrals.js";
|
||||||
|
import { getCusRewards } from "../cusResponseUtils/getCusRewards.js";
|
||||||
|
import { getCusUpcomingInvoice } from "../cusResponseUtils/getCusUpcomingInvoice.js";
|
||||||
|
|
||||||
|
export const getApiCustomerExpand = async ({
|
||||||
|
ctx,
|
||||||
|
fullCus,
|
||||||
|
expand,
|
||||||
|
}: {
|
||||||
|
ctx: AutumnContext;
|
||||||
|
fullCus: FullCustomer;
|
||||||
|
expand: CusExpand[];
|
||||||
|
}): Promise<ApiCustomerExpand> => {
|
||||||
|
const { org, env, db, logger } = ctx;
|
||||||
|
const getCusTrialsUsed = () => {
|
||||||
|
if (expand.includes(CusExpand.TrialsUsed)) {
|
||||||
|
return fullCus.trials_used;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getApiCusEntities = () => {
|
||||||
|
if (expand.includes(CusExpand.Entities)) {
|
||||||
|
return fullCus.entities.map((e) => ApiBaseEntitySchema.parse(e));
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const invoices = expand.includes(CusExpand.Invoices)
|
||||||
|
? invoicesToResponse({
|
||||||
|
invoices: fullCus.invoices || [],
|
||||||
|
logger,
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const [rewards, upcomingInvoice, referrals, paymentMethod] =
|
||||||
|
await Promise.all([
|
||||||
|
getCusRewards({
|
||||||
|
org,
|
||||||
|
env,
|
||||||
|
fullCus,
|
||||||
|
subIds: fullCus.customer_products.flatMap(
|
||||||
|
(cp: FullCusProduct) => cp.subscription_ids || [],
|
||||||
|
),
|
||||||
|
expand,
|
||||||
|
}),
|
||||||
|
getCusUpcomingInvoice({
|
||||||
|
db,
|
||||||
|
org,
|
||||||
|
env,
|
||||||
|
fullCus,
|
||||||
|
expand,
|
||||||
|
}),
|
||||||
|
getCusReferrals({
|
||||||
|
db,
|
||||||
|
fullCus,
|
||||||
|
expand,
|
||||||
|
}),
|
||||||
|
getCusPaymentMethodRes({
|
||||||
|
org,
|
||||||
|
env,
|
||||||
|
fullCus,
|
||||||
|
expand,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
trials_used: getCusTrialsUsed(),
|
||||||
|
entities: getApiCusEntities(),
|
||||||
|
rewards,
|
||||||
|
upcoming_invoice: upcomingInvoice,
|
||||||
|
referrals,
|
||||||
|
payment_method: paymentMethod,
|
||||||
|
invoices,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
APICusProductSchema,
|
ApiCusProductSchema,
|
||||||
ApiVersion,
|
ApiVersion,
|
||||||
type ApiVersionClass,
|
type ApiVersionClass,
|
||||||
CusProductStatus,
|
CusProductStatus,
|
||||||
@@ -149,7 +149,7 @@ export const getCusProductResponse = async ({
|
|||||||
options: cusProduct.options,
|
options: cusProduct.options,
|
||||||
});
|
});
|
||||||
|
|
||||||
return APICusProductSchema.parse({
|
return ApiCusProductSchema.parse({
|
||||||
id: fullProduct.id,
|
id: fullProduct.id,
|
||||||
name: fullProduct.name,
|
name: fullProduct.name,
|
||||||
group: fullProduct.group || null,
|
group: fullProduct.group || null,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
ACTIVE_STATUSES,
|
ACTIVE_STATUSES,
|
||||||
type APICusProduct,
|
type ApiCusProduct,
|
||||||
ApiVersion,
|
ApiVersion,
|
||||||
type ApiVersionClass,
|
type ApiVersionClass,
|
||||||
type CusProductStatus,
|
type CusProductStatus,
|
||||||
@@ -13,9 +13,9 @@ import { getCusProductResponse } from "./getCusProductResponse.js";
|
|||||||
const mergeCusProductResponses = ({
|
const mergeCusProductResponses = ({
|
||||||
cusProductResponses,
|
cusProductResponses,
|
||||||
}: {
|
}: {
|
||||||
cusProductResponses: APICusProduct[];
|
cusProductResponses: ApiCusProduct[];
|
||||||
}) => {
|
}) => {
|
||||||
const getProductKey = (product: APICusProduct) => {
|
const getProductKey = (product: ApiCusProduct) => {
|
||||||
const status = ACTIVE_STATUSES.includes(product.status as CusProductStatus)
|
const status = ACTIVE_STATUSES.includes(product.status as CusProductStatus)
|
||||||
? "active"
|
? "active"
|
||||||
: product.status;
|
: product.status;
|
||||||
@@ -84,10 +84,10 @@ export const processFullCusProducts = async ({
|
|||||||
|
|
||||||
if (apiVersion.gte(ApiVersion.V1_1)) {
|
if (apiVersion.gte(ApiVersion.V1_1)) {
|
||||||
main = mergeCusProductResponses({
|
main = mergeCusProductResponses({
|
||||||
cusProductResponses: main as APICusProduct[],
|
cusProductResponses: main as ApiCusProduct[],
|
||||||
});
|
});
|
||||||
addOns = mergeCusProductResponses({
|
addOns = mergeCusProductResponses({
|
||||||
cusProductResponses: addOns as APICusProduct[],
|
cusProductResponses: addOns as ApiCusProduct[],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
APICusReferralSchema,
|
ApiCusReferralSchema,
|
||||||
CusExpand,
|
CusExpand,
|
||||||
type FullCustomer,
|
type FullCustomer,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
@@ -28,7 +28,7 @@ export const getCusReferrals = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return referred.map((r) =>
|
return referred.map((r) =>
|
||||||
APICusReferralSchema.parse({
|
ApiCusReferralSchema.parse({
|
||||||
program_id: r.reward_program?.id,
|
program_id: r.reward_program?.id,
|
||||||
customer: {
|
customer: {
|
||||||
id: r.customer.id,
|
id: r.customer.id,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { ApiCusUpcomingInvoice } from "@autumn/shared";
|
||||||
import {
|
import {
|
||||||
type AppEnv,
|
type AppEnv,
|
||||||
CusExpand,
|
CusExpand,
|
||||||
@@ -66,7 +67,7 @@ export const getCusUpcomingInvoice = async ({
|
|||||||
|
|
||||||
lines.push({
|
lines.push({
|
||||||
product_id: cusProd?.product.id || null,
|
product_id: cusProd?.product.id || null,
|
||||||
description: line.description,
|
description: line.description || "",
|
||||||
amount: atmnLineAmount,
|
amount: atmnLineAmount,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -95,7 +96,7 @@ export const getCusUpcomingInvoice = async ({
|
|||||||
currency: upcomingInvoice.currency,
|
currency: upcomingInvoice.currency,
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = {
|
const res: ApiCusUpcomingInvoice = {
|
||||||
lines,
|
lines,
|
||||||
discounts,
|
discounts,
|
||||||
subtotal: atmnSubtotal,
|
subtotal: atmnSubtotal,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
type APIInvoice,
|
type ApiInvoice,
|
||||||
CusExpand,
|
CusExpand,
|
||||||
type Customer,
|
type Customer,
|
||||||
type CustomerData,
|
type CustomerData,
|
||||||
@@ -84,7 +84,7 @@ export const getCusInvoices = async ({
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
withItems?: boolean;
|
withItems?: boolean;
|
||||||
features?: Feature[];
|
features?: Feature[];
|
||||||
}): Promise<APIInvoice[]> => {
|
}): Promise<ApiInvoice[]> => {
|
||||||
const finalInvoices = notNullish(invoices)
|
const finalInvoices = notNullish(invoices)
|
||||||
? invoices
|
? invoices
|
||||||
: await InvoiceService.list({
|
: await InvoiceService.list({
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
|
ApiBaseEntitySchema,
|
||||||
ApiCusFeatureV2Schema,
|
ApiCusFeatureV2Schema,
|
||||||
ApiCustomerSchema,
|
ApiCustomerSchema,
|
||||||
|
ApiCustomerV2Schema,
|
||||||
ApiVersion,
|
ApiVersion,
|
||||||
type ApiVersionClass,
|
type ApiVersionClass,
|
||||||
type AppEnv,
|
type AppEnv,
|
||||||
@@ -9,7 +11,6 @@ import {
|
|||||||
CustomerResponseSchema,
|
CustomerResponseSchema,
|
||||||
cusProductsToCusEnts,
|
cusProductsToCusEnts,
|
||||||
cusProductsToCusPrices,
|
cusProductsToCusPrices,
|
||||||
EntityResponseSchema,
|
|
||||||
type Feature,
|
type Feature,
|
||||||
FeatureType,
|
FeatureType,
|
||||||
type FullCusProduct,
|
type FullCusProduct,
|
||||||
@@ -135,8 +136,12 @@ export const getCustomerDetails = async ({
|
|||||||
expand,
|
expand,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const apiCustomerSchema = apiVersion.gte(ApiVersion.V1_2)
|
||||||
|
? ApiCustomerSchema
|
||||||
|
: ApiCustomerV2Schema;
|
||||||
|
|
||||||
const cusResponse = {
|
const cusResponse = {
|
||||||
...ApiCustomerSchema.parse({
|
...apiCustomerSchema.parse({
|
||||||
...customer,
|
...customer,
|
||||||
stripe_id: customer.processor?.id,
|
stripe_id: customer.processor?.id,
|
||||||
features: entList,
|
features: entList,
|
||||||
@@ -156,7 +161,7 @@ export const getCustomerDetails = async ({
|
|||||||
metadata: customer.metadata,
|
metadata: customer.metadata,
|
||||||
entities: expand.includes(CusExpand.Entities)
|
entities: expand.includes(CusExpand.Entities)
|
||||||
? customer.entities.map((e) =>
|
? customer.entities.map((e) =>
|
||||||
EntityResponseSchema.parse({
|
ApiBaseEntitySchema.parse({
|
||||||
id: e.id,
|
id: e.id,
|
||||||
name: e.name,
|
name: e.name,
|
||||||
customer_id: customer.id,
|
customer_id: customer.id,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const getOrCreateCustomer = async ({
|
|||||||
withCache = false,
|
withCache = false,
|
||||||
}: {
|
}: {
|
||||||
req: ExtendedRequest;
|
req: ExtendedRequest;
|
||||||
customerId: string;
|
customerId: string | null;
|
||||||
customerData?: CustomerData;
|
customerData?: CustomerData;
|
||||||
inStatuses?: CusProductStatus[];
|
inStatuses?: CusProductStatus[];
|
||||||
skipGet?: boolean;
|
skipGet?: boolean;
|
||||||
|
|||||||
@@ -1,34 +1,15 @@
|
|||||||
import { ApiVersion, CusExpand, CusProductStatus } from "@autumn/shared";
|
import { GetCustomerQuerySchema } from "@autumn/shared";
|
||||||
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
|
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
|
||||||
import { getCusWithCache } from "../cusCache/getCusWithCache.js";
|
import { getCusWithCache } from "../cusCache/getCusWithCache.js";
|
||||||
import { getApiCustomer } from "../cusUtils/apiCusUtils/getApiCustomer.js";
|
import { getApiCustomer } from "../cusUtils/apiCusUtils/getApiCustomer.js";
|
||||||
import { parseCusExpand } from "../cusUtils/cusUtils.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /customers/:customer_id (V2 with versioning system)
|
|
||||||
*
|
|
||||||
* This is the NEW implementation using the versioning system.
|
|
||||||
* DO NOT touch the old handleGetCustomer.ts until this is validated.
|
|
||||||
*
|
|
||||||
* Key differences:
|
|
||||||
* 1. Each resource (customer/product/feature) handles its own versioning
|
|
||||||
* 2. getApiCustomer/getApiCusProduct/getApiCusFeature apply version changes
|
|
||||||
* 3. No version branching in handler logic
|
|
||||||
* 4. Side effects handled explicitly (expand invoices for V1_0)
|
|
||||||
*/
|
|
||||||
export const handleGetCustomerV2 = createRoute({
|
export const handleGetCustomerV2 = createRoute({
|
||||||
|
query: GetCustomerQuerySchema,
|
||||||
handler: async (c) => {
|
handler: async (c) => {
|
||||||
const ctx = c.get("ctx");
|
const ctx = c.get("ctx");
|
||||||
const customerId = c.req.param("customer_id");
|
const customerId = c.req.param("customer_id");
|
||||||
const { env, db, logger, org, features } = ctx;
|
const { env, db, logger, org } = ctx;
|
||||||
const { expand } = c.req.query();
|
const { expand = [] } = c.req.valid("query");
|
||||||
|
|
||||||
const expandArray = parseCusExpand(expand);
|
|
||||||
|
|
||||||
// Side effect: V1_0 always expands invoices
|
|
||||||
if (ctx.apiVersion.lt(ApiVersion.V1_1)) {
|
|
||||||
expandArray.push(CusExpand.Invoices);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info(`[V2] Getting customer ${customerId} for org ${org.slug}`);
|
logger.info(`[V2] Getting customer ${customerId} for org ${org.slug}`);
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
@@ -38,91 +19,19 @@ export const handleGetCustomerV2 = createRoute({
|
|||||||
idOrInternalId: customerId,
|
idOrInternalId: customerId,
|
||||||
org,
|
org,
|
||||||
env,
|
env,
|
||||||
expand: expandArray,
|
expand,
|
||||||
logger,
|
logger,
|
||||||
allowNotFound: false,
|
allowNotFound: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(`[V2] Get customer took ${Date.now() - startTime}ms`);
|
logger.info(`[V2] Get customer took ${Date.now() - startTime}ms`);
|
||||||
|
|
||||||
// Get feature balances
|
|
||||||
const inStatuses = org.config.include_past_due
|
|
||||||
? [CusProductStatus.Active, CusProductStatus.PastDue]
|
|
||||||
: [CusProductStatus.Active];
|
|
||||||
|
|
||||||
const customer = await getApiCustomer({
|
const customer = await getApiCustomer({
|
||||||
ctx,
|
ctx,
|
||||||
fullCus: fullCus,
|
fullCus: fullCus,
|
||||||
|
expand,
|
||||||
});
|
});
|
||||||
|
|
||||||
return c.json(customer);
|
return c.json(customer);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// const balances = await getCusBalances({
|
|
||||||
// cusEntsWithCusProduct: cusEnts,
|
|
||||||
// cusPrices: cusProductsToCusPrices({
|
|
||||||
// cusProducts: customer.customer_products,
|
|
||||||
// inStatuses,
|
|
||||||
// }),
|
|
||||||
// org,
|
|
||||||
// apiVersion: ctx.apiVersion.semver as any, // TODO: fix type
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // Fetch optional expanded fields
|
|
||||||
// const subIds = customer.customer_products.flatMap(
|
|
||||||
// (cp: any) => cp.subscription_ids || [],
|
|
||||||
// );
|
|
||||||
|
|
||||||
// const rewards = await getCusRewards({
|
|
||||||
// org,
|
|
||||||
// env,
|
|
||||||
// fullCus: customer,
|
|
||||||
// subIds,
|
|
||||||
// expand: expandArray,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const upcomingInvoice = await getCusUpcomingInvoice({
|
|
||||||
// db,
|
|
||||||
// org,
|
|
||||||
// env,
|
|
||||||
// fullCus: customer,
|
|
||||||
// expand: expandArray,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const referrals = await getCusReferrals({
|
|
||||||
// db,
|
|
||||||
// fullCus: customer,
|
|
||||||
// expand: expandArray,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const paymentMethod = await getCusPaymentMethodRes({
|
|
||||||
// org,
|
|
||||||
// env,
|
|
||||||
// fullCus: customer,
|
|
||||||
// expand: expandArray,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const invoices = expandArray.includes(CusExpand.Invoices)
|
|
||||||
// ? invoicesToResponse({
|
|
||||||
// invoices: customer.invoices || [],
|
|
||||||
// logger,
|
|
||||||
// })
|
|
||||||
// : undefined;
|
|
||||||
|
|
||||||
// const entities = expandArray.includes(CusExpand.Entities)
|
|
||||||
// ? customer.entities.map((e: any) => ({
|
|
||||||
// id: e.id,
|
|
||||||
// name: e.name,
|
|
||||||
// customer_id: customer.id,
|
|
||||||
// feature_id: e.feature_id,
|
|
||||||
// created_at: e.created_at,
|
|
||||||
// env: customer.env,
|
|
||||||
// }))
|
|
||||||
// : undefined;
|
|
||||||
|
|
||||||
// const trialsUsed = expandArray.includes(CusExpand.TrialsUsed)
|
|
||||||
// ? customer.trials_used
|
|
||||||
// : undefined;
|
|
||||||
|
|
||||||
// const { with_autumn_id } = c.req.query();
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import {
|
||||||
|
CreateCustomerParamsSchema,
|
||||||
|
CreateCustomerQuerySchema,
|
||||||
|
} from "@autumn/shared";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
|
||||||
|
import type { ExtendedRequest } from "@/utils/models/Request.js";
|
||||||
|
import { getApiCustomer } from "../cusUtils/apiCusUtils/getApiCustomer.js";
|
||||||
|
import { getOrCreateCustomer } from "../cusUtils/getOrCreateCustomer.js";
|
||||||
|
|
||||||
|
export const handlePostCustomer = createRoute({
|
||||||
|
query: CreateCustomerQuerySchema.extend({
|
||||||
|
with_autumn_id: z.boolean().optional(),
|
||||||
|
}),
|
||||||
|
|
||||||
|
body: CreateCustomerParamsSchema,
|
||||||
|
|
||||||
|
handler: async (c) => {
|
||||||
|
const ctx = c.get("ctx");
|
||||||
|
|
||||||
|
const { expand = [], with_autumn_id = false } = c.req.valid("query");
|
||||||
|
const createCusParams = c.req.valid("json");
|
||||||
|
|
||||||
|
const fullCus = await getOrCreateCustomer({
|
||||||
|
req: ctx as ExtendedRequest,
|
||||||
|
customerId: createCusParams.id,
|
||||||
|
customerData: createCusParams,
|
||||||
|
expand,
|
||||||
|
entityId: createCusParams.entity_id,
|
||||||
|
entityData: createCusParams.entity_data,
|
||||||
|
withCache: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const customer = await getApiCustomer({
|
||||||
|
ctx,
|
||||||
|
fullCus: fullCus,
|
||||||
|
expand,
|
||||||
|
withAutumnId: with_autumn_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return c.json(customer);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -19,12 +19,12 @@ export const toApiFeature = ({ feature }: { feature: Feature }) => {
|
|||||||
// return FeatureResponseSchema.parse(feature);
|
// return FeatureResponseSchema.parse(feature);
|
||||||
// 1. Get feature type
|
// 1. Get feature type
|
||||||
let featureType = feature.type;
|
let featureType = feature.type;
|
||||||
if (feature.type == FeatureType.Metered) {
|
if (feature.type === FeatureType.Metered) {
|
||||||
featureType = feature.config.usage_type;
|
featureType = feature.config.usage_type;
|
||||||
}
|
}
|
||||||
|
|
||||||
let creditSchema;
|
let creditSchema;
|
||||||
if (feature.type == FeatureType.CreditSystem) {
|
if (feature.type === FeatureType.CreditSystem) {
|
||||||
creditSchema = feature.config.schema.map((s: CreditSchemaItem) => ({
|
creditSchema = feature.config.schema.map((s: CreditSchemaItem) => ({
|
||||||
metered_feature_id: s.metered_feature_id,
|
metered_feature_id: s.metered_feature_id,
|
||||||
credit_cost: s.credit_amount,
|
credit_cost: s.credit_amount,
|
||||||
@@ -54,8 +54,8 @@ export const fromApiFeature = ({
|
|||||||
env: AppEnv;
|
env: AppEnv;
|
||||||
}) => {
|
}) => {
|
||||||
const isMetered =
|
const isMetered =
|
||||||
apiFeature.type == ApiFeatureType.SingleUsage ||
|
apiFeature.type === ApiFeatureType.SingleUsage ||
|
||||||
apiFeature.type == ApiFeatureType.ContinuousUse;
|
apiFeature.type === ApiFeatureType.ContinuousUse;
|
||||||
|
|
||||||
const featureType: FeatureType = isMetered
|
const featureType: FeatureType = isMetered
|
||||||
? FeatureType.Metered
|
? FeatureType.Metered
|
||||||
@@ -71,8 +71,8 @@ export const fromApiFeature = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (featureType == FeatureType.CreditSystem) {
|
if (featureType === FeatureType.CreditSystem) {
|
||||||
if (!apiFeature.credit_schema || apiFeature.credit_schema.length == 0) {
|
if (!apiFeature.credit_schema || apiFeature.credit_schema.length === 0) {
|
||||||
throw new RecaseError({
|
throw new RecaseError({
|
||||||
message: "Credit system schema is required",
|
message: "Credit system schema is required",
|
||||||
code: "CREDIT_SYSTEM_SCHEMA_REQUIRED",
|
code: "CREDIT_SYSTEM_SCHEMA_REQUIRED",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
type APIInvoice,
|
type ApiInvoice,
|
||||||
APIInvoiceItemSchema,
|
ApiInvoiceItemSchema,
|
||||||
type Customer,
|
type Customer,
|
||||||
type Feature,
|
type Feature,
|
||||||
type Invoice,
|
type Invoice,
|
||||||
@@ -41,7 +41,7 @@ export const processInvoice = ({
|
|||||||
(f) => f.internal_id === i.internal_feature_id,
|
(f) => f.internal_id === i.internal_feature_id,
|
||||||
);
|
);
|
||||||
|
|
||||||
return APIInvoiceItemSchema.parse({
|
return ApiInvoiceItemSchema.parse({
|
||||||
description: i.description,
|
description: i.description,
|
||||||
period_start: i.period_start,
|
period_start: i.period_start,
|
||||||
period_end: i.period_end,
|
period_end: i.period_end,
|
||||||
@@ -50,7 +50,7 @@ export const processInvoice = ({
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
: undefined,
|
: undefined,
|
||||||
} as APIInvoice;
|
} as ApiInvoice;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class InvoiceService {
|
export class InvoiceService {
|
||||||
|
|||||||
@@ -74,13 +74,13 @@ export const handleMigrate = async (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fromProduct.is_add_on || toProduct.is_add_on) {
|
// if (fromProduct.is_add_on || toProduct.is_add_on) {
|
||||||
throw new RecaseError({
|
// throw new RecaseError({
|
||||||
message: `Cannot migrate customers for add on products`,
|
// message: `Cannot migrate customers for add on products`,
|
||||||
code: ErrCode.InvalidRequest,
|
// code: ErrCode.InvalidRequest,
|
||||||
statusCode: 400,
|
// statusCode: 400,
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
for (const price of toProduct.prices) {
|
for (const price of toProduct.prices) {
|
||||||
const billingType = getBillingType(price.config);
|
const billingType = getBillingType(price.config);
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
import {
|
import type {
|
||||||
MigrationJob,
|
|
||||||
Customer,
|
|
||||||
Organization,
|
|
||||||
AppEnv,
|
AppEnv,
|
||||||
FullProduct,
|
|
||||||
FullCusProduct,
|
|
||||||
Feature,
|
Feature,
|
||||||
|
FullCusProduct,
|
||||||
|
FullProduct,
|
||||||
|
MigrationJob,
|
||||||
|
Organization,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
|
|
||||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||||
|
|
||||||
import { CusService } from "@/internal/customers/CusService.js";
|
|
||||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
|
||||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||||
|
import { CusService } from "@/internal/customers/CusService.js";
|
||||||
|
import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js";
|
||||||
|
import type { ExtendedRequest } from "@/utils/models/Request.js";
|
||||||
import { migrationToAttachParams } from "../migrationUtils/migrationToAttachParams.js";
|
import { migrationToAttachParams } from "../migrationUtils/migrationToAttachParams.js";
|
||||||
import { runMigrationAttach } from "../migrationUtils/runMigrationAttach.js";
|
import { runMigrationAttach } from "../migrationUtils/runMigrationAttach.js";
|
||||||
import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js";
|
|
||||||
|
|
||||||
export const migrateCustomer = async ({
|
export const migrateCustomer = async ({
|
||||||
db,
|
db,
|
||||||
@@ -42,7 +40,7 @@ export const migrateCustomer = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
const stripeCli = createStripeCli({ org, env });
|
const stripeCli = createStripeCli({ org, env });
|
||||||
let fullCus = await CusService.getFull({
|
const fullCus = await CusService.getFull({
|
||||||
db,
|
db,
|
||||||
idOrInternalId: customerId,
|
idOrInternalId: customerId,
|
||||||
orgId,
|
orgId,
|
||||||
@@ -51,7 +49,7 @@ export const migrateCustomer = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 1. Build req object
|
// 1. Build req object
|
||||||
let req = {
|
const req = {
|
||||||
db,
|
db,
|
||||||
orgId,
|
orgId,
|
||||||
env,
|
env,
|
||||||
@@ -63,7 +61,8 @@ export const migrateCustomer = async ({
|
|||||||
|
|
||||||
const cusProducts = fullCus.customer_products;
|
const cusProducts = fullCus.customer_products;
|
||||||
const filteredCusProducts = cusProducts.filter(
|
const filteredCusProducts = cusProducts.filter(
|
||||||
(cp: FullCusProduct) => cp.product.internal_id == fromProduct.internal_id,
|
(cp: FullCusProduct) =>
|
||||||
|
cp.product.internal_id === fromProduct.internal_id,
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const cusProduct of filteredCusProducts) {
|
for (const cusProduct of filteredCusProducts) {
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Customer,
|
type Customer,
|
||||||
Feature,
|
type Feature,
|
||||||
FullProduct,
|
type FullProduct,
|
||||||
MigrationJob,
|
type MigrationJob,
|
||||||
MigrationJobStep,
|
MigrationJobStep,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import { MigrationService } from "../MigrationService.js";
|
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
|
||||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
|
||||||
import { migrateCustomer } from "./migrateCustomer.js";
|
|
||||||
import { sendMigrationEmail } from "../../emails/sendMigrationEmail.js";
|
|
||||||
import { createStripePriceIFNotExist } from "@/external/stripe/createStripePrice/createStripePrice.js";
|
import { createStripePriceIFNotExist } from "@/external/stripe/createStripePrice/createStripePrice.js";
|
||||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||||
|
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||||
|
import { MigrationService } from "../MigrationService.js";
|
||||||
|
import { migrateCustomer } from "./migrateCustomer.js";
|
||||||
|
|
||||||
export const migrateCustomers = async ({
|
export const migrateCustomers = async ({
|
||||||
db,
|
db,
|
||||||
@@ -41,17 +42,17 @@ export const migrateCustomers = async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
let batchCount = 0;
|
let batchCount = 0;
|
||||||
let { org_id: orgId, env } = migrationJob;
|
const { org_id: orgId, env } = migrationJob;
|
||||||
|
|
||||||
let org = await OrgService.get({
|
const org = await OrgService.get({
|
||||||
db,
|
db,
|
||||||
orgId,
|
orgId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create stripe prices if they don't exist
|
// Create stripe prices if they don't exist
|
||||||
let stripeCli = createStripeCli({ org, env });
|
const stripeCli = createStripeCli({ org, env });
|
||||||
let batchCreate = [];
|
const batchCreate = [];
|
||||||
for (let price of toProduct.prices) {
|
for (const price of toProduct.prices) {
|
||||||
batchCreate.push(
|
batchCreate.push(
|
||||||
createStripePriceIFNotExist({
|
createStripePriceIFNotExist({
|
||||||
db,
|
db,
|
||||||
@@ -67,12 +68,12 @@ export const migrateCustomers = async ({
|
|||||||
|
|
||||||
await Promise.all(batchCreate);
|
await Promise.all(batchCreate);
|
||||||
|
|
||||||
let batchSize = 5;
|
const batchSize = 5;
|
||||||
|
|
||||||
for (let i = 0; i < customers.length; i += batchSize) {
|
for (let i = 0; i < customers.length; i += batchSize) {
|
||||||
let batchCustomers = customers.slice(i, i + batchSize);
|
const batchCustomers = customers.slice(i, i + batchSize);
|
||||||
let batchPromises = [];
|
const batchPromises = [];
|
||||||
for (let customer of batchCustomers) {
|
for (const customer of batchCustomers) {
|
||||||
if (!customer.id) continue;
|
if (!customer.id) continue;
|
||||||
batchPromises.push(
|
batchPromises.push(
|
||||||
migrateCustomer({
|
migrateCustomer({
|
||||||
@@ -90,9 +91,9 @@ export const migrateCustomers = async ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let results = await Promise.all(batchPromises);
|
const results = await Promise.all(batchPromises);
|
||||||
let numPassed = results.filter((r) => r).length;
|
const numPassed = results.filter((r) => r).length;
|
||||||
let numFailed = results.filter((r) => !r).length;
|
const numFailed = results.filter((r) => !r).length;
|
||||||
logger.info(
|
logger.info(
|
||||||
`Job: ${migrationJob.id} - Migrated ${i + batchCustomers.length}/${
|
`Job: ${migrationJob.id} - Migrated ${i + batchCustomers.length}/${
|
||||||
customers.length
|
customers.length
|
||||||
@@ -100,14 +101,14 @@ export const migrateCustomers = async ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Get current number of customers migrated
|
// Get current number of customers migrated
|
||||||
let curMigrationJob = await MigrationService.getJob({
|
const curMigrationJob = await MigrationService.getJob({
|
||||||
db,
|
db,
|
||||||
id: migrationJob.id,
|
id: migrationJob.id,
|
||||||
});
|
});
|
||||||
let curSucceeded =
|
const curSucceeded =
|
||||||
curMigrationJob.step_details[MigrationJobStep.MigrateCustomers]
|
curMigrationJob.step_details[MigrationJobStep.MigrateCustomers]
|
||||||
?.succeeded || 0;
|
?.succeeded || 0;
|
||||||
let curFailed =
|
const curFailed =
|
||||||
curMigrationJob.step_details[MigrationJobStep.MigrateCustomers]?.failed ||
|
curMigrationJob.step_details[MigrationJobStep.MigrateCustomers]?.failed ||
|
||||||
0;
|
0;
|
||||||
|
|
||||||
@@ -134,7 +135,7 @@ export const migrateCustomers = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get number of errors
|
// Get number of errors
|
||||||
let migrationDetails: any = {};
|
const migrationDetails: any = {};
|
||||||
// try {
|
// try {
|
||||||
// let errors = await MigrationService.getErrors({
|
// let errors = await MigrationService.getErrors({
|
||||||
// db,
|
// db,
|
||||||
@@ -152,7 +153,7 @@ export const migrateCustomers = async ({
|
|||||||
// logger.error(error);
|
// logger.error(error);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
let curMigrationJob = await MigrationService.getJob({
|
const curMigrationJob = await MigrationService.getJob({
|
||||||
db,
|
db,
|
||||||
id: migrationJob.id,
|
id: migrationJob.id,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
import {
|
import type { FullCusProduct, FullCustomer, FullProduct } from "@autumn/shared";
|
||||||
type FullCusProduct,
|
|
||||||
type FullCustomer,
|
|
||||||
type FullProduct,
|
|
||||||
} from "@autumn/shared";
|
|
||||||
import type Stripe from "stripe";
|
import type Stripe from "stripe";
|
||||||
import { getStripeCusData } from "@/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getStripeCusData.js";
|
import { getStripeCusData } from "@/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getStripeCusData.js";
|
||||||
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
||||||
@@ -21,7 +17,7 @@ export const migrationToAttachParams = async ({
|
|||||||
cusProduct: FullCusProduct;
|
cusProduct: FullCusProduct;
|
||||||
newProduct: FullProduct;
|
newProduct: FullProduct;
|
||||||
}): Promise<AttachParams> => {
|
}): Promise<AttachParams> => {
|
||||||
const { org, apiVersion } = req;
|
// const { org } = req;
|
||||||
const internalEntityId = cusProduct.internal_entity_id || undefined;
|
const internalEntityId = cusProduct.internal_entity_id || undefined;
|
||||||
|
|
||||||
const { stripeCus, paymentMethod, now } = await getStripeCusData({
|
const { stripeCus, paymentMethod, now } = await getStripeCusData({
|
||||||
@@ -56,7 +52,7 @@ export const migrationToAttachParams = async ({
|
|||||||
cusProducts: customer.customer_products,
|
cusProducts: customer.customer_products,
|
||||||
|
|
||||||
// Others
|
// Others
|
||||||
apiVersion: apiVersion.value,
|
apiVersion: cusProduct.api_semver || undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
return attachParams;
|
return attachParams;
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export const handleUpdateProductV2 = createRoute({
|
|||||||
const itemsExist = notNullish(body.items);
|
const itemsExist = notNullish(body.items);
|
||||||
|
|
||||||
const cusProductExists = cusProductsCurVersion.length > 0;
|
const cusProductExists = cusProductsCurVersion.length > 0;
|
||||||
|
|
||||||
if (cusProductExists && itemsExist) {
|
if (cusProductExists && itemsExist) {
|
||||||
if (disable_version === "true") {
|
if (disable_version === "true") {
|
||||||
throw new RecaseError({
|
throw new RecaseError({
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
BillingType,
|
BillingType,
|
||||||
ErrCode,
|
ErrCode,
|
||||||
FullProduct,
|
type Feature,
|
||||||
Price,
|
type FullProduct,
|
||||||
|
type Price,
|
||||||
PriceType,
|
PriceType,
|
||||||
Product,
|
type Product,
|
||||||
UsagePriceConfig,
|
type UsagePriceConfig,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
|
|
||||||
import { Feature } from "@autumn/shared";
|
|
||||||
import { getBillingType } from "../priceUtils.js";
|
|
||||||
import Stripe from "stripe";
|
|
||||||
import RecaseError from "@/utils/errorUtils.js";
|
import RecaseError from "@/utils/errorUtils.js";
|
||||||
|
import { getBillingType } from "../priceUtils.js";
|
||||||
|
|
||||||
export const findPrepaidPrice = ({
|
export const findPrepaidPrice = ({
|
||||||
prices,
|
prices,
|
||||||
@@ -21,15 +19,16 @@ export const findPrepaidPrice = ({
|
|||||||
internalFeatureId?: string;
|
internalFeatureId?: string;
|
||||||
}) => {
|
}) => {
|
||||||
return prices.find((p: Price) => {
|
return prices.find((p: Price) => {
|
||||||
if (p.config.type != PriceType.Usage) return false;
|
if (p.config.type !== PriceType.Usage) return false;
|
||||||
|
|
||||||
const billingType = getBillingType(p.config);
|
const billingType = getBillingType(p.config);
|
||||||
|
|
||||||
const config = p.config as UsagePriceConfig;
|
const config = p.config as UsagePriceConfig;
|
||||||
|
|
||||||
if (billingType != BillingType.UsageInAdvance) return false;
|
if (billingType !== BillingType.UsageInAdvance) return false;
|
||||||
|
|
||||||
if (internalFeatureId) {
|
if (internalFeatureId) {
|
||||||
return config.internal_feature_id == internalFeatureId;
|
return config.internal_feature_id === internalFeatureId;
|
||||||
} else return true;
|
} else return true;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export const isV4Usage = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
billingType === BillingType.UsageInArrear &&
|
billingType === BillingType.UsageInArrear &&
|
||||||
(cusProduct.api_version === ApiVersion.Beta ||
|
(cusProduct.api_semver === ApiVersion.Beta ||
|
||||||
notNullish(cusProduct.internal_entity_id))
|
notNullish(cusProduct.internal_entity_id))
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
import {
|
import {
|
||||||
AppEnv,
|
type AppEnv,
|
||||||
ErrCode,
|
ErrCode,
|
||||||
Feature,
|
type Feature,
|
||||||
FeatureType,
|
FeatureType,
|
||||||
FeatureUsageType,
|
FeatureUsageType,
|
||||||
ProductItem,
|
type ProductItem,
|
||||||
ProductItemFeatureType,
|
ProductItemFeatureType,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import { isBooleanFeatureItem } from "./productItemUtils/getItemType.js";
|
|
||||||
import { validateFeatureId } from "@/internal/features/featureUtils.js";
|
import { validateFeatureId } from "@/internal/features/featureUtils.js";
|
||||||
import {
|
import {
|
||||||
constructBooleanFeature,
|
constructBooleanFeature,
|
||||||
constructMeteredFeature,
|
constructMeteredFeature,
|
||||||
} from "@/internal/features/utils/constructFeatureUtils.js";
|
} from "@/internal/features/utils/constructFeatureUtils.js";
|
||||||
|
|
||||||
import RecaseError from "@/utils/errorUtils.js";
|
import RecaseError from "@/utils/errorUtils.js";
|
||||||
import { nullish } from "@/utils/genUtils.js";
|
import { nullish } from "@/utils/genUtils.js";
|
||||||
|
import { isBooleanFeatureItem } from "./productItemUtils/getItemType.js";
|
||||||
|
|
||||||
export const createFeaturesFromItems = ({
|
export const createFeaturesFromItems = ({
|
||||||
items,
|
items,
|
||||||
@@ -28,22 +27,23 @@ export const createFeaturesFromItems = ({
|
|||||||
orgId: string;
|
orgId: string;
|
||||||
env: AppEnv;
|
env: AppEnv;
|
||||||
}) => {
|
}) => {
|
||||||
let newFeatures: Feature[] = [];
|
const newFeatures: Feature[] = [];
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (!item.feature_id) {
|
if (!item.feature_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let feature = curFeatures.find((f) => f.id == item.feature_id);
|
const feature = curFeatures.find((f) => f.id === item.feature_id);
|
||||||
|
|
||||||
if (feature) {
|
if (feature) {
|
||||||
if (nullish(item.feature_type)) {
|
if (nullish(item.feature_type)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// 1. Check that feature_type matches
|
// 1. Check that feature_type matches
|
||||||
if (item.feature_type == ProductItemFeatureType.Static) {
|
if (item.feature_type === ProductItemFeatureType.Static) {
|
||||||
let booleanFail =
|
const booleanFail =
|
||||||
item.feature_type == ProductItemFeatureType.Static &&
|
item.feature_type === ProductItemFeatureType.Static &&
|
||||||
feature.type != FeatureType.Boolean;
|
feature.type !== FeatureType.Boolean;
|
||||||
|
|
||||||
if (booleanFail) {
|
if (booleanFail) {
|
||||||
throw new RecaseError({
|
throw new RecaseError({
|
||||||
@@ -53,7 +53,7 @@ export const createFeaturesFromItems = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let usageFail = item.feature_type != feature.config?.usage_type;
|
const usageFail = item.feature_type !== feature.config?.usage_type;
|
||||||
if (usageFail) {
|
if (usageFail) {
|
||||||
throw new RecaseError({
|
throw new RecaseError({
|
||||||
message: `Feature ${item.feature_id} already exists but is not a ${item.feature_type} feature`,
|
message: `Feature ${item.feature_id} already exists but is not a ${item.feature_type} feature`,
|
||||||
@@ -88,7 +88,7 @@ export const createFeaturesFromItems = ({
|
|||||||
orgId,
|
orgId,
|
||||||
env,
|
env,
|
||||||
usageType:
|
usageType:
|
||||||
item.feature_type == ProductItemFeatureType.ContinuousUse
|
item.feature_type === ProductItemFeatureType.ContinuousUse
|
||||||
? FeatureUsageType.Continuous
|
? FeatureUsageType.Continuous
|
||||||
: FeatureUsageType.Single,
|
: FeatureUsageType.Single,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {
|
import type {
|
||||||
AppEnv,
|
AppEnv,
|
||||||
Entitlement,
|
Entitlement,
|
||||||
Feature,
|
Feature,
|
||||||
@@ -6,14 +6,13 @@ import {
|
|||||||
Product,
|
Product,
|
||||||
ProductItem,
|
ProductItem,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import { itemToPriceAndEnt } from "./itemToPriceAndEnt.js";
|
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||||
|
import { FeatureService } from "@/internal/features/FeatureService.js";
|
||||||
import { PriceService } from "@/internal/products/prices/PriceService.js";
|
import { PriceService } from "@/internal/products/prices/PriceService.js";
|
||||||
import { EntitlementService } from "../../entitlements/EntitlementService.js";
|
import { EntitlementService } from "../../entitlements/EntitlementService.js";
|
||||||
import { validateProductItems } from "../validateProductItems.js";
|
import { validateProductItems } from "../validateProductItems.js";
|
||||||
import { FeatureService } from "@/internal/features/FeatureService.js";
|
|
||||||
import { isFeatureItem } from "./getItemType.js";
|
import { isFeatureItem } from "./getItemType.js";
|
||||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
import { itemToPriceAndEnt } from "./itemToPriceAndEnt.js";
|
||||||
import { formatPrice } from "../../prices/priceUtils.js";
|
|
||||||
|
|
||||||
const updateDbPricesAndEnts = async ({
|
const updateDbPricesAndEnts = async ({
|
||||||
db,
|
db,
|
||||||
@@ -62,8 +61,8 @@ const updateDbPricesAndEnts = async ({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Check if any custom prices use this entitlement...
|
// Check if any custom prices use this entitlement...
|
||||||
let deletedEntIds = deletedEnts.map((ent) => ent.id!);
|
const deletedEntIds = deletedEnts.map((ent) => ent.id!);
|
||||||
let customPrices = await PriceService.getCustomInEntIds({
|
const customPrices = await PriceService.getCustomInEntIds({
|
||||||
db,
|
db,
|
||||||
entitlementIds: deletedEntIds,
|
entitlementIds: deletedEntIds,
|
||||||
});
|
});
|
||||||
@@ -75,9 +74,9 @@ const updateDbPricesAndEnts = async ({
|
|||||||
ids: deletedEntIds,
|
ids: deletedEntIds,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
let updateOrDelete: any = [];
|
const updateOrDelete: any = [];
|
||||||
for (const ent of deletedEnts) {
|
for (const ent of deletedEnts) {
|
||||||
let hasCustomPrice = customPrices.some(
|
const hasCustomPrice = customPrices.some(
|
||||||
(price) => price.entitlement_id == ent.id,
|
(price) => price.entitlement_id == ent.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -179,7 +178,7 @@ export const handleNewProductItems = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate product items...
|
// Validate product items...
|
||||||
let { allFeatures, newFeatures } = validateProductItems({
|
const { allFeatures, newFeatures } = validateProductItems({
|
||||||
newItems,
|
newItems,
|
||||||
features,
|
features,
|
||||||
orgId: product.org_id!,
|
orgId: product.org_id!,
|
||||||
@@ -188,14 +187,14 @@ export const handleNewProductItems = async ({
|
|||||||
|
|
||||||
features = allFeatures;
|
features = allFeatures;
|
||||||
|
|
||||||
let newPrices: Price[] = [];
|
const newPrices: Price[] = [];
|
||||||
let newEnts: Entitlement[] = [];
|
const newEnts: Entitlement[] = [];
|
||||||
|
|
||||||
let updatedPrices: Price[] = [];
|
const updatedPrices: Price[] = [];
|
||||||
let updatedEnts: Entitlement[] = [];
|
const updatedEnts: Entitlement[] = [];
|
||||||
|
|
||||||
let deletedPrices: Price[] = curPrices.filter((price) => {
|
const deletedPrices: Price[] = curPrices.filter((price) => {
|
||||||
let item = newItems.find((item) => item.price_id == price.id);
|
const item = newItems.find((item) => item.price_id === price.id);
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -203,20 +202,20 @@ export const handleNewProductItems = async ({
|
|||||||
return isFeatureItem(item);
|
return isFeatureItem(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
let deletedEnts: Entitlement[] = curEnts.filter(
|
const deletedEnts: Entitlement[] = curEnts.filter(
|
||||||
(ent) => !newItems.some((item) => item.entitlement_id == ent.id),
|
(ent) => !newItems.some((item) => item.entitlement_id == ent.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
let samePrices: Price[] = [];
|
const samePrices: Price[] = [];
|
||||||
let sameEnts: Entitlement[] = [];
|
const sameEnts: Entitlement[] = [];
|
||||||
|
|
||||||
for (const item of newItems) {
|
for (const item of newItems) {
|
||||||
let feature = features.find((f) => f.id == item.feature_id);
|
const feature = features.find((f) => f.id == item.feature_id);
|
||||||
let curEnt = curEnts.find((ent) => ent.id == item.entitlement_id);
|
const curEnt = curEnts.find((ent) => ent.id == item.entitlement_id);
|
||||||
let curPrice = curPrices.find((price) => price.id == item.price_id);
|
const curPrice = curPrices.find((price) => price.id == item.price_id);
|
||||||
|
|
||||||
// 2. Update price and entitlement?
|
// 2. Update price and entitlement?
|
||||||
let { newPrice, newEnt, updatedPrice, updatedEnt, samePrice, sameEnt } =
|
const { newPrice, newEnt, updatedPrice, updatedEnt, samePrice, sameEnt } =
|
||||||
itemToPriceAndEnt({
|
itemToPriceAndEnt({
|
||||||
item,
|
item,
|
||||||
orgId: product.org_id!,
|
orgId: product.org_id!,
|
||||||
|
|||||||
@@ -1,29 +1,27 @@
|
|||||||
import RecaseError from "@/utils/errorUtils.js";
|
|
||||||
import {
|
import {
|
||||||
ProductItem,
|
type AppEnv,
|
||||||
EntInterval,
|
EntInterval,
|
||||||
ErrCode,
|
ErrCode,
|
||||||
TierInfinite,
|
type Feature,
|
||||||
ProductItemSchema,
|
|
||||||
Infinite,
|
|
||||||
ProductItemInterval,
|
|
||||||
Feature,
|
|
||||||
FeatureType,
|
FeatureType,
|
||||||
AppEnv,
|
Infinite,
|
||||||
OnIncrease,
|
OnIncrease,
|
||||||
|
type ProductItem,
|
||||||
|
ProductItemInterval,
|
||||||
|
ProductItemSchema,
|
||||||
UsageModel,
|
UsageModel,
|
||||||
RolloverDuration,
|
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import { StatusCodes } from "http-status-codes";
|
import { StatusCodes } from "http-status-codes";
|
||||||
|
import RecaseError from "@/utils/errorUtils.js";
|
||||||
import { notNullish, nullish } from "@/utils/genUtils.js";
|
import { notNullish, nullish } from "@/utils/genUtils.js";
|
||||||
|
import { createFeaturesFromItems } from "./createFeaturesFromItems.js";
|
||||||
|
import { itemToEntInterval } from "./itemIntervalUtils.js";
|
||||||
import {
|
import {
|
||||||
isBooleanFeatureItem,
|
isBooleanFeatureItem,
|
||||||
isFeatureItem,
|
isFeatureItem,
|
||||||
isFeaturePriceItem,
|
isFeaturePriceItem,
|
||||||
isPriceItem,
|
isPriceItem,
|
||||||
} from "./productItemUtils/getItemType.js";
|
} from "./productItemUtils/getItemType.js";
|
||||||
import { itemToEntInterval } from "./itemIntervalUtils.js";
|
|
||||||
import { createFeaturesFromItems } from "./createFeaturesFromItems.js";
|
|
||||||
|
|
||||||
const validateProductItem = ({
|
const validateProductItem = ({
|
||||||
item,
|
item,
|
||||||
@@ -116,9 +114,9 @@ const validateProductItem = ({
|
|||||||
// 5. If it's a price, can't have day, minute or hour interval
|
// 5. If it's a price, can't have day, minute or hour interval
|
||||||
if (isFeaturePriceItem(item) || isPriceItem(item)) {
|
if (isFeaturePriceItem(item) || isPriceItem(item)) {
|
||||||
if (
|
if (
|
||||||
item.interval == ProductItemInterval.Day ||
|
item.interval === ProductItemInterval.Day ||
|
||||||
item.interval == ProductItemInterval.Minute ||
|
item.interval === ProductItemInterval.Minute ||
|
||||||
item.interval == ProductItemInterval.Hour
|
item.interval === ProductItemInterval.Hour
|
||||||
) {
|
) {
|
||||||
throw new RecaseError({
|
throw new RecaseError({
|
||||||
message: `Price can't have day, minute or hour interval`,
|
message: `Price can't have day, minute or hour interval`,
|
||||||
@@ -129,8 +127,8 @@ const validateProductItem = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
item.usage_model == UsageModel.Prepaid &&
|
item.usage_model === UsageModel.Prepaid &&
|
||||||
item.config?.on_increase == OnIncrease.BillImmediately
|
item.config?.on_increase === OnIncrease.BillImmediately
|
||||||
) {
|
) {
|
||||||
throw new RecaseError({
|
throw new RecaseError({
|
||||||
message: `Bill immediately is not supported for prepaid just yet, contact us at hey@useautumn.com if you're interested!`,
|
message: `Bill immediately is not supported for prepaid just yet, contact us at hey@useautumn.com if you're interested!`,
|
||||||
@@ -159,7 +157,7 @@ export const validateProductItems = ({
|
|||||||
orgId: string;
|
orgId: string;
|
||||||
env: AppEnv;
|
env: AppEnv;
|
||||||
}) => {
|
}) => {
|
||||||
let { allFeatures, newFeatures } = createFeaturesFromItems({
|
const { allFeatures, newFeatures } = createFeaturesFromItems({
|
||||||
items: newItems,
|
items: newItems,
|
||||||
curFeatures: features,
|
curFeatures: features,
|
||||||
orgId,
|
orgId,
|
||||||
@@ -178,21 +176,21 @@ export const validateProductItems = ({
|
|||||||
// 1. Check values
|
// 1. Check values
|
||||||
for (let index = 0; index < newItems.length; index++) {
|
for (let index = 0; index < newItems.length; index++) {
|
||||||
validateProductItem({ item: newItems[index], features });
|
validateProductItem({ item: newItems[index], features });
|
||||||
let feature = features.find((f) => f.id == newItems[index].feature_id);
|
const feature = features.find((f) => f.id === newItems[index].feature_id);
|
||||||
|
|
||||||
if (feature && feature.type == FeatureType.Metered) {
|
if (feature && feature.type === FeatureType.Metered) {
|
||||||
newItems[index].feature_type = feature.config?.usage_type;
|
newItems[index].feature_type = feature.config?.usage_type;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let index = 0; index < newItems.length; index++) {
|
for (let index = 0; index < newItems.length; index++) {
|
||||||
let item = newItems[index];
|
const item = newItems[index];
|
||||||
let entInterval = itemToEntInterval(item);
|
const entInterval = itemToEntInterval(item);
|
||||||
const intervalCount = item.interval_count || 1;
|
const intervalCount = item.interval_count || 1;
|
||||||
|
|
||||||
if (isFeaturePriceItem(item) && entInterval == EntInterval.Lifetime) {
|
if (isFeaturePriceItem(item) && entInterval === EntInterval.Lifetime) {
|
||||||
let otherItem = newItems.find((i: any, index2: any) => {
|
const otherItem = newItems.find((i: any, index2: any) => {
|
||||||
return i.feature_id == item.feature_id && index2 != index;
|
return i.feature_id === item.feature_id && index2 !== index;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (otherItem && isFeaturePriceItem(otherItem)) {
|
if (otherItem && isFeaturePriceItem(otherItem)) {
|
||||||
@@ -206,11 +204,11 @@ export const validateProductItems = ({
|
|||||||
|
|
||||||
// Boolean duplicate
|
// Boolean duplicate
|
||||||
if (isBooleanFeatureItem(item)) {
|
if (isBooleanFeatureItem(item)) {
|
||||||
let otherItem = newItems.find((i: any, index2: any) => {
|
const otherItem = newItems.find((i: any, index2: any) => {
|
||||||
return (
|
return (
|
||||||
i.feature_id == item.feature_id &&
|
i.feature_id === item.feature_id &&
|
||||||
index2 != index &&
|
index2 !== index &&
|
||||||
item.entity_feature_id == i.entity_feature_id
|
item.entity_feature_id === i.entity_feature_id
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -223,13 +221,13 @@ export const validateProductItems = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let otherItem = newItems.find((i: any, index2: any) => {
|
const otherItem = newItems.find((i: any, index2: any) => {
|
||||||
return (
|
return (
|
||||||
i.feature_id == item.feature_id &&
|
i.feature_id === item.feature_id &&
|
||||||
index2 != index &&
|
index2 !== index &&
|
||||||
itemToEntInterval(i) == entInterval &&
|
itemToEntInterval(i) === entInterval &&
|
||||||
(i.interval_count || 1) == intervalCount &&
|
(i.interval_count || 1) === intervalCount &&
|
||||||
i.entity_feature_id == item.entity_feature_id
|
i.entity_feature_id === item.entity_feature_id
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -253,7 +251,7 @@ export const validateProductItems = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.usage_model && item.usage_model == otherItem?.usage_model) {
|
if (item.usage_model && item.usage_model === otherItem?.usage_model) {
|
||||||
throw new RecaseError({
|
throw new RecaseError({
|
||||||
message: `You're trying to add the same feature (${item.feature_id}), with the same reset interval. You should either change the reset interval of one of the items, or make one of them a prepaid quantity`,
|
message: `You're trying to add the same feature (${item.feature_id}), with the same reset interval. You should either change the reset interval of one of the items, or make one of them a prepaid quantity`,
|
||||||
code: ErrCode.InvalidInputs,
|
code: ErrCode.InvalidInputs,
|
||||||
@@ -262,8 +260,8 @@ export const validateProductItems = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isPriceItem(item)) {
|
if (isPriceItem(item)) {
|
||||||
let otherItem = newItems.find((i: any, index2: any) => {
|
const otherItem = newItems.find((i: any, index2: any) => {
|
||||||
return i.interval === item.interval && index2 != index;
|
return i.interval === item.interval && index2 !== index;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (otherItem) {
|
if (otherItem) {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const getFeatureDeductions = ({
|
|||||||
const unlimitedExists = cusEnts.some(
|
const unlimitedExists = cusEnts.some(
|
||||||
(cusEnt) =>
|
(cusEnt) =>
|
||||||
cusEnt.entitlement.allowance_type === AllowanceType.Unlimited &&
|
cusEnt.entitlement.allowance_type === AllowanceType.Unlimited &&
|
||||||
cusEnt.entitlement.internal_feature_id == feature.internal_id,
|
cusEnt.entitlement.internal_feature_id === feature.internal_id,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (unlimitedExists) {
|
if (unlimitedExists) {
|
||||||
@@ -76,7 +76,7 @@ const getFeatureDeductions = ({
|
|||||||
deduction = new Decimal(totalBalance).sub(targetBalance).toNumber();
|
deduction = new Decimal(totalBalance).sub(targetBalance).toNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deduction == 0) {
|
if (deduction === 0) {
|
||||||
console.log(` - Skipping feature ${feature.id} -- deduction is 0`);
|
console.log(` - Skipping feature ${feature.id} -- deduction is 0`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -230,7 +230,7 @@ export const updateUsage = async ({
|
|||||||
let { feature, deduction: toDeduct } = obj;
|
let { feature, deduction: toDeduct } = obj;
|
||||||
|
|
||||||
for (const cusEnt of cusEnts) {
|
for (const cusEnt of cusEnts) {
|
||||||
if (cusEnt.entitlement.internal_feature_id != feature.internal_id) {
|
if (cusEnt.entitlement.internal_feature_id !== feature.internal_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ export const updateUsage = async ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (toDeduct == 0) continue;
|
if (toDeduct === 0) continue;
|
||||||
|
|
||||||
toDeduct = await deductAllowanceFromCusEnt({
|
toDeduct = await deductAllowanceFromCusEnt({
|
||||||
toDeduct,
|
toDeduct,
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ export const checkCusSubCorrect = async ({
|
|||||||
|
|
||||||
// Add to schedules
|
// Add to schedules
|
||||||
const scheduleIndexes: number[] = [];
|
const scheduleIndexes: number[] = [];
|
||||||
const apiVersion = cusProduct.api_version || defaultApiVersion;
|
const apiVersion = cusProduct.api_semver || defaultApiVersion;
|
||||||
|
|
||||||
if (isFreeProduct(product.prices)) {
|
if (isFreeProduct(product.prices)) {
|
||||||
assert(
|
assert(
|
||||||
|
|||||||
@@ -289,7 +289,10 @@ export const routeHandler = async <TLoad = undefined>({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.message.includes("not a valid email address")) {
|
if (
|
||||||
|
error.message.includes("not a valid email address") ||
|
||||||
|
error.message.includes("email: Invalid input")
|
||||||
|
) {
|
||||||
req.logtail.warn(`Invalid email address`);
|
req.logtail.warn(`Invalid email address`);
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
message: error.message,
|
message: error.message,
|
||||||
|
|||||||
@@ -43,9 +43,8 @@ const getBreakdown = ({
|
|||||||
intervalCount: number;
|
intervalCount: number;
|
||||||
}) => {
|
}) => {
|
||||||
const wordsFeature = customer.features[TestFeature.Words];
|
const wordsFeature = customer.features[TestFeature.Words];
|
||||||
// @ts-expect-error
|
|
||||||
return wordsFeature.breakdown?.find(
|
return wordsFeature.breakdown?.find(
|
||||||
(b: any) => b.interval_count == intervalCount,
|
(b: any) => b.interval_count === intervalCount,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -109,12 +108,12 @@ describe(`${chalk.yellowBright(`${testCase}: Testing multi interval features wit
|
|||||||
|
|
||||||
expect(
|
expect(
|
||||||
wordsFeature.breakdown?.some(
|
wordsFeature.breakdown?.some(
|
||||||
(b: any) => b.interval_count == 1 && b.interval == "month",
|
(b: any) => b.interval_count === 1 && b.interval === "month",
|
||||||
),
|
),
|
||||||
).to.equal(true);
|
).to.equal(true);
|
||||||
expect(
|
expect(
|
||||||
wordsFeature.breakdown?.some(
|
wordsFeature.breakdown?.some(
|
||||||
(b: any) => b.interval_count == 2 && b.interval == "month",
|
(b: any) => b.interval_count === 2 && b.interval === "month",
|
||||||
),
|
),
|
||||||
).to.equal(true);
|
).to.equal(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js";
|
|
||||||
import {
|
import {
|
||||||
type AppEnv,
|
type AppEnv,
|
||||||
CusExpand,
|
CusExpand,
|
||||||
@@ -15,6 +14,7 @@ import { setupBefore } from "tests/before.js";
|
|||||||
import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js";
|
import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js";
|
||||||
import { advanceTestClock } from "tests/utils/stripeUtils.js";
|
import { advanceTestClock } from "tests/utils/stripeUtils.js";
|
||||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||||
|
import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||||
|
|
||||||
import { CusService } from "@/internal/customers/CusService.js";
|
import { CusService } from "@/internal/customers/CusService.js";
|
||||||
import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js";
|
import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js";
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
|
import type { Customer } from "@autumn/shared";
|
||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import type Stripe from "stripe";
|
||||||
|
import { setupBefore } from "tests/before.js";
|
||||||
import { v1ProductToBasePrice } from "tests/utils/testProductUtils/testProductUtils.js";
|
import { v1ProductToBasePrice } from "tests/utils/testProductUtils/testProductUtils.js";
|
||||||
|
import { calculateMetered1Price } from "@/external/stripe/utils.js";
|
||||||
|
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||||
import { AutumnCli } from "../../cli/AutumnCli.js";
|
import { AutumnCli } from "../../cli/AutumnCli.js";
|
||||||
import { features, products } from "../../global.js";
|
import { features, products } from "../../global.js";
|
||||||
import { compareMainProduct } from "../../utils/compare.js";
|
import { compareMainProduct } from "../../utils/compare.js";
|
||||||
import { advanceClockForInvoice } from "../../utils/stripeUtils.js";
|
|
||||||
import { timeout } from "../../utils/genUtils.js";
|
import { timeout } from "../../utils/genUtils.js";
|
||||||
import { calculateMetered1Price } from "@/external/stripe/utils.js";
|
import { advanceClockForInvoice } from "../../utils/stripeUtils.js";
|
||||||
import chalk from "chalk";
|
|
||||||
import { Customer } from "@autumn/shared";
|
|
||||||
import { setupBefore } from "tests/before.js";
|
|
||||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
|
||||||
import Stripe from "stripe";
|
|
||||||
|
|
||||||
const testCase = "usage1";
|
const testCase = "usage1";
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ describe(`${chalk.yellowBright("usage1: Testing basic usage product")}`, () => {
|
|||||||
testClockId = testClockId_;
|
testClockId = testClockId_;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should attach usage based product", async function () {
|
it("should attach usage based product", async () => {
|
||||||
await AutumnCli.attach({
|
await AutumnCli.attach({
|
||||||
customerId: customerId,
|
customerId: customerId,
|
||||||
productId: products.proWithOverage.id,
|
productId: products.proWithOverage.id,
|
||||||
@@ -53,7 +53,7 @@ describe(`${chalk.yellowBright("usage1: Testing basic usage product")}`, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("usage1: should send metered1 events", async function () {
|
it("usage1: should send metered1 events", async () => {
|
||||||
const batchUpdates = [];
|
const batchUpdates = [];
|
||||||
for (let i = 0; i < NUM_EVENTS; i++) {
|
for (let i = 0; i < NUM_EVENTS; i++) {
|
||||||
batchUpdates.push(
|
batchUpdates.push(
|
||||||
@@ -68,7 +68,7 @@ describe(`${chalk.yellowBright("usage1: Testing basic usage product")}`, () => {
|
|||||||
await timeout(25000);
|
await timeout(25000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should have correct metered1 balance after sending events", async function () {
|
it("should have correct metered1 balance after sending events", async () => {
|
||||||
const res: any = await AutumnCli.entitled(customerId, features.metered1.id);
|
const res: any = await AutumnCli.entitled(customerId, features.metered1.id);
|
||||||
|
|
||||||
expect(res!.allowed).to.be.true;
|
expect(res!.allowed).to.be.true;
|
||||||
@@ -90,7 +90,7 @@ describe(`${chalk.yellowBright("usage1: Testing basic usage product")}`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Check invoice
|
// Check invoice
|
||||||
it("should advance stripe test clock and wait for event", async function () {
|
it("should advance stripe test clock and wait for event", async () => {
|
||||||
await advanceClockForInvoice({
|
await advanceClockForInvoice({
|
||||||
stripeCli,
|
stripeCli,
|
||||||
testClockId,
|
testClockId,
|
||||||
@@ -98,7 +98,7 @@ describe(`${chalk.yellowBright("usage1: Testing basic usage product")}`, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should have correct invoice amount", async function () {
|
it("should have correct invoice amount", async () => {
|
||||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||||
const invoices = cusRes!.invoices;
|
const invoices = cusRes!.invoices;
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,15 @@
|
|||||||
import { assert, expect } from "chai";
|
import { expect } from "chai";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { Decimal } from "decimal.js";
|
||||||
|
import type Stripe from "stripe";
|
||||||
|
import { setupBefore } from "tests/before.js";
|
||||||
|
import { advanceClockForInvoice } from "tests/utils/stripeUtils.js";
|
||||||
|
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||||
import { AutumnCli } from "../../cli/AutumnCli.js";
|
import { AutumnCli } from "../../cli/AutumnCli.js";
|
||||||
import { advanceProducts, creditSystems, features } from "../../global.js";
|
import { advanceProducts, creditSystems, features } from "../../global.js";
|
||||||
|
import { getCreditsUsed } from "../../utils/advancedUsageUtils.js";
|
||||||
import { compareMainProduct } from "../../utils/compare.js";
|
import { compareMainProduct } from "../../utils/compare.js";
|
||||||
import { advanceClockForInvoice } from "../../utils/stripeUtils.js";
|
|
||||||
import { timeout } from "../../utils/genUtils.js";
|
import { timeout } from "../../utils/genUtils.js";
|
||||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
|
||||||
import { Decimal } from "decimal.js";
|
|
||||||
|
|
||||||
import {
|
|
||||||
checkUsageInvoiceAmount,
|
|
||||||
getCreditsUsed,
|
|
||||||
} from "../../utils/advancedUsageUtils.js";
|
|
||||||
|
|
||||||
import chalk from "chalk";
|
|
||||||
import { setupBefore } from "tests/before.js";
|
|
||||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
|
||||||
import Stripe from "stripe";
|
|
||||||
|
|
||||||
// FIRST, REGULAR CHECK GPU STARTER MONTHLY
|
// FIRST, REGULAR CHECK GPU STARTER MONTHLY
|
||||||
|
|
||||||
@@ -47,7 +41,7 @@ describe(`${chalk.yellowBright("usage2: Testing basic usage product")}`, () => {
|
|||||||
stripeCli = this.stripeCli;
|
stripeCli = this.stripeCli;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should attach gpu system starter", async function () {
|
it("should attach gpu system starter", async () => {
|
||||||
await AutumnCli.attach({
|
await AutumnCli.attach({
|
||||||
customerId: customerId,
|
customerId: customerId,
|
||||||
productId: advanceProducts.gpuSystemStarter.id,
|
productId: advanceProducts.gpuSystemStarter.id,
|
||||||
@@ -61,18 +55,18 @@ describe(`${chalk.yellowBright("usage2: Testing basic usage product")}`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Use up events
|
// Use up events
|
||||||
it("should send events and have correct balance (up to 10 DP)", async function () {
|
it("should send events and have correct balance (up to 10 DP)", async () => {
|
||||||
let eventCount = 20;
|
const eventCount = 20;
|
||||||
|
|
||||||
const batchEvents = [];
|
const batchEvents = [];
|
||||||
for (let i = 0; i < eventCount; i++) {
|
for (let i = 0; i < eventCount; i++) {
|
||||||
let randomVal = new Decimal(Math.random().toFixed(PRECISION))
|
const randomVal = new Decimal(Math.random().toFixed(PRECISION))
|
||||||
.mul(CREDIT_MULTIPLIER)
|
.mul(CREDIT_MULTIPLIER)
|
||||||
.mul(Math.random() > 0.2 ? 1 : -1)
|
.mul(Math.random() > 0.2 ? 1 : -1)
|
||||||
.toNumber();
|
.toNumber();
|
||||||
let gpuId = i % 2 == 0 ? features.gpu1.id : features.gpu2.id;
|
const gpuId = i % 2 === 0 ? features.gpu1.id : features.gpu2.id;
|
||||||
|
|
||||||
let creditsUsed = getCreditsUsed(
|
const creditsUsed = getCreditsUsed(
|
||||||
creditSystems.gpuCredits,
|
creditSystems.gpuCredits,
|
||||||
gpuId,
|
gpuId,
|
||||||
randomVal,
|
randomVal,
|
||||||
@@ -101,7 +95,7 @@ describe(`${chalk.yellowBright("usage2: Testing basic usage product")}`, () => {
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
let creditAllowance =
|
const creditAllowance =
|
||||||
advanceProducts.gpuSystemStarter.entitlements.gpuCredits.allowance!;
|
advanceProducts.gpuSystemStarter.entitlements.gpuCredits.allowance!;
|
||||||
|
|
||||||
expect(allowed).to.be.true;
|
expect(allowed).to.be.true;
|
||||||
@@ -113,34 +107,30 @@ describe(`${chalk.yellowBright("usage2: Testing basic usage product")}`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Check invoice.created event
|
// Check invoice.created event
|
||||||
it("should have correct invoice amount / updated meter balance", async function () {
|
it("should have correct invoice amount / updated meter balance", async () => {
|
||||||
await advanceClockForInvoice({
|
await advanceClockForInvoice({
|
||||||
stripeCli,
|
stripeCli,
|
||||||
testClockId,
|
testClockId,
|
||||||
waitForMeterUpdate: ASSERT_INVOICE_AMOUNT,
|
waitForMeterUpdate: ASSERT_INVOICE_AMOUNT,
|
||||||
});
|
});
|
||||||
|
// const res = await AutumnCli.getCustomer(customerId);
|
||||||
const res = await AutumnCli.getCustomer(customerId);
|
// const invoices = res!.invoices;
|
||||||
const invoices = res!.invoices;
|
// if (ASSERT_INVOICE_AMOUNT) {
|
||||||
|
// await checkUsageInvoiceAmount({
|
||||||
if (ASSERT_INVOICE_AMOUNT) {
|
// invoices,
|
||||||
await checkUsageInvoiceAmount({
|
// totalUsage: totalCreditsUsed,
|
||||||
invoices,
|
// product: advanceProducts.gpuSystemStarter,
|
||||||
totalUsage: totalCreditsUsed,
|
// featureId: creditSystems.gpuCredits.id,
|
||||||
product: advanceProducts.gpuSystemStarter,
|
// });
|
||||||
featureId: creditSystems.gpuCredits.id,
|
// } else {
|
||||||
});
|
// const { allowed, balanceObj }: any = await AutumnCli.entitled(
|
||||||
} else {
|
// customerId,
|
||||||
const { allowed, balanceObj }: any = await AutumnCli.entitled(
|
// creditSystems.gpuCredits.id,
|
||||||
customerId,
|
// true,
|
||||||
creditSystems.gpuCredits.id,
|
// );
|
||||||
true,
|
// const allowance =
|
||||||
);
|
// advanceProducts.gpuSystemStarter.entitlements.gpuCredits.allowance!;
|
||||||
|
// assert.equal(balanceObj.balance, allowance);
|
||||||
let allowance =
|
// }
|
||||||
advanceProducts.gpuSystemStarter.entitlements.gpuCredits.allowance!;
|
|
||||||
|
|
||||||
assert.equal(balanceObj.balance, allowance);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import { Customer } from "@autumn/shared";
|
import type { Customer } from "@autumn/shared";
|
||||||
|
import { expect } from "chai";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { compareMainProduct } from "../../utils/compare.js";
|
import type Stripe from "stripe";
|
||||||
|
import { setupBefore } from "tests/before.js";
|
||||||
|
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||||
import { AutumnCli } from "../../cli/AutumnCli.js";
|
import { AutumnCli } from "../../cli/AutumnCli.js";
|
||||||
import { advanceProducts, creditSystems } from "../../global.js";
|
import { advanceProducts, creditSystems } from "../../global.js";
|
||||||
|
|
||||||
import { expect } from "chai";
|
|
||||||
import { advanceClockForInvoice } from "../../utils/stripeUtils.js";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
sendGPUEvents,
|
|
||||||
checkCreditBalance,
|
checkCreditBalance,
|
||||||
checkUsageInvoiceAmount,
|
checkUsageInvoiceAmount,
|
||||||
|
sendGPUEvents,
|
||||||
} from "../../utils/advancedUsageUtils.js";
|
} from "../../utils/advancedUsageUtils.js";
|
||||||
import Stripe from "stripe";
|
import { compareMainProduct } from "../../utils/compare.js";
|
||||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
import { advanceClockForInvoice } from "../../utils/stripeUtils.js";
|
||||||
import { setupBefore } from "tests/before.js";
|
|
||||||
|
|
||||||
// THIRD, TEST GPU PRO ANNUAL
|
// THIRD, TEST GPU PRO ANNUAL
|
||||||
|
|
||||||
@@ -30,7 +28,7 @@ describe(`${chalk.yellowBright("usage4: GPU starter annual")}`, () => {
|
|||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
await setupBefore(this);
|
await setupBefore(this);
|
||||||
let res = await initCustomer({
|
const res = await initCustomer({
|
||||||
customerId,
|
customerId,
|
||||||
org: this.org,
|
org: this.org,
|
||||||
env: this.env,
|
env: this.env,
|
||||||
@@ -44,7 +42,7 @@ describe(`${chalk.yellowBright("usage4: GPU starter annual")}`, () => {
|
|||||||
stripeCli = this.stripeCli;
|
stripeCli = this.stripeCli;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should attach GPU starter annual", async function () {
|
it("should attach GPU starter annual", async () => {
|
||||||
await AutumnCli.attach({
|
await AutumnCli.attach({
|
||||||
customerId: customerId,
|
customerId: customerId,
|
||||||
productId: advanceProducts.gpuStarterAnnual.id,
|
productId: advanceProducts.gpuStarterAnnual.id,
|
||||||
@@ -59,8 +57,8 @@ describe(`${chalk.yellowBright("usage4: GPU starter annual")}`, () => {
|
|||||||
expect(res!.invoices.length).to.equal(1);
|
expect(res!.invoices.length).to.equal(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should send 20 events and have correct balance", async function () {
|
it("should send 20 events and have correct balance", async () => {
|
||||||
let eventCount = 20;
|
const eventCount = 20;
|
||||||
const { creditsUsed } = await sendGPUEvents({
|
const { creditsUsed } = await sendGPUEvents({
|
||||||
customerId,
|
customerId,
|
||||||
eventCount,
|
eventCount,
|
||||||
@@ -76,7 +74,7 @@ describe(`${chalk.yellowBright("usage4: GPU starter annual")}`, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should have invoice after a month and correct balance", async function () {
|
it("should have invoice after a month and correct balance", async () => {
|
||||||
await advanceClockForInvoice({
|
await advanceClockForInvoice({
|
||||||
stripeCli,
|
stripeCli,
|
||||||
testClockId,
|
testClockId,
|
||||||
@@ -86,7 +84,7 @@ describe(`${chalk.yellowBright("usage4: GPU starter annual")}`, () => {
|
|||||||
const res = await AutumnCli.getCustomer(customerId);
|
const res = await AutumnCli.getCustomer(customerId);
|
||||||
const invoices = res!.invoices;
|
const invoices = res!.invoices;
|
||||||
|
|
||||||
let invoiceIndex = invoices.findIndex((invoice: any) =>
|
const invoiceIndex = invoices.findIndex((invoice: any) =>
|
||||||
invoice.product_ids.includes(advanceProducts.gpuStarterAnnual.id),
|
invoice.product_ids.includes(advanceProducts.gpuStarterAnnual.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,30 @@
|
|||||||
import chalk from "chalk";
|
|
||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
import chalk from "chalk";
|
||||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
|
||||||
import { setupBefore } from "tests/before.js";
|
import { setupBefore } from "tests/before.js";
|
||||||
import { AutumnCli } from "tests/cli/AutumnCli.js";
|
import { AutumnCli } from "tests/cli/AutumnCli.js";
|
||||||
import { features, products } from "tests/global.js";
|
import { features, products } from "tests/global.js";
|
||||||
import { compareMainProduct } from "tests/utils/compare.js";
|
import { compareMainProduct } from "tests/utils/compare.js";
|
||||||
import { constructRawProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
|
||||||
import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js";
|
|
||||||
import { createProducts } from "tests/utils/productUtils.js";
|
import { createProducts } from "tests/utils/productUtils.js";
|
||||||
|
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||||
|
import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js";
|
||||||
|
import { constructRawProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||||
|
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||||
|
|
||||||
let monthlyItem = constructPrepaidItem({
|
const monthlyItem = constructPrepaidItem({
|
||||||
featureId: features.metered1.id,
|
featureId: features.metered1.id,
|
||||||
price: 9,
|
price: 9,
|
||||||
billingUnits: 250,
|
billingUnits: 250,
|
||||||
});
|
});
|
||||||
|
|
||||||
let monthly = constructRawProduct({
|
const monthly = constructRawProduct({
|
||||||
id: "basic4_monthly",
|
id: "basic4_monthly",
|
||||||
items: [monthlyItem],
|
items: [monthlyItem],
|
||||||
});
|
});
|
||||||
|
|
||||||
const testCase = "basic4";
|
const testCase = "basic4";
|
||||||
describe(`${chalk.yellowBright("basic4: Testing attach monthly add on")}`, () => {
|
describe(`${chalk.yellowBright("basic4: Testing attach monthly add on")}`, () => {
|
||||||
let customerId = testCase;
|
const customerId = testCase;
|
||||||
let autumn: AutumnInt = new AutumnInt();
|
const autumn: AutumnInt = new AutumnInt();
|
||||||
let db, org, env;
|
let db, org, env;
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
@@ -51,7 +51,7 @@ describe(`${chalk.yellowBright("basic4: Testing attach monthly add on")}`, () =>
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should attach pro", async function () {
|
it("should attach pro", async () => {
|
||||||
await autumn.attach({
|
await autumn.attach({
|
||||||
customer_id: customerId,
|
customer_id: customerId,
|
||||||
product_id: products.pro.id,
|
product_id: products.pro.id,
|
||||||
@@ -66,7 +66,7 @@ describe(`${chalk.yellowBright("basic4: Testing attach monthly add on")}`, () =>
|
|||||||
|
|
||||||
const monthlyQuantity = 500;
|
const monthlyQuantity = 500;
|
||||||
|
|
||||||
it("should attach monthly add on", async function () {
|
it("should attach monthly add on", async () => {
|
||||||
await AutumnCli.attach({
|
await AutumnCli.attach({
|
||||||
customerId: customerId,
|
customerId: customerId,
|
||||||
productId: products.monthlyAddOnMetered1.id,
|
productId: products.monthlyAddOnMetered1.id,
|
||||||
@@ -80,14 +80,15 @@ describe(`${chalk.yellowBright("basic4: Testing attach monthly add on")}`, () =>
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should have correct product & entitlements", async function () {
|
it("should have correct product & entitlements", async () => {
|
||||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||||
|
|
||||||
const proMetered1 = products.pro.entitlements.metered1.allowance;
|
const proMetered1 = products.pro.entitlements.metered1.allowance;
|
||||||
|
|
||||||
const monthlyMetered1Balance = cusRes.entitlements.find(
|
const monthlyMetered1Balance = cusRes.entitlements.find(
|
||||||
(e: any) =>
|
(e: any) =>
|
||||||
e.feature_id === features.metered1.id &&
|
e.feature_id === features.metered1.id &&
|
||||||
e.interval ==
|
e.interval ===
|
||||||
products.monthlyAddOnMetered1.entitlements.metered1.interval,
|
products.monthlyAddOnMetered1.entitlements.metered1.interval,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -104,7 +105,7 @@ describe(`${chalk.yellowBright("basic4: Testing attach monthly add on")}`, () =>
|
|||||||
expect(cusRes.invoices.length).to.equal(2);
|
expect(cusRes.invoices.length).to.equal(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should have correct /check result for metered1", async function () {
|
it("should have correct /check result for metered1", async () => {
|
||||||
const res: any = await AutumnCli.entitled(customerId, features.metered1.id);
|
const res: any = await AutumnCli.entitled(customerId, features.metered1.id);
|
||||||
|
|
||||||
const metered1Balance = res!.balances.find(
|
const metered1Balance = res!.balances.find(
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
|
import { CusProductStatus } from "@autumn/shared";
|
||||||
|
import { expect } from "chai";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { setupBefore } from "tests/before.js";
|
import { setupBefore } from "tests/before.js";
|
||||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
|
||||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
|
||||||
import { AutumnCli } from "tests/cli/AutumnCli.js";
|
import { AutumnCli } from "tests/cli/AutumnCli.js";
|
||||||
import { products } from "tests/global.js";
|
import { products } from "tests/global.js";
|
||||||
import { compareMainProduct } from "tests/utils/compare.js";
|
import { compareMainProduct } from "tests/utils/compare.js";
|
||||||
import { CusProductStatus } from "@autumn/shared";
|
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||||
import { expect } from "chai";
|
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||||
import { timeout } from "@/utils/genUtils.js";
|
|
||||||
|
|
||||||
const testCase = "basic8";
|
const testCase = "basic8";
|
||||||
|
|
||||||
describe(`${chalk.yellowBright("basic8: Testing trial duplicates (same fingerprint)")}`, () => {
|
describe(`${chalk.yellowBright("basic8: Testing trial duplicates (same fingerprint)")}`, () => {
|
||||||
const customerId = testCase;
|
const customerId = testCase;
|
||||||
let customerId2 = testCase + "2";
|
const customerId2 = testCase + "2";
|
||||||
const autumn = new AutumnInt();
|
const autumn = new AutumnInt();
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
@@ -40,7 +39,7 @@ describe(`${chalk.yellowBright("basic8: Testing trial duplicates (same fingerpri
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should attach pro with trial and have correct product & invoice", async function () {
|
it("should attach pro with trial and have correct product & invoice", async () => {
|
||||||
await AutumnCli.attach({
|
await AutumnCli.attach({
|
||||||
customerId: customerId,
|
customerId: customerId,
|
||||||
productId: products.proWithTrial.id,
|
productId: products.proWithTrial.id,
|
||||||
@@ -59,7 +58,7 @@ describe(`${chalk.yellowBright("basic8: Testing trial duplicates (same fingerpri
|
|||||||
expect(invoices[0].total).to.equal(0, "Invoice total should be 0");
|
expect(invoices[0].total).to.equal(0, "Invoice total should be 0");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should attach pro with trial to second customer and have correct product & invoice (pro with trial, full price)", async function () {
|
it("should attach pro with trial to second customer and have correct product & invoice (pro with trial, full price)", async () => {
|
||||||
await autumn.attach({
|
await autumn.attach({
|
||||||
customer_id: customerId2,
|
customer_id: customerId2,
|
||||||
product_id: products.proWithTrial.id,
|
product_id: products.proWithTrial.id,
|
||||||
|
|||||||
@@ -1,35 +1,38 @@
|
|||||||
import { expect } from "chai";
|
import type {
|
||||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
AppEnv,
|
||||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
LimitedItem,
|
||||||
import { AppEnv, LimitedItem, Organization, ProductV2 } from "@autumn/shared";
|
Organization,
|
||||||
|
ProductV2,
|
||||||
|
} from "@autumn/shared";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import Stripe from "stripe";
|
|
||||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
|
||||||
import { setupBefore } from "tests/before.js";
|
|
||||||
import { createProducts } from "tests/utils/productUtils.js";
|
|
||||||
import { addPrefixToProducts } from "../utils.js";
|
|
||||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
|
||||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
|
||||||
import { TestFeature } from "tests/setup/v2Features.js";
|
|
||||||
import { replaceItems } from "../utils.js";
|
|
||||||
import { timeout } from "@/utils/genUtils.js";
|
|
||||||
import { advanceTestClock } from "tests/utils/stripeUtils.js";
|
|
||||||
import { addWeeks } from "date-fns";
|
import { addWeeks } from "date-fns";
|
||||||
|
import type Stripe from "stripe";
|
||||||
|
import { setupBefore } from "tests/before.js";
|
||||||
import { defaultApiVersion } from "tests/constants.js";
|
import { defaultApiVersion } from "tests/constants.js";
|
||||||
import { runMigrationTest } from "./runMigrationTest.js";
|
import { TestFeature } from "tests/setup/v2Features.js";
|
||||||
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
|
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
|
||||||
|
import { createProducts } from "tests/utils/productUtils.js";
|
||||||
|
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||||
|
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||||
|
import { timeout } from "@/utils/genUtils.js";
|
||||||
|
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||||
|
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||||
|
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||||
|
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
|
||||||
|
import { addPrefixToProducts, replaceItems } from "../utils.js";
|
||||||
|
import { runMigrationTest } from "./runMigrationTest.js";
|
||||||
|
|
||||||
let messagesItem = constructFeatureItem({
|
const messagesItem = constructFeatureItem({
|
||||||
featureId: TestFeature.Messages,
|
featureId: TestFeature.Messages,
|
||||||
includedUsage: 500,
|
includedUsage: 500,
|
||||||
}) as LimitedItem;
|
}) as LimitedItem;
|
||||||
|
|
||||||
let wordsItem = constructFeatureItem({
|
const wordsItem = constructFeatureItem({
|
||||||
featureId: TestFeature.Words,
|
featureId: TestFeature.Words,
|
||||||
includedUsage: 100,
|
includedUsage: 100,
|
||||||
}) as LimitedItem;
|
}) as LimitedItem;
|
||||||
|
|
||||||
export let free = constructProduct({
|
export const free = constructProduct({
|
||||||
items: [messagesItem, wordsItem],
|
items: [messagesItem, wordsItem],
|
||||||
type: "free",
|
type: "free",
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
@@ -38,13 +41,13 @@ export let free = constructProduct({
|
|||||||
const testCase = "migrations1";
|
const testCase = "migrations1";
|
||||||
|
|
||||||
describe(`${chalk.yellowBright(`${testCase}: Testing migration for free product`)}`, () => {
|
describe(`${chalk.yellowBright(`${testCase}: Testing migration for free product`)}`, () => {
|
||||||
let customerId = testCase;
|
const customerId = testCase;
|
||||||
let autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
|
const autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
|
||||||
let testClockId: string;
|
let testClockId: string;
|
||||||
let db: DrizzleCli, org: Organization, env: AppEnv;
|
let db: DrizzleCli, org: Organization, env: AppEnv;
|
||||||
let stripeCli: Stripe;
|
let stripeCli: Stripe;
|
||||||
|
|
||||||
let curUnix = new Date().getTime();
|
const curUnix = new Date().getTime();
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
await setupBefore(this);
|
await setupBefore(this);
|
||||||
@@ -81,7 +84,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for free product`
|
|||||||
testClockId = testClockId1!;
|
testClockId = testClockId1!;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should attach free product", async function () {
|
it("should attach free product", async () => {
|
||||||
await attachAndExpectCorrect({
|
await attachAndExpectCorrect({
|
||||||
autumn,
|
autumn,
|
||||||
customerId,
|
customerId,
|
||||||
@@ -95,9 +98,9 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for free product`
|
|||||||
});
|
});
|
||||||
|
|
||||||
let newFree: ProductV2;
|
let newFree: ProductV2;
|
||||||
let increaseMessagesBy = 100;
|
const increaseMessagesBy = 100;
|
||||||
let reduceWordsBy = 50;
|
const reduceWordsBy = 50;
|
||||||
it("should update product to new version", async function () {
|
it("should update product to new version", async () => {
|
||||||
newFree = structuredClone(free);
|
newFree = structuredClone(free);
|
||||||
|
|
||||||
let newItems = replaceItems({
|
let newItems = replaceItems({
|
||||||
@@ -126,9 +129,9 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for free product`
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should attach track usage and get correct balance", async function () {
|
it("should attach track usage and get correct balance", async () => {
|
||||||
let wordsUsage = 25;
|
const wordsUsage = 25;
|
||||||
let messagesUsage = 20;
|
const messagesUsage = 20;
|
||||||
await autumn.track({
|
await autumn.track({
|
||||||
customer_id: customerId,
|
customer_id: customerId,
|
||||||
value: wordsUsage,
|
value: wordsUsage,
|
||||||
@@ -141,6 +144,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for free product`
|
|||||||
feature_id: TestFeature.Messages,
|
feature_id: TestFeature.Messages,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await timeout(2000);
|
||||||
await advanceTestClock({
|
await advanceTestClock({
|
||||||
stripeCli,
|
stripeCli,
|
||||||
testClockId,
|
testClockId,
|
||||||
|
|||||||
@@ -1,35 +1,32 @@
|
|||||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
|
||||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
|
||||||
import {
|
import {
|
||||||
AppEnv,
|
type AppEnv,
|
||||||
BillingInterval,
|
BillingInterval,
|
||||||
Organization,
|
type Organization,
|
||||||
ProductItemInterval,
|
ProductItemInterval,
|
||||||
ProductV2,
|
type ProductV2,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import Stripe from "stripe";
|
|
||||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
|
||||||
import { setupBefore } from "tests/before.js";
|
|
||||||
import { createProducts } from "tests/utils/productUtils.js";
|
|
||||||
import { addPrefixToProducts } from "../utils.js";
|
|
||||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
|
||||||
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
|
||||||
import { TestFeature } from "tests/setup/v2Features.js";
|
|
||||||
import { replaceItems } from "../utils.js";
|
|
||||||
|
|
||||||
import { defaultApiVersion } from "tests/constants.js";
|
|
||||||
import { runMigrationTest } from "./runMigrationTest.js";
|
|
||||||
import { timeout } from "@/utils/genUtils.js";
|
|
||||||
import { advanceTestClock } from "tests/utils/stripeUtils.js";
|
|
||||||
import { addDays } from "date-fns";
|
import { addDays } from "date-fns";
|
||||||
|
import type Stripe from "stripe";
|
||||||
|
import { setupBefore } from "tests/before.js";
|
||||||
|
import { defaultApiVersion } from "tests/constants.js";
|
||||||
|
import { TestFeature } from "tests/setup/v2Features.js";
|
||||||
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
|
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
|
||||||
|
import { createProducts } from "tests/utils/productUtils.js";
|
||||||
|
import { advanceTestClock } from "tests/utils/stripeUtils.js";
|
||||||
|
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||||
|
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||||
|
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
||||||
|
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||||
|
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||||
|
import { addPrefixToProducts, replaceItems } from "../utils.js";
|
||||||
|
import { runMigrationTest } from "./runMigrationTest.js";
|
||||||
|
|
||||||
let wordsItem = constructArrearItem({
|
const wordsItem = constructArrearItem({
|
||||||
featureId: TestFeature.Words,
|
featureId: TestFeature.Words,
|
||||||
});
|
});
|
||||||
|
|
||||||
export let pro = constructProduct({
|
export const pro = constructProduct({
|
||||||
items: [wordsItem],
|
items: [wordsItem],
|
||||||
type: "pro",
|
type: "pro",
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
@@ -39,13 +36,13 @@ export let pro = constructProduct({
|
|||||||
const testCase = "migrations3";
|
const testCase = "migrations3";
|
||||||
|
|
||||||
describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro with trial`)}`, () => {
|
describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro with trial`)}`, () => {
|
||||||
let customerId = testCase;
|
const customerId = testCase;
|
||||||
let autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
|
const autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
|
||||||
let testClockId: string;
|
let testClockId: string;
|
||||||
let db: DrizzleCli, org: Organization, env: AppEnv;
|
let db: DrizzleCli, org: Organization, env: AppEnv;
|
||||||
let stripeCli: Stripe;
|
let stripeCli: Stripe;
|
||||||
|
|
||||||
let curUnix = new Date().getTime();
|
const curUnix = new Date().getTime();
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
await setupBefore(this);
|
await setupBefore(this);
|
||||||
@@ -82,7 +79,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro with tria
|
|||||||
testClockId = testClockId1!;
|
testClockId = testClockId1!;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should attach free product", async function () {
|
it("should attach free product", async () => {
|
||||||
await attachAndExpectCorrect({
|
await attachAndExpectCorrect({
|
||||||
autumn,
|
autumn,
|
||||||
customerId,
|
customerId,
|
||||||
@@ -95,8 +92,8 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro with tria
|
|||||||
});
|
});
|
||||||
|
|
||||||
let newPro: ProductV2;
|
let newPro: ProductV2;
|
||||||
let increaseWordsBy = 1500;
|
const increaseWordsBy = 1500;
|
||||||
it("should update product to new version", async function () {
|
it("should update product to new version", async () => {
|
||||||
newPro = structuredClone(pro);
|
newPro = structuredClone(pro);
|
||||||
|
|
||||||
let newItems = replaceItems({
|
let newItems = replaceItems({
|
||||||
@@ -124,8 +121,8 @@ describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro with tria
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should attach track usage and get correct balance", async function () {
|
it("should attach track usage and get correct balance", async () => {
|
||||||
let wordsUsage = 120000;
|
const wordsUsage = 120000;
|
||||||
await autumn.track({
|
await autumn.track({
|
||||||
customer_id: customerId,
|
customer_id: customerId,
|
||||||
value: wordsUsage,
|
value: wordsUsage,
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
import { LegacyVersion } from "@autumn/shared";
|
import { ApiVersion } from "@autumn/shared";
|
||||||
|
|
||||||
export const defaultApiVersion = LegacyVersion.v1_2;
|
export const defaultApiVersion = ApiVersion.V1_2;
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ describe(`${chalk.yellowBright("multiAttach3: Testing multi attach for trial pro
|
|||||||
await advanceTestClock({
|
await advanceTestClock({
|
||||||
stripeCli,
|
stripeCli,
|
||||||
testClockId,
|
testClockId,
|
||||||
advanceTo: addDays(new Date(), 8).getTime(),
|
advanceTo: addDays(new Date(), 10).getTime(),
|
||||||
waitForSeconds: 30,
|
waitForSeconds: 30,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ describe(`${chalk.yellowBright("mergedDowngrade5: Testing downgrade to free")}`,
|
|||||||
|
|
||||||
for (let index = 0; index < ops.length; index++) {
|
for (let index = 0; index < ops.length; index++) {
|
||||||
const op = ops[index];
|
const op = ops[index];
|
||||||
|
|
||||||
await attachAndExpectCorrect({
|
await attachAndExpectCorrect({
|
||||||
autumn,
|
autumn,
|
||||||
customerId,
|
customerId,
|
||||||
@@ -176,6 +177,7 @@ describe(`${chalk.yellowBright("mergedDowngrade5: Testing downgrade to free")}`,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
|
|
||||||
// it("should advance test clock and have correct premium downgraded for entity 2", async function () {
|
// it("should advance test clock and have correct premium downgraded for entity 2", async function () {
|
||||||
// await advanceToNextInvoice({
|
// await advanceToNextInvoice({
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ const ops = [
|
|||||||
{ product: g2Pro, status: CusProductStatus.Active },
|
{ product: g2Pro, status: CusProductStatus.Active },
|
||||||
{ product: g1Pro, status: CusProductStatus.Scheduled },
|
{ product: g1Pro, status: CusProductStatus.Scheduled },
|
||||||
],
|
],
|
||||||
// otherProducts: [g2Pro],
|
otherProducts: [g2Pro],
|
||||||
|
skipFeatureCheck: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -123,6 +124,9 @@ describe(`${chalk.yellowBright("mergedGroup1: Testing products from diff groups"
|
|||||||
|
|
||||||
it("should attach pro product", async () => {
|
it("should attach pro product", async () => {
|
||||||
for (const op of ops) {
|
for (const op of ops) {
|
||||||
|
// console.log(
|
||||||
|
// `Op: ${op.product.id}, Other Products: ${op.otherProducts?.map((p) => p.id).join(", ")}`,
|
||||||
|
// );
|
||||||
await attachAndExpectCorrect({
|
await attachAndExpectCorrect({
|
||||||
autumn,
|
autumn,
|
||||||
customerId,
|
customerId,
|
||||||
@@ -132,6 +136,7 @@ describe(`${chalk.yellowBright("mergedGroup1: Testing products from diff groups"
|
|||||||
db,
|
db,
|
||||||
org,
|
org,
|
||||||
env,
|
env,
|
||||||
|
skipFeatureCheck: op.skipFeatureCheck,
|
||||||
});
|
});
|
||||||
|
|
||||||
const customer = await autumn.customers.get(customerId);
|
const customer = await autumn.customers.get(customerId);
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ const ops = [
|
|||||||
{ product: g1Pro, status: CusProductStatus.Scheduled },
|
{ product: g1Pro, status: CusProductStatus.Scheduled },
|
||||||
],
|
],
|
||||||
// otherProducts: [g2Premium],
|
// otherProducts: [g2Premium],
|
||||||
|
skipFeatureCheck: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -123,6 +124,7 @@ describe(`${chalk.yellowBright("mergedGroup2: Testing products from diff groups"
|
|||||||
db,
|
db,
|
||||||
org,
|
org,
|
||||||
env,
|
env,
|
||||||
|
skipFeatureCheck: op.skipFeatureCheck,
|
||||||
});
|
});
|
||||||
|
|
||||||
const customer = await autumn.customers.get(customerId);
|
const customer = await autumn.customers.get(customerId);
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ export const expectSubToBeCorrect = async ({
|
|||||||
|
|
||||||
// Add to schedules
|
// Add to schedules
|
||||||
const scheduleIndexes: number[] = [];
|
const scheduleIndexes: number[] = [];
|
||||||
const apiVersion = cusProduct.api_version || defaultApiVersion;
|
const apiVersion = cusProduct.api_semver || defaultApiVersion;
|
||||||
|
|
||||||
if (isFreeProduct(product.prices)) {
|
if (isFreeProduct(product.prices)) {
|
||||||
expect(cusProduct.subscription_ids, "free product should have no subs").to
|
expect(cusProduct.subscription_ids, "free product should have no subs").to
|
||||||
@@ -244,7 +244,7 @@ export const expectSubToBeCorrect = async ({
|
|||||||
cp.product.group === product.group &&
|
cp.product.group === product.group &&
|
||||||
cp.status === CusProductStatus.Scheduled &&
|
cp.status === CusProductStatus.Scheduled &&
|
||||||
(cp.internal_entity_id
|
(cp.internal_entity_id
|
||||||
? cp.internal_entity_id == cusProduct.internal_entity_id
|
? cp.internal_entity_id === cusProduct.internal_entity_id
|
||||||
: nullish(cp.internal_entity_id)),
|
: nullish(cp.internal_entity_id)),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -324,7 +324,6 @@ export const expectSubToBeCorrect = async ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (existingIndex !== -1) {
|
if (existingIndex !== -1) {
|
||||||
// @ts-expect-error
|
|
||||||
supposedSubItems[existingIndex].quantity += lineItem.quantity;
|
supposedSubItems[existingIndex].quantity += lineItem.quantity;
|
||||||
} else {
|
} else {
|
||||||
supposedSubItems.push({
|
supposedSubItems.push({
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ export const attachAndExpectCorrect = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!skipFeatureCheck && branch === AttachBranch.Downgrade) {
|
if (!skipFeatureCheck && branch !== AttachBranch.Downgrade) {
|
||||||
expectFeaturesCorrect({
|
expectFeaturesCorrect({
|
||||||
customer,
|
customer,
|
||||||
product,
|
product,
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import { nullish } from "@/utils/genUtils.js";
|
|
||||||
import { expect } from "chai";
|
|
||||||
import { notNullish } from "@/utils/genUtils.js";
|
|
||||||
import {
|
import {
|
||||||
CreateEntity,
|
type CreateEntity,
|
||||||
FeatureOptions,
|
type FeatureOptions,
|
||||||
FeatureType,
|
|
||||||
Infinite,
|
Infinite,
|
||||||
ProductV2,
|
type ProductV2,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import { Customer, Entity } from "autumn-js";
|
import type { Customer, Entity } from "autumn-js";
|
||||||
|
import { expect } from "chai";
|
||||||
|
import { notNullish, nullish } from "@/utils/genUtils.js";
|
||||||
|
|
||||||
export const expectFeaturesCorrect = ({
|
export const expectFeaturesCorrect = ({
|
||||||
customer,
|
customer,
|
||||||
@@ -38,22 +36,24 @@ export const expectFeaturesCorrect = ({
|
|||||||
|
|
||||||
const otherItems = otherProducts?.flatMap((p) => p.items) || [];
|
const otherItems = otherProducts?.flatMap((p) => p.items) || [];
|
||||||
|
|
||||||
|
// console.log(`Product:`, product);
|
||||||
|
// console.log("Customer features:", customer.features);
|
||||||
for (const featureId of featureIds) {
|
for (const featureId of featureIds) {
|
||||||
let includedUsage: string | number = 0;
|
let includedUsage: string | number = 0;
|
||||||
|
|
||||||
let item = items.find((i) => i.feature_id === featureId)!;
|
const item = items.find((i) => i.feature_id === featureId)!;
|
||||||
expect(item, `Item ${featureId} exists`).to.exist;
|
expect(item, `Item ${featureId} exists`).to.exist;
|
||||||
|
|
||||||
if (item.included_usage === undefined) continue;
|
if (item.included_usage === undefined) continue;
|
||||||
|
|
||||||
for (const item of [...items, ...otherItems]) {
|
for (const item of [...items, ...otherItems]) {
|
||||||
if (item.feature_id !== featureId) continue;
|
if (item.feature_id !== featureId) continue;
|
||||||
if (item.included_usage == Infinite) {
|
if (item.included_usage === Infinite) {
|
||||||
includedUsage = Infinite;
|
includedUsage = Infinite;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let numEntities =
|
const numEntities =
|
||||||
entities?.filter((e) => e.feature_id === item.entity_feature_id)
|
entities?.filter((e) => e.feature_id === item.entity_feature_id)
|
||||||
.length || 1;
|
.length || 1;
|
||||||
|
|
||||||
@@ -72,26 +72,25 @@ export const expectFeaturesCorrect = ({
|
|||||||
|
|
||||||
expect(feature, `Feature ${featureId} exists`).to.exist;
|
expect(feature, `Feature ${featureId} exists`).to.exist;
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
|
|
||||||
// 1. Check that included usage matches
|
// 1. Check that included usage matches
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
feature.included_usage,
|
feature.included_usage,
|
||||||
`Feature ${featureId} included usage is correct`,
|
`Feature ${featureId} included usage is correct`,
|
||||||
).to.equal(includedUsage);
|
).to.equal(includedUsage);
|
||||||
|
|
||||||
// 2. Check that unlimited is set correctly
|
// 2. Check that unlimited is set correctly
|
||||||
if (item?.included_usage == Infinite) {
|
if (item?.included_usage === Infinite) {
|
||||||
expect(feature.unlimited, `Feature ${featureId} is unlimited`).to.be.true;
|
expect(feature.unlimited, `Feature ${featureId} is unlimited`).to.be.true;
|
||||||
} else {
|
} else {
|
||||||
expect(
|
expect(
|
||||||
feature.unlimited == false || nullish(feature.unlimited),
|
feature.unlimited === false || nullish(feature.unlimited),
|
||||||
`Feature ${featureId} is not unlimited`,
|
`Feature ${featureId} is not unlimited`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Check that usage is correct...
|
// 3. Check that usage is correct...
|
||||||
let featureUsage =
|
const featureUsage =
|
||||||
usage?.reduce((acc, curr) => {
|
usage?.reduce((acc, curr) => {
|
||||||
if (curr.featureId === featureId) {
|
if (curr.featureId === featureId) {
|
||||||
acc += curr.value;
|
acc += curr.value;
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
|
||||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
|
||||||
import { getStripeSchedules } from "@/external/stripe/stripeSubUtils.js";
|
|
||||||
import { findStripePriceFromPrices } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
|
|
||||||
import { cusProductToPrices } from "@autumn/shared";
|
|
||||||
import { CusService } from "@/internal/customers/CusService.js";
|
|
||||||
import { isV4Usage } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
|
|
||||||
import {
|
import {
|
||||||
AppEnv,
|
type AppEnv,
|
||||||
AttachBranch,
|
AttachBranch,
|
||||||
AttachPreview,
|
type AttachPreview,
|
||||||
CusProductStatus,
|
CusProductStatus,
|
||||||
FullCusProduct,
|
cusProductToPrices,
|
||||||
FullCustomer,
|
type FullCusProduct,
|
||||||
Organization,
|
type FullCustomer,
|
||||||
ProductV2,
|
type Organization,
|
||||||
|
type ProductV2,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
import Stripe from "stripe";
|
|
||||||
import { expectSubItemsCorrect } from "./expectSubUtils.js";
|
|
||||||
import { expectProductAttached } from "./expectProductAttached.js";
|
|
||||||
import { isFreeProductV2 } from "@/internal/products/productUtils/classifyProduct.js";
|
|
||||||
import { advanceTestClock } from "../stripeUtils.js";
|
|
||||||
import { hoursToFinalizeInvoice } from "../constants.js";
|
|
||||||
import { addHours } from "date-fns";
|
import { addHours } from "date-fns";
|
||||||
|
import type Stripe from "stripe";
|
||||||
import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js";
|
import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js";
|
||||||
|
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||||
|
import type { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||||
|
import { findStripePriceFromPrices } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
|
||||||
|
import { getStripeSchedules } from "@/external/stripe/stripeSubUtils.js";
|
||||||
|
import { CusService } from "@/internal/customers/CusService.js";
|
||||||
|
import { isV4Usage } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
|
||||||
|
import { isFreeProductV2 } from "@/internal/products/productUtils/classifyProduct.js";
|
||||||
|
import { hoursToFinalizeInvoice } from "../constants.js";
|
||||||
|
import { advanceTestClock } from "../stripeUtils.js";
|
||||||
|
import { expectProductAttached } from "./expectProductAttached.js";
|
||||||
|
import { expectSubItemsCorrect } from "./expectSubUtils.js";
|
||||||
|
|
||||||
export const expectNextCycleCorrect = async ({
|
export const expectNextCycleCorrect = async ({
|
||||||
autumn,
|
autumn,
|
||||||
@@ -108,7 +108,7 @@ export const expectDowngradeCorrect = async ({
|
|||||||
const customer = await autumn.customers.get(customerId);
|
const customer = await autumn.customers.get(customerId);
|
||||||
|
|
||||||
const productCount = customer.products.reduce((acc: number, product: any) => {
|
const productCount = customer.products.reduce((acc: number, product: any) => {
|
||||||
if (product.group == curProduct.group) {
|
if (product.group === curProduct.group) {
|
||||||
return acc + 1;
|
return acc + 1;
|
||||||
} else return acc;
|
} else return acc;
|
||||||
}, 0);
|
}, 0);
|
||||||
@@ -201,7 +201,7 @@ export const expectSubScheduleCorrect = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cusProduct = fullCus.customer_products.find(
|
const cusProduct = fullCus.customer_products.find(
|
||||||
(cp: FullCusProduct) => cp.product.id == productId,
|
(cp: FullCusProduct) => cp.product.id === productId,
|
||||||
)!;
|
)!;
|
||||||
|
|
||||||
const scheduleSets = await getStripeSchedules({
|
const scheduleSets = await getStripeSchedules({
|
||||||
@@ -225,7 +225,6 @@ export const expectSubScheduleCorrect = async ({
|
|||||||
|
|
||||||
if (isV4Usage({ price: autumnPrice!, cusProduct })) {
|
if (isV4Usage({ price: autumnPrice!, cusProduct })) {
|
||||||
missingUsageCount++;
|
missingUsageCount++;
|
||||||
continue;
|
|
||||||
} else {
|
} else {
|
||||||
expect(stripePrice).to.exist;
|
expect(stripePrice).to.exist;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
addMonths,
|
addMonths,
|
||||||
addWeeks,
|
addWeeks,
|
||||||
format,
|
format,
|
||||||
|
subHours,
|
||||||
} from "date-fns";
|
} from "date-fns";
|
||||||
import puppeteer from "puppeteer-core";
|
import puppeteer from "puppeteer-core";
|
||||||
import type { Stripe } from "stripe";
|
import type { Stripe } from "stripe";
|
||||||
@@ -302,11 +303,12 @@ export const advanceClockForInvoice = async ({
|
|||||||
startingFrom = new Date();
|
startingFrom = new Date();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (numberOfDays) {
|
// if (numberOfDays) {
|
||||||
advanceTo = addDays(startingFrom, numberOfDays).getTime();
|
// advanceTo = addDays(startingFrom, numberOfDays).getTime();
|
||||||
} else {
|
// } else {
|
||||||
advanceTo = addMinutes(addMonths(startingFrom, 1), 10).getTime();
|
// advanceTo = addMonths(startingFrom, 1).getTime();
|
||||||
}
|
// }
|
||||||
|
advanceTo = subHours(addMonths(Date.now(), 1), 1).getTime();
|
||||||
|
|
||||||
await stripeCli.testHelpers.testClocks.advance(testClockId, {
|
await stripeCli.testHelpers.testClocks.advance(testClockId, {
|
||||||
frozen_time: Math.ceil(advanceTo / 1000),
|
frozen_time: Math.ceil(advanceTo / 1000),
|
||||||
|
|||||||
29
shared/api/common/queryHelpers.ts
Normal file
29
shared/api/common/queryHelpers.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to handle query string arrays that can be either:
|
||||||
|
* - Single value: ?expand=invoices → "invoices"
|
||||||
|
* - Comma-separated: ?expand=invoices,rewards → ["invoices", "rewards"] (requires queryMiddleware)
|
||||||
|
* - Multiple values: ?expand=invoices&expand=products → ["invoices", "products"]
|
||||||
|
*
|
||||||
|
* Normalizes all to array format for consistent Zod validation
|
||||||
|
*
|
||||||
|
* Note: Comma-separated parsing requires the queryMiddleware to be applied first.
|
||||||
|
* The middleware uses the `qs` library to parse query strings with comma support.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const schema = z.object({
|
||||||
|
* expand: queryStringArray(z.enum(CusExpand)).optional(),
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function queryStringArray<T extends z.ZodTypeAny>(schema: T) {
|
||||||
|
return z.preprocess((val) => {
|
||||||
|
// Convert single string to array for consistent handling
|
||||||
|
if (typeof val === "string") {
|
||||||
|
return [val];
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}, z.array(schema));
|
||||||
|
}
|
||||||
@@ -1,23 +1,31 @@
|
|||||||
import { APICusProductSchema } from "@api/customers/components/apiCusProduct.js";
|
import { ApiCusReferralSchema } from "@api/customers/components/apiCusReferral.js";
|
||||||
import { APICusReferralSchema } from "@api/customers/components/apiCusReferral.js";
|
|
||||||
import { ApiCusFeatureSchema } from "@api/customers/cusFeatures/apiCusFeature.js";
|
import { ApiCusFeatureSchema } from "@api/customers/cusFeatures/apiCusFeature.js";
|
||||||
import { APICusRewardsSchema } from "@api/models.js";
|
import { ApiCusProductSchema } from "@api/customers/cusProducts/apiCusProduct.js";
|
||||||
import { APIInvoiceSchema } from "@api/others/apiInvoice.js";
|
import { ApiBaseEntitySchema } from "@api/entities/apiEntity.js";
|
||||||
import { EntityResponseSchema } from "@models/cusModels/entityModels/entityResModels.js";
|
import { ApiCusRewardsSchema } from "@api/others/apiDiscount.js";
|
||||||
|
import { ApiInvoiceSchema } from "@api/others/apiInvoice.js";
|
||||||
import { AppEnv } from "@models/genModels/genEnums.js";
|
import { AppEnv } from "@models/genModels/genEnums.js";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
import { APICusUpcomingInvoiceSchema } from "./components/apiCusUpcomingInvoice.js";
|
import { ApiCusUpcomingInvoiceSchema } from "./components/apiCusUpcomingInvoice.js";
|
||||||
|
|
||||||
export const APITrialsUsedSchema = z.object({
|
export const ApiTrialsUsedSchema = z.object({
|
||||||
product_id: z.string(),
|
product_id: z.string(),
|
||||||
customer_id: z.string(),
|
customer_id: z.string(),
|
||||||
fingerprint: z.string().nullish(),
|
fingerprint: z.string().nullish(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const ApiCusExpandSchema = z.object({
|
||||||
|
invoices: z.array(ApiInvoiceSchema).optional(),
|
||||||
|
entities: z.array(ApiBaseEntitySchema).optional(),
|
||||||
|
trials_used: z.array(ApiTrialsUsedSchema).optional(),
|
||||||
|
rewards: ApiCusRewardsSchema.nullish(),
|
||||||
|
referrals: z.array(ApiCusReferralSchema).optional(),
|
||||||
|
upcoming_invoice: ApiCusUpcomingInvoiceSchema.nullish(),
|
||||||
|
payment_method: z.any().nullish(),
|
||||||
|
});
|
||||||
|
|
||||||
export const ApiCustomerSchema = z.object({
|
export const ApiCustomerSchema = z.object({
|
||||||
// Internal fields
|
// Internal fields
|
||||||
autumn_id: z.string().nullish(),
|
|
||||||
|
|
||||||
id: z.string().nullable().meta({
|
id: z.string().nullable().meta({
|
||||||
description: "Your internal ID for the customer",
|
description: "Your internal ID for the customer",
|
||||||
example: "cus_123",
|
example: "cus_123",
|
||||||
@@ -34,18 +42,13 @@ export const ApiCustomerSchema = z.object({
|
|||||||
fingerprint: z.string().nullable(),
|
fingerprint: z.string().nullable(),
|
||||||
stripe_id: z.string().nullable().default(null),
|
stripe_id: z.string().nullable().default(null),
|
||||||
env: z.enum(AppEnv),
|
env: z.enum(AppEnv),
|
||||||
|
|
||||||
products: z.array(APICusProductSchema),
|
|
||||||
features: z.record(z.string(), ApiCusFeatureSchema),
|
|
||||||
invoices: z.array(APIInvoiceSchema).optional(),
|
|
||||||
trials_used: z.array(APITrialsUsedSchema).optional(),
|
|
||||||
|
|
||||||
rewards: APICusRewardsSchema.nullish(),
|
|
||||||
metadata: z.record(z.any(), z.any()).default({}),
|
metadata: z.record(z.any(), z.any()).default({}),
|
||||||
entities: z.array(EntityResponseSchema).optional(),
|
|
||||||
referrals: z.array(APICusReferralSchema).optional(),
|
products: z.array(ApiCusProductSchema),
|
||||||
upcoming_invoice: APICusUpcomingInvoiceSchema.nullish(),
|
features: z.record(z.string(), ApiCusFeatureSchema),
|
||||||
payment_method: z.any().nullish(),
|
|
||||||
|
...ApiCusExpandSchema.shape,
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ApiCustomer = z.infer<typeof ApiCustomerSchema>;
|
export type ApiCustomer = z.infer<typeof ApiCustomerSchema>;
|
||||||
|
export type ApiCustomerExpand = z.infer<typeof ApiCusExpandSchema>;
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
import { ApiCustomerSchema } from "@api/customers/apiCustomer.js";
|
|
||||||
import { ApiVersion } from "@api/versionUtils/ApiVersion.js";
|
import { ApiVersion } from "@api/versionUtils/ApiVersion.js";
|
||||||
import {
|
import {
|
||||||
AffectedResource,
|
AffectedResource,
|
||||||
defineVersionChange,
|
defineVersionChange,
|
||||||
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
|
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
|
||||||
import { z } from "zod/v4";
|
import type { z } from "zod/v4";
|
||||||
import {
|
import {
|
||||||
type ApiCusFeatureV0,
|
type ApiCusFeatureV0,
|
||||||
ApiCusFeatureV0Schema,
|
ApiCusFeatureV0Schema,
|
||||||
} from "../cusFeatures/previousVersions/apiCusFeatureV0.js";
|
} from "../cusFeatures/previousVersions/apiCusFeatureV0.js";
|
||||||
import { ApiCusFeatureV1Schema } from "../cusFeatures/previousVersions/apiCusFeatureV1.js";
|
import { ApiCustomerV0Schema } from "../previousVersions/apiCustomerV0.js";
|
||||||
|
import { ApiCustomerV1Schema } from "../previousVersions/apiCustomerV1.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* V0_2: Customer feature field additions
|
* V0_1_CustomerChange: Transforms customer TO V0_1 format
|
||||||
*
|
*
|
||||||
* This change handles the fields added in V0_2.
|
* Applied when: targetVersion <= V0_1
|
||||||
* When transforming to V0_1, we remove: next_reset_at, allowance, usage_limit
|
*
|
||||||
|
* Fields added in V0_2 (that we remove here):
|
||||||
|
* - next_reset_at
|
||||||
|
* - allowance
|
||||||
|
* - usage_limit
|
||||||
*
|
*
|
||||||
* V0_2+ format (V1):
|
* V0_2+ format (V1):
|
||||||
* - feature_id, unlimited, interval, balance, used
|
* - feature_id, unlimited, interval, balance, used
|
||||||
@@ -26,40 +30,27 @@ import { ApiCusFeatureV1Schema } from "../cusFeatures/previousVersions/apiCusFea
|
|||||||
* - NO next_reset_at, allowance, usage_limit (original format)
|
* - NO next_reset_at, allowance, usage_limit (original format)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// V0_2+ customer schema (features as array of V1 - with extra fields)
|
export const V0_1_CustomerChange = defineVersionChange({
|
||||||
const V0_2_CustomerSchema = ApiCustomerSchema.extend({
|
oldVersion: ApiVersion.V0_1, // Applied when targetVersion <= V0_1
|
||||||
features: z.array(ApiCusFeatureV1Schema),
|
newVersion: ApiVersion.V0_2, // Breaking change introduced in V0_2
|
||||||
});
|
|
||||||
|
|
||||||
// V0_1 customer schema (features as array of V0 - original format)
|
|
||||||
const V0_1_CustomerSchema = ApiCustomerSchema.extend({
|
|
||||||
features: z.array(ApiCusFeatureV0Schema),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const V0_2_CusFeatureChange = defineVersionChange({
|
|
||||||
version: ApiVersion.V0_2,
|
|
||||||
description: [
|
description: [
|
||||||
"Features: V1 format (with next_reset_at, allowance) → V0 format (minimal fields)",
|
"Features: V1 format (with next_reset_at, allowance) → V0 format (minimal fields)",
|
||||||
],
|
],
|
||||||
|
|
||||||
affectedResources: [AffectedResource.Customer],
|
affectedResources: [AffectedResource.Customer],
|
||||||
newSchema: V0_2_CustomerSchema,
|
oldSchema: ApiCustomerV0Schema,
|
||||||
oldSchema: V0_1_CustomerSchema,
|
newSchema: ApiCustomerV1Schema,
|
||||||
|
|
||||||
affectsRequest: false,
|
|
||||||
affectsResponse: true,
|
|
||||||
hasSideEffects: false,
|
|
||||||
|
|
||||||
// Response: V0_2 Customer → V0_1 Customer
|
// Response: V0_2 Customer → V0_1 Customer
|
||||||
// Remove next_reset_at, allowance, usage_limit fields
|
// Remove next_reset_at, allowance, usage_limit fields
|
||||||
transformResponse: ({
|
transformResponse: ({
|
||||||
input,
|
input,
|
||||||
}: {
|
}: {
|
||||||
input: z.infer<typeof V0_2_CustomerSchema>;
|
input: z.infer<typeof ApiCustomerV1Schema>;
|
||||||
}) => {
|
}) => {
|
||||||
const v0_features: ApiCusFeatureV0[] = [];
|
const v0_features: ApiCusFeatureV0[] = [];
|
||||||
|
|
||||||
for (const feature of input.features) {
|
for (const feature of input.entitlements) {
|
||||||
// Unlimited features
|
// Unlimited features
|
||||||
if (feature.unlimited) {
|
if (feature.unlimited) {
|
||||||
const v0Feature = ApiCusFeatureV0Schema.parse({
|
const v0Feature = ApiCusFeatureV0Schema.parse({
|
||||||
@@ -74,7 +65,7 @@ export const V0_2_CusFeatureChange = defineVersionChange({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Boolean/Static features
|
// Boolean/Static features
|
||||||
if (!feature.interval && !feature.balance && feature.used === undefined) {
|
if (!feature.interval && !feature.balance && !feature.unlimited) {
|
||||||
const v0Feature = ApiCusFeatureV0Schema.parse({
|
const v0Feature = ApiCusFeatureV0Schema.parse({
|
||||||
feature_id: feature.feature_id,
|
feature_id: feature.feature_id,
|
||||||
interval: null,
|
interval: null,
|
||||||
@@ -92,13 +83,29 @@ export const V0_2_CusFeatureChange = defineVersionChange({
|
|||||||
used: feature.used,
|
used: feature.used,
|
||||||
// Remove: next_reset_at, allowance, usage_limit
|
// Remove: next_reset_at, allowance, usage_limit
|
||||||
});
|
});
|
||||||
console.log("v0Feature", v0Feature);
|
|
||||||
v0_features.push(v0Feature);
|
v0_features.push(v0Feature);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const products = input.products.map((p) => {
|
||||||
|
const {
|
||||||
|
current_period_start: _currentPeriodStart,
|
||||||
|
current_period_end: _currentPeriodEnd,
|
||||||
|
...rest
|
||||||
|
} = p;
|
||||||
|
return rest;
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...input,
|
...input,
|
||||||
features: v0_features,
|
customer: {
|
||||||
} satisfies z.infer<typeof V0_1_CustomerSchema>;
|
...input.customer,
|
||||||
|
name: input.customer.name || "",
|
||||||
|
email: input.customer.email || "",
|
||||||
|
fingerprint: input.customer.fingerprint || "",
|
||||||
|
},
|
||||||
|
entitlements: v0_features,
|
||||||
|
products,
|
||||||
|
} satisfies z.infer<typeof ApiCustomerV0Schema>;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
168
shared/api/customers/changes/V0_2_CustomerChange.ts
Normal file
168
shared/api/customers/changes/V0_2_CustomerChange.ts
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import { ApiVersion } from "@api/versionUtils/ApiVersion.js";
|
||||||
|
import {
|
||||||
|
AffectedResource,
|
||||||
|
defineVersionChange,
|
||||||
|
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
|
||||||
|
import type { z } from "zod/v4";
|
||||||
|
import {
|
||||||
|
type ApiCusFeatureV1,
|
||||||
|
ApiCusFeatureV1Schema,
|
||||||
|
} from "../cusFeatures/previousVersions/apiCusFeatureV1.js";
|
||||||
|
import type { ApiCusFeatureV2 } from "../cusFeatures/previousVersions/apiCusFeatureV2.js";
|
||||||
|
import { transformCusProductV2ToV1 } from "../cusProducts/changes/V0_2_CusProductChange.js";
|
||||||
|
import {
|
||||||
|
type CustomerLegacyData,
|
||||||
|
CustomerLegacyDataSchema,
|
||||||
|
} from "../customerLegacyData.js";
|
||||||
|
import { ApiCustomerV1Schema } from "../previousVersions/apiCustomerV1.js";
|
||||||
|
import { ApiCustomerV2Schema } from "../previousVersions/apiCustomerV2.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* V0_2_CustomerChange: Transforms customer response TO V0_2 format
|
||||||
|
*
|
||||||
|
* Applied when: targetVersion <= V0_2
|
||||||
|
*
|
||||||
|
* Breaking changes introduced in V1.1 (that we reverse here):
|
||||||
|
*
|
||||||
|
* 1. Structure: Merged customer object → Split structure
|
||||||
|
* - V1.1+: Single merged object with all fields
|
||||||
|
* - V0_2: Split into {customer, products, add_ons, entitlements, invoices}
|
||||||
|
*
|
||||||
|
* 2. Features: Renamed and transformed
|
||||||
|
* - V1.1+: "features" with usage/included_usage/interval_count fields
|
||||||
|
* - V0_2: "entitlements" with used/allowance fields
|
||||||
|
*
|
||||||
|
* Input: ApiCustomerV2 (V1.1+ merged format)
|
||||||
|
* Output: ApiCustomerV1 (V0_2 split format)
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const V0_2_CustomerChange = defineVersionChange({
|
||||||
|
newVersion: ApiVersion.V1_1, // Breaking change introduced in V1_1
|
||||||
|
oldVersion: ApiVersion.V0_2, // Applied when targetVersion <= V0_2
|
||||||
|
description: [
|
||||||
|
"Customer response split into separate objects (customer, products, add_ons, entitlements, invoices)",
|
||||||
|
"Features renamed to entitlements",
|
||||||
|
"Features: V2 format (usage, included_usage, interval_count) → V1 format (used, allowance)",
|
||||||
|
],
|
||||||
|
affectedResources: [AffectedResource.Customer],
|
||||||
|
newSchema: ApiCustomerV2Schema,
|
||||||
|
oldSchema: ApiCustomerV1Schema,
|
||||||
|
|
||||||
|
legacyDataSchema: CustomerLegacyDataSchema,
|
||||||
|
|
||||||
|
// Response: V1.1+ (V2) → V0_2 (V1)
|
||||||
|
transformResponse: ({
|
||||||
|
input,
|
||||||
|
legacyData,
|
||||||
|
}: {
|
||||||
|
input: z.infer<typeof ApiCustomerV2Schema>;
|
||||||
|
legacyData?: CustomerLegacyData;
|
||||||
|
}) => {
|
||||||
|
// Step 1: Transform features V2 → V1
|
||||||
|
const v1_features: ApiCusFeatureV1[] = transformFeaturesV2ToV1({
|
||||||
|
features: input.features,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 2: Split merged structure
|
||||||
|
const {
|
||||||
|
features: _features,
|
||||||
|
products = [],
|
||||||
|
invoices,
|
||||||
|
trials_used,
|
||||||
|
...customerFields
|
||||||
|
} = input;
|
||||||
|
|
||||||
|
const mainProducts = products
|
||||||
|
.filter((p) => !p.is_add_on)
|
||||||
|
.map((p) =>
|
||||||
|
transformCusProductV2ToV1({
|
||||||
|
input: p,
|
||||||
|
legacyData: legacyData?.cusProductLegacyData[p.id],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const addOns = products
|
||||||
|
.filter((p) => p.is_add_on)
|
||||||
|
.map((p) =>
|
||||||
|
transformCusProductV2ToV1({
|
||||||
|
input: p,
|
||||||
|
legacyData: legacyData?.cusProductLegacyData[p.id],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
customer: {
|
||||||
|
...customerFields,
|
||||||
|
internal_id: "",
|
||||||
|
},
|
||||||
|
products: mainProducts,
|
||||||
|
add_ons: addOns,
|
||||||
|
|
||||||
|
entitlements: v1_features,
|
||||||
|
invoices: invoices || [],
|
||||||
|
trials_used,
|
||||||
|
} satisfies z.infer<typeof ApiCustomerV1Schema>;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform features from V2 format to V1 format
|
||||||
|
*
|
||||||
|
* V2 → V1 field mappings:
|
||||||
|
* - usage → used
|
||||||
|
* - included_usage → allowance
|
||||||
|
* - Remove: interval_count, overage_allowed, rollovers
|
||||||
|
*/
|
||||||
|
function transformFeaturesV2ToV1({
|
||||||
|
features,
|
||||||
|
}: {
|
||||||
|
features: ApiCusFeatureV2[];
|
||||||
|
}): ApiCusFeatureV1[] {
|
||||||
|
const v1_features: ApiCusFeatureV1[] = [];
|
||||||
|
|
||||||
|
for (const feature of features) {
|
||||||
|
// Unlimited features
|
||||||
|
if (feature.unlimited) {
|
||||||
|
const v1Feature = ApiCusFeatureV1Schema.parse({
|
||||||
|
feature_id: feature.feature_id,
|
||||||
|
unlimited: true,
|
||||||
|
interval: null,
|
||||||
|
balance: null,
|
||||||
|
used: null,
|
||||||
|
});
|
||||||
|
v1_features.push(v1Feature);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Boolean/Static features (only have feature_id, no other fields)
|
||||||
|
if (
|
||||||
|
!feature.interval &&
|
||||||
|
!feature.balance &&
|
||||||
|
feature.usage === undefined &&
|
||||||
|
feature.included_usage === undefined
|
||||||
|
) {
|
||||||
|
const v1Feature = ApiCusFeatureV1Schema.parse({
|
||||||
|
feature_id: feature.feature_id,
|
||||||
|
interval: null,
|
||||||
|
});
|
||||||
|
v1_features.push(v1Feature);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular/Metered features
|
||||||
|
const v1Feature = ApiCusFeatureV1Schema.parse({
|
||||||
|
feature_id: feature.feature_id,
|
||||||
|
unlimited: feature.unlimited || false,
|
||||||
|
interval: feature.interval,
|
||||||
|
balance: feature.balance,
|
||||||
|
used: feature.usage, // usage → used
|
||||||
|
next_reset_at: feature.next_reset_at,
|
||||||
|
allowance: feature.included_usage, // included_usage → allowance
|
||||||
|
usage_limit: feature.usage_limit,
|
||||||
|
// Remove: interval_count, overage_allowed, rollovers
|
||||||
|
});
|
||||||
|
v1_features.push(v1Feature);
|
||||||
|
}
|
||||||
|
|
||||||
|
return v1_features;
|
||||||
|
}
|
||||||
@@ -6,24 +6,28 @@ import {
|
|||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* V1_1: Invoices expansion behavior changed (side effect only)
|
* V0_2_InvoicesAlwaysExpanded: Side effect for legacy invoice expansion
|
||||||
*
|
*
|
||||||
* V1_1+: Invoices require explicit expand parameter
|
* Applied when: targetVersion <= V0_2
|
||||||
* V1_0: Invoices always included (side effect - must be handled in handler)
|
*
|
||||||
|
* Before V1_1, invoices were always included in customer responses.
|
||||||
|
* After V1_1, invoices require an explicit expand parameter.
|
||||||
*
|
*
|
||||||
* This change has side effects and doesn't transform data.
|
* This change has side effects and doesn't transform data.
|
||||||
* The handler must add CusExpand.Invoices for V1_0 requests.
|
* The handler must add expand=invoices for requests targeting V0_2 or older.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Schema is just any since this is a side-effect only change
|
// Schema is just any since this is a side-effect only change
|
||||||
const NoOpSchema = z.any();
|
const NoOpSchema = z.any();
|
||||||
|
|
||||||
export class V1_1_LegacyExpandInvoices extends VersionChange<
|
export class V0_2_InvoicesAlwaysExpanded extends VersionChange<
|
||||||
typeof NoOpSchema,
|
typeof NoOpSchema,
|
||||||
typeof NoOpSchema
|
typeof NoOpSchema
|
||||||
> {
|
> {
|
||||||
readonly version = ApiVersion.V1_1;
|
readonly newVersion = ApiVersion.V1_1; // Breaking change introduced in V1_1
|
||||||
readonly description = "Invoices always expanded before V1_1";
|
readonly oldVersion = ApiVersion.V0_2; // Applied when targetVersion <= V0_2
|
||||||
|
readonly description =
|
||||||
|
"Invoices always expanded before V1_1 (requires expand parameter in handler)";
|
||||||
readonly affectedResources = [AffectedResource.Customer];
|
readonly affectedResources = [AffectedResource.Customer];
|
||||||
readonly hasSideEffects = true;
|
readonly hasSideEffects = true;
|
||||||
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import { ApiCustomerSchema } from "@api/customers/apiCustomer.js";
|
|
||||||
import { ApiVersion } from "@api/versionUtils/ApiVersion.js";
|
|
||||||
import {
|
|
||||||
AffectedResource,
|
|
||||||
defineVersionChange,
|
|
||||||
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
|
|
||||||
import { z } from "zod/v4";
|
|
||||||
import {
|
|
||||||
type ApiCusFeatureV1,
|
|
||||||
ApiCusFeatureV1Schema,
|
|
||||||
} from "../cusFeatures/previousVersions/apiCusFeatureV1.js";
|
|
||||||
import { ApiCusFeatureV2Schema } from "../cusFeatures/previousVersions/apiCusFeatureV2.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* V1_1: Customer feature schema changes
|
|
||||||
*
|
|
||||||
* V1_1+ format (V2):
|
|
||||||
* - id (feature id)
|
|
||||||
* - usage (current usage)
|
|
||||||
* - included_usage (allowance)
|
|
||||||
* - interval_count (interval multiplier)
|
|
||||||
* - overage_allowed (whether overages permitted)
|
|
||||||
* - type, name (feature metadata)
|
|
||||||
* - rollovers (rollover data)
|
|
||||||
*
|
|
||||||
* V1_0 format (V1):
|
|
||||||
* - feature_id (feature id)
|
|
||||||
* - used (current usage)
|
|
||||||
* - allowance (included usage)
|
|
||||||
* - next_reset_at, usage_limit
|
|
||||||
* - No interval_count
|
|
||||||
* - No overage_allowed
|
|
||||||
* - No type, name
|
|
||||||
* - No rollovers
|
|
||||||
*/
|
|
||||||
|
|
||||||
// V1_1+ customer schema (features as array of V2)
|
|
||||||
const V1_1_CustomerSchema = ApiCustomerSchema.extend({
|
|
||||||
features: z.array(ApiCusFeatureV2Schema),
|
|
||||||
});
|
|
||||||
|
|
||||||
// V1_0 customer schema (features as array of V1)
|
|
||||||
const V1_0_CustomerSchema = ApiCustomerSchema.extend({
|
|
||||||
features: z.array(ApiCusFeatureV1Schema),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const V1_1_CusFeatureChange = defineVersionChange({
|
|
||||||
version: ApiVersion.V1_1,
|
|
||||||
description: [
|
|
||||||
"Features: V2 format (usage, included_usage, interval_count) → V1 format (used, allowance)",
|
|
||||||
"Renamed features to entitlements",
|
|
||||||
],
|
|
||||||
affectedResources: [AffectedResource.Customer],
|
|
||||||
newSchema: V1_1_CustomerSchema,
|
|
||||||
oldSchema: V1_0_CustomerSchema,
|
|
||||||
|
|
||||||
affectsRequest: false,
|
|
||||||
affectsResponse: true,
|
|
||||||
hasSideEffects: false,
|
|
||||||
|
|
||||||
// Response: V2 Customer → V1 Customer
|
|
||||||
// Transform field names and structure
|
|
||||||
transformResponse: ({
|
|
||||||
input,
|
|
||||||
}: {
|
|
||||||
input: z.infer<typeof V1_1_CustomerSchema>;
|
|
||||||
}) => {
|
|
||||||
const v1_features: ApiCusFeatureV1[] = [];
|
|
||||||
|
|
||||||
for (const feature of input.features) {
|
|
||||||
// Unlimited features
|
|
||||||
if (feature.unlimited) {
|
|
||||||
const v1Feature = ApiCusFeatureV1Schema.parse({
|
|
||||||
feature_id: feature.feature_id,
|
|
||||||
unlimited: true,
|
|
||||||
interval: null,
|
|
||||||
balance: null,
|
|
||||||
used: null,
|
|
||||||
});
|
|
||||||
v1_features.push(v1Feature);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Boolean/Static features (only have feature_id, no other fields)
|
|
||||||
if (
|
|
||||||
!feature.interval &&
|
|
||||||
!feature.balance &&
|
|
||||||
feature.usage === undefined &&
|
|
||||||
feature.included_usage === undefined
|
|
||||||
) {
|
|
||||||
const v1Feature = ApiCusFeatureV1Schema.parse({
|
|
||||||
feature_id: feature.feature_id,
|
|
||||||
interval: null,
|
|
||||||
});
|
|
||||||
v1_features.push(v1Feature);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regular/Metered features
|
|
||||||
const v1Feature = ApiCusFeatureV1Schema.parse({
|
|
||||||
feature_id: feature.feature_id,
|
|
||||||
unlimited: feature.unlimited || false,
|
|
||||||
interval: feature.interval,
|
|
||||||
balance: feature.balance,
|
|
||||||
used: feature.usage, // usage → used
|
|
||||||
next_reset_at: feature.next_reset_at,
|
|
||||||
allowance: feature.included_usage, // included_usage → allowance
|
|
||||||
usage_limit: feature.usage_limit,
|
|
||||||
// Remove: interval_count, overage_allowed, rollovers
|
|
||||||
});
|
|
||||||
v1_features.push(v1Feature);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...input,
|
|
||||||
features: v1_features,
|
|
||||||
} satisfies z.infer<typeof V1_0_CustomerSchema>;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -31,11 +31,14 @@ const V1_1_CustomerSchema = ApiCustomerSchema.extend({
|
|||||||
features: z.array(ApiCusFeatureV2Schema),
|
features: z.array(ApiCusFeatureV2Schema),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const V1_2_FeaturesArrayToObject = defineVersionChange({
|
export const V1_1_FeaturesArrayToObject = defineVersionChange({
|
||||||
version: ApiVersion.V1_2,
|
newVersion: ApiVersion.V1_2,
|
||||||
|
oldVersion: ApiVersion.V1_1,
|
||||||
|
|
||||||
description: [
|
description: [
|
||||||
"Features: object with breakdown → array with expanded intervals",
|
"Features: object with breakdown → array with expanded intervals",
|
||||||
],
|
],
|
||||||
|
|
||||||
affectedResources: [AffectedResource.Customer],
|
affectedResources: [AffectedResource.Customer],
|
||||||
newSchema: V1_2_CustomerSchema,
|
newSchema: V1_2_CustomerSchema,
|
||||||
oldSchema: V1_1_CustomerSchema,
|
oldSchema: V1_1_CustomerSchema,
|
||||||
@@ -61,8 +64,7 @@ export const V1_2_FeaturesArrayToObject = defineVersionChange({
|
|||||||
for (const breakdownItem of feature.breakdown) {
|
for (const breakdownItem of feature.breakdown) {
|
||||||
v1_1_features.push({
|
v1_1_features.push({
|
||||||
feature_id: feature.id,
|
feature_id: feature.id,
|
||||||
// type: feature.type,
|
|
||||||
// name: feature.name,
|
|
||||||
// All interval-specific data from breakdown
|
// All interval-specific data from breakdown
|
||||||
interval: breakdownItem.interval,
|
interval: breakdownItem.interval,
|
||||||
interval_count: breakdownItem.interval_count,
|
interval_count: breakdownItem.interval_count,
|
||||||
@@ -113,8 +115,6 @@ export const V1_2_FeaturesArrayToObject = defineVersionChange({
|
|||||||
|
|
||||||
// Boolean / unlimited features don't have the rest of the fields
|
// Boolean / unlimited features don't have the rest of the fields
|
||||||
|
|
||||||
console.log("v1_1_features", v1_1_features);
|
|
||||||
|
|
||||||
// Validation happens automatically in VersionChange base class
|
// Validation happens automatically in VersionChange base class
|
||||||
return {
|
return {
|
||||||
...input,
|
...input,
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import { ApiCustomerSchema } from "@api/customers/apiCustomer.js";
|
|
||||||
import { APICusProductSchema } from "@api/customers/components/apiCusProduct.js";
|
|
||||||
import {
|
|
||||||
type ApiCusFeature,
|
|
||||||
ApiCusFeatureSchema,
|
|
||||||
} from "@api/customers/cusFeatures/apiCusFeature.js";
|
|
||||||
import { ApiVersion } from "@api/versionUtils/ApiVersion.js";
|
|
||||||
import {
|
|
||||||
AffectedResource,
|
|
||||||
VersionChange,
|
|
||||||
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
|
|
||||||
import { z } from "zod/v4";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* V1_1: Customer response structure changed from split to merged
|
|
||||||
*
|
|
||||||
* V1_1+ format: Merged single object with features/products
|
|
||||||
* V1_0 format: Split into {customer, products, add_ons, entitlements, invoices}
|
|
||||||
*/
|
|
||||||
|
|
||||||
// V1_1+ merged response schema
|
|
||||||
const V1_1_CustomerResponseSchema = ApiCustomerSchema;
|
|
||||||
|
|
||||||
// V1_0 split response schema
|
|
||||||
const V1_0_CustomerResponseSchema = z.object({
|
|
||||||
customer: ApiCustomerSchema.omit({
|
|
||||||
features: true,
|
|
||||||
products: true,
|
|
||||||
invoices: true,
|
|
||||||
trials_used: true,
|
|
||||||
}),
|
|
||||||
products: z.array(APICusProductSchema),
|
|
||||||
add_ons: z.array(APICusProductSchema),
|
|
||||||
entitlements: z.array(ApiCusFeatureSchema),
|
|
||||||
invoices: z.array(z.any()),
|
|
||||||
trials_used: z.array(z.any()).optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Feature changes
|
|
||||||
const toV1_0_cusFeature = ({ cusFeature }: { cusFeature: ApiCusFeature }) => {};
|
|
||||||
|
|
||||||
export class V1_1_MergedResponse extends VersionChange<
|
|
||||||
typeof V1_1_CustomerResponseSchema,
|
|
||||||
typeof V1_0_CustomerResponseSchema
|
|
||||||
> {
|
|
||||||
readonly version = ApiVersion.V1_1;
|
|
||||||
readonly description = "Merged customer response → split structure";
|
|
||||||
readonly affectedResources = [AffectedResource.Customer];
|
|
||||||
readonly affectsRequest = false;
|
|
||||||
readonly affectsResponse = true;
|
|
||||||
|
|
||||||
readonly newSchema = V1_1_CustomerResponseSchema;
|
|
||||||
readonly oldSchema = V1_0_CustomerResponseSchema;
|
|
||||||
|
|
||||||
// Response: V1_1 merged → V1_0 split
|
|
||||||
transformResponse({
|
|
||||||
input,
|
|
||||||
}: {
|
|
||||||
input: z.infer<typeof V1_1_CustomerResponseSchema>;
|
|
||||||
}): z.infer<typeof V1_0_CustomerResponseSchema> {
|
|
||||||
const {
|
|
||||||
features,
|
|
||||||
products = [],
|
|
||||||
invoices,
|
|
||||||
trials_used,
|
|
||||||
...customerFields
|
|
||||||
} = input;
|
|
||||||
|
|
||||||
return {
|
|
||||||
customer: customerFields,
|
|
||||||
products: products.filter((p) => !p.is_add_on),
|
|
||||||
add_ons: products.filter((p) => p.is_add_on),
|
|
||||||
entitlements: Array.isArray(features)
|
|
||||||
? features
|
|
||||||
: Object.values(features),
|
|
||||||
invoices: invoices || [],
|
|
||||||
trials_used,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
export const APICusReferralSchema = z.object({
|
export const ApiCusReferralSchema = z.object({
|
||||||
program_id: z.string(),
|
program_id: z.string(),
|
||||||
customer: z.object({
|
customer: z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
@@ -11,4 +11,4 @@ export const APICusReferralSchema = z.object({
|
|||||||
created_at: z.number(),
|
created_at: z.number(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type APICusReferralResponse = z.infer<typeof APICusReferralSchema>;
|
export type ApiCusReferralResponse = z.infer<typeof ApiCusReferralSchema>;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { APIDiscountSchema } from "@api/others/apiDiscount.js";
|
import { ApiDiscountSchema } from "@api/others/apiDiscount.js";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
export const APICusUpcomingInvoiceSchema = z.object({
|
export const ApiCusUpcomingInvoiceSchema = z.object({
|
||||||
lines: z.array(
|
lines: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
product_id: z.string().nullish(),
|
product_id: z.string().nullish(),
|
||||||
@@ -9,10 +9,10 @@ export const APICusUpcomingInvoiceSchema = z.object({
|
|||||||
amount: z.number(),
|
amount: z.number(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
discounts: z.array(APIDiscountSchema),
|
discounts: z.array(ApiDiscountSchema),
|
||||||
subtotal: z.number(),
|
subtotal: z.number(),
|
||||||
total: z.number(),
|
total: z.number(),
|
||||||
currency: z.string(),
|
currency: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type APICusUpcomingInvoice = z.infer<typeof APICusUpcomingInvoiceSchema>;
|
export type ApiCusUpcomingInvoice = z.infer<typeof ApiCusUpcomingInvoiceSchema>;
|
||||||
|
|||||||
@@ -1,17 +1,6 @@
|
|||||||
import { EntInterval } from "@models/productModels/entModels/entEnums.js";
|
import { EntInterval } from "@models/productModels/entModels/entEnums.js";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
// const res: any = {
|
|
||||||
// feature_id: ent.feature.id,
|
|
||||||
// unlimited: isBoolean ? undefined : unlimited,
|
|
||||||
// interval: isBoolean || unlimited ? null : ent.interval || undefined,
|
|
||||||
// balance: isBoolean ? undefined : unlimited ? null : 0,
|
|
||||||
// total: isBoolean || unlimited ? undefined : 0,
|
|
||||||
// adjustment: isBoolean || unlimited ? undefined : 0,
|
|
||||||
// used: isBoolean ? undefined : unlimited ? null : 0,
|
|
||||||
// unused: 0,
|
|
||||||
// };
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ApiCusFeatureV0Schema - The very first version of the customer feature API model
|
* ApiCusFeatureV0Schema - The very first version of the customer feature API model
|
||||||
* Minimal fields: only feature_id, unlimited, interval, balance, used
|
* Minimal fields: only feature_id, unlimited, interval, balance, used
|
||||||
@@ -25,31 +14,3 @@ export const ApiCusFeatureV0Schema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type ApiCusFeatureV0 = z.infer<typeof ApiCusFeatureV0Schema>;
|
export type ApiCusFeatureV0 = z.infer<typeof ApiCusFeatureV0Schema>;
|
||||||
|
|
||||||
/**
|
|
||||||
* EXAMPLES V0:
|
|
||||||
*
|
|
||||||
* Boolean Feature:
|
|
||||||
* {
|
|
||||||
* "feature_id": "figma-integration",
|
|
||||||
* "interval": null
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* Unlimited Feature:
|
|
||||||
* {
|
|
||||||
* "feature_id": "apply-code",
|
|
||||||
* "unlimited": true,
|
|
||||||
* "interval": null,
|
|
||||||
* "balance": null,
|
|
||||||
* "used": null
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* Regular/Metered Feature:
|
|
||||||
* {
|
|
||||||
* "feature_id": "chat-messages",
|
|
||||||
* "unlimited": false,
|
|
||||||
* "interval": "month",
|
|
||||||
* "balance": 591,
|
|
||||||
* "used": 409
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
|
|||||||
@@ -1,71 +1,10 @@
|
|||||||
import { EntInterval } from "@models/productModels/entModels/entEnums.js";
|
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
import { ApiCusFeatureV0Schema } from "./apiCusFeatureV0.js";
|
||||||
|
|
||||||
/**
|
export const ApiCusFeatureV1Schema = ApiCusFeatureV0Schema.extend({
|
||||||
* ApiCusFeatureV1Schema - Second version of the customer feature API model
|
|
||||||
* Added: next_reset_at, allowance, usage_limit fields
|
|
||||||
*
|
|
||||||
* FEATURE TYPES AND THEIR RESPONSE STRUCTURES:
|
|
||||||
*
|
|
||||||
* 1. BOOLEAN FEATURES:
|
|
||||||
* - Simple on/off features (e.g., "figma-integration", "apply-code")
|
|
||||||
* - interval: null (always null for boolean features)
|
|
||||||
* - Other fields (unlimited, balance, used) are omitted/undefined
|
|
||||||
*
|
|
||||||
* 2. UNLIMITED FEATURES:
|
|
||||||
* - Features with no usage limits (e.g., "deepseek-messages")
|
|
||||||
* - unlimited: true
|
|
||||||
* - interval: null (no reset period needed)
|
|
||||||
* - balance: null (no balance to track)
|
|
||||||
* - used: null (no usage counting)
|
|
||||||
*
|
|
||||||
* 3. REGULAR (METERED) FEATURES:
|
|
||||||
* - Features with usage limits and tracking (e.g., "chat-messages")
|
|
||||||
* - unlimited: false
|
|
||||||
* - interval: "month" | "year" | "week" | "day" (reset period)
|
|
||||||
* - balance: number (remaining usage)
|
|
||||||
* - used: number (amount consumed)
|
|
||||||
*/
|
|
||||||
export const ApiCusFeatureV1Schema = z.object({
|
|
||||||
feature_id: z.string(),
|
|
||||||
unlimited: z.boolean().nullish(),
|
|
||||||
interval: z.enum(EntInterval).nullish(),
|
|
||||||
balance: z.number().nullish(),
|
|
||||||
used: z.number().nullish(),
|
|
||||||
next_reset_at: z.number().nullish(),
|
next_reset_at: z.number().nullish(),
|
||||||
allowance: z.number().nullish(),
|
allowance: z.number().nullish(),
|
||||||
usage_limit: z.number().nullish(),
|
usage_limit: z.number().nullish(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ApiCusFeatureV1 = z.infer<typeof ApiCusFeatureV1Schema>;
|
export type ApiCusFeatureV1 = z.infer<typeof ApiCusFeatureV1Schema>;
|
||||||
|
|
||||||
/**
|
|
||||||
* EXAMPLES V1:
|
|
||||||
*
|
|
||||||
* Boolean Feature:
|
|
||||||
* {
|
|
||||||
* "feature_id": "pro-access",
|
|
||||||
* "interval": null
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* Unlimited Feature:
|
|
||||||
* {
|
|
||||||
* "feature_id": "projects",
|
|
||||||
* "unlimited": true,
|
|
||||||
* "interval": null,
|
|
||||||
* "balance": null,
|
|
||||||
* "used": null
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* Regular/Metered Feature:
|
|
||||||
* {
|
|
||||||
* "feature_id": "translated-words",
|
|
||||||
* "unlimited": false,
|
|
||||||
* "interval": "month",
|
|
||||||
* "balance": -306981,
|
|
||||||
* "used": 326981,
|
|
||||||
* "next_reset_at": 1760549553168,
|
|
||||||
* "allowance": 20000,
|
|
||||||
* "usage_limit": 20000
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { ApiProductItemSchema } from "@api/products/apiProductItem.js";
|
import { ApiProductItemSchema } from "@api/products/apiProductItem.js";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
export const APICusProductSchema = z.object({
|
export const ApiCusProductSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string().nullable(),
|
name: z.string().nullable(),
|
||||||
group: z.string().nullable(),
|
group: z.string().nullable(),
|
||||||
status: z.enum(["active", "expired", "scheduled", "trialing"]),
|
status: z.enum(["active", "expired", "scheduled", "trialing", "past_due"]),
|
||||||
|
|
||||||
canceled_at: z.number().nullish(),
|
canceled_at: z.number().nullish(),
|
||||||
started_at: z.number(),
|
started_at: z.number(),
|
||||||
@@ -24,4 +24,4 @@ export const APICusProductSchema = z.object({
|
|||||||
quantity: z.number().optional(),
|
quantity: z.number().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type APICusProduct = z.infer<typeof APICusProductSchema>;
|
export type ApiCusProduct = z.infer<typeof ApiCusProductSchema>;
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import type { ApiProductItemSchema } from "@api/products/apiProductItem.js";
|
||||||
|
import { ApiVersion } from "@api/versionUtils/ApiVersion.js";
|
||||||
|
import {
|
||||||
|
AffectedResource,
|
||||||
|
defineVersionChange,
|
||||||
|
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
|
||||||
|
import { TierInfinite } from "@models/productV2Models/productItemModels/productItemModels.js";
|
||||||
|
import {
|
||||||
|
isFeatureItem,
|
||||||
|
isPriceItem,
|
||||||
|
} from "@utils/productDisplayUtils/getItemType.js";
|
||||||
|
import { notNullish } from "@utils/utils.js";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
import type { CusProductLegacyData } from "../cusProductLegacyData.js";
|
||||||
|
import { ApiCusProductV1Schema } from "../previousVersions/apiCusProductV1.js";
|
||||||
|
import { ApiCusProductV2Schema } from "../previousVersions/apiCusProductV2.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform product from V2 format to V1 format
|
||||||
|
* Exported so it can be reused in other transformations (e.g., V0_2_CustomerChange)
|
||||||
|
*/
|
||||||
|
export const transformItemToPrice = ({
|
||||||
|
item,
|
||||||
|
}: {
|
||||||
|
item: z.infer<typeof ApiProductItemSchema>;
|
||||||
|
}) => {
|
||||||
|
const singleTier =
|
||||||
|
isPriceItem(item) ||
|
||||||
|
(item.included_usage === 0 && (item.price || item.tiers?.length === 1));
|
||||||
|
|
||||||
|
if (singleTier) {
|
||||||
|
return {
|
||||||
|
amount: item.price || item.tiers?.[0].amount || 0,
|
||||||
|
interval: item.interval,
|
||||||
|
quantity: item.quantity,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Add allowance to tiers
|
||||||
|
const allowance = item.included_usage as number;
|
||||||
|
let tiers: {
|
||||||
|
to: number | "inf";
|
||||||
|
amount: number;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
if (notNullish(allowance) && allowance > 0) {
|
||||||
|
tiers = [
|
||||||
|
{
|
||||||
|
to: allowance,
|
||||||
|
amount: 0,
|
||||||
|
},
|
||||||
|
...(item.tiers
|
||||||
|
? item.tiers.map((tier) => {
|
||||||
|
const isLastTier = tier.to === -1 || tier.to === TierInfinite;
|
||||||
|
return {
|
||||||
|
to: isLastTier ? tier.to : (tier.to as number) + allowance,
|
||||||
|
amount: tier.amount,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: item.price
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
to: -1,
|
||||||
|
amount: item.price || 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
tiers = item.tiers
|
||||||
|
? item.tiers.map((tier) => {
|
||||||
|
const isLastTier = tier.to === -1 || tier.to === TierInfinite;
|
||||||
|
return {
|
||||||
|
to: isLastTier ? tier.to : (tier.to as number) + allowance,
|
||||||
|
amount: tier.amount,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: item.price
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
to: -1,
|
||||||
|
amount: item.price || 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tiers: tiers,
|
||||||
|
name: "",
|
||||||
|
quantity: item.quantity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export function transformCusProductV2ToV1({
|
||||||
|
input,
|
||||||
|
legacyData,
|
||||||
|
}: {
|
||||||
|
input: z.infer<typeof ApiCusProductV2Schema>;
|
||||||
|
legacyData?: CusProductLegacyData;
|
||||||
|
}): z.infer<typeof ApiCusProductV1Schema> {
|
||||||
|
const v2CusProduct = input;
|
||||||
|
|
||||||
|
const prices =
|
||||||
|
v2CusProduct.items
|
||||||
|
?.filter((i) => !isFeatureItem(i))
|
||||||
|
.map((i) => transformItemToPrice({ item: i })) || [];
|
||||||
|
|
||||||
|
const subId = legacyData?.subscription_id;
|
||||||
|
|
||||||
|
const v1CusProduct = {
|
||||||
|
id: v2CusProduct.id,
|
||||||
|
name: v2CusProduct.name,
|
||||||
|
group: v2CusProduct.group || "",
|
||||||
|
status: v2CusProduct.status,
|
||||||
|
created_at: v2CusProduct.started_at, // legacy field..., just use started at...
|
||||||
|
canceled_at: v2CusProduct.canceled_at,
|
||||||
|
|
||||||
|
processor: {
|
||||||
|
type: "stripe",
|
||||||
|
subscription_id: null,
|
||||||
|
},
|
||||||
|
subscription_ids: subId ? [subId] : [],
|
||||||
|
|
||||||
|
prices: prices,
|
||||||
|
starts_at: v2CusProduct.started_at,
|
||||||
|
|
||||||
|
current_period_end: v2CusProduct.current_period_end,
|
||||||
|
current_period_start: v2CusProduct.current_period_start,
|
||||||
|
} as z.infer<typeof ApiCusProductV1Schema>;
|
||||||
|
|
||||||
|
return v1CusProduct;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CusProductLegacyDataSchema = z.object({
|
||||||
|
subscription_id: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const V0_2_CusProductChange = defineVersionChange({
|
||||||
|
newVersion: ApiVersion.V1_1, // Breaking change introduced in V1_1
|
||||||
|
oldVersion: ApiVersion.V0_2, // Applied when targetVersion <= V0_2
|
||||||
|
description: ["Customer product response revamped to fit ProductV2 schema"],
|
||||||
|
|
||||||
|
affectedResources: [AffectedResource.CusProduct],
|
||||||
|
newSchema: ApiCusProductV2Schema,
|
||||||
|
oldSchema: ApiCusProductV1Schema,
|
||||||
|
legacyDataSchema: CusProductLegacyDataSchema,
|
||||||
|
|
||||||
|
// Response: V1.1+ (V2) → V0_2 (V1)
|
||||||
|
transformResponse: transformCusProductV2ToV1,
|
||||||
|
});
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import { APICusProductSchema } from "@api/customers/components/apiCusProduct.js";
|
|
||||||
import { ApiVersion } from "@api/versionUtils/ApiVersion.js";
|
|
||||||
import {
|
|
||||||
AffectedResource,
|
|
||||||
VersionChange,
|
|
||||||
} from "@api/versionUtils/versionChangeUtils/VersionChange.js";
|
|
||||||
import type { z } from "zod/v4";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* V0_2: Customer products gained 'items' field
|
|
||||||
*
|
|
||||||
* V0_2+ format: Has items field with product features
|
|
||||||
* V0_1 format: No items field, no current_period_end/start
|
|
||||||
*/
|
|
||||||
|
|
||||||
// V0_2+ product schema (with items)
|
|
||||||
const V0_2_CusProductSchema = APICusProductSchema;
|
|
||||||
|
|
||||||
// V0_1 product schema (without items and period fields)
|
|
||||||
const V0_1_CusProductSchema = APICusProductSchema.omit({
|
|
||||||
items: true,
|
|
||||||
current_period_end: true,
|
|
||||||
current_period_start: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
export class V0_2_ProductItems extends VersionChange<
|
|
||||||
typeof V0_2_CusProductSchema,
|
|
||||||
typeof V0_1_CusProductSchema
|
|
||||||
> {
|
|
||||||
readonly version = ApiVersion.V0_2;
|
|
||||||
readonly description = "Products gained 'items' field";
|
|
||||||
readonly affectedResources = [AffectedResource.CusProduct];
|
|
||||||
readonly affectsRequest = false;
|
|
||||||
readonly affectsResponse = true;
|
|
||||||
|
|
||||||
readonly newSchema = V0_2_CusProductSchema;
|
|
||||||
readonly oldSchema = V0_1_CusProductSchema;
|
|
||||||
|
|
||||||
transformResponse({
|
|
||||||
input,
|
|
||||||
}: {
|
|
||||||
input: z.infer<typeof V0_2_CusProductSchema>;
|
|
||||||
}): z.infer<typeof V0_1_CusProductSchema> {
|
|
||||||
// Remove items and period fields for V0_1
|
|
||||||
// biome-ignore lint/correctness/noUnusedVariables: Using destructuring to omit fields
|
|
||||||
const { items, current_period_end, current_period_start, ...rest } = input;
|
|
||||||
|
|
||||||
return rest;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
7
shared/api/customers/cusProducts/cusProductLegacyData.ts
Normal file
7
shared/api/customers/cusProducts/cusProductLegacyData.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const CusProductLegacyDataSchema = z.object({
|
||||||
|
subscription_id: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CusProductLegacyData = z.infer<typeof CusProductLegacyDataSchema>;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const ApiCusProductV0PriceSchema = z.object({
|
||||||
|
amount: z.number(),
|
||||||
|
interval: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ApiCusProductV0Schema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string().nullable(),
|
||||||
|
group: z.string().nullable(),
|
||||||
|
status: z.enum(["active", "expired", "scheduled", "trialing", "past_due"]),
|
||||||
|
created_at: z.number(),
|
||||||
|
canceled_at: z.number().nullish(),
|
||||||
|
|
||||||
|
processor: z
|
||||||
|
.object({
|
||||||
|
type: z.string(),
|
||||||
|
subscription_id: z.string().nullish(),
|
||||||
|
})
|
||||||
|
.nullish(),
|
||||||
|
|
||||||
|
subscription_ids: z.array(z.string()).default([]),
|
||||||
|
prices: z.array(z.any()).default([]),
|
||||||
|
|
||||||
|
starts_at: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ApiCusProductV0 = z.infer<typeof ApiCusProductV0Schema>;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
import { ApiCusProductV0Schema } from "./apiCusProductV0.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ApiCusProductV1Schema - Customer Product schema for API V1.0 (pre V0_2)
|
||||||
|
*
|
||||||
|
* Missing fields compared to V2:
|
||||||
|
* - items: Product items were added in V0_2
|
||||||
|
* - current_period_start: Period tracking added in V0_2
|
||||||
|
* - current_period_end: Period tracking added in V0_2
|
||||||
|
*/
|
||||||
|
export const ApiCusProductV1Schema = ApiCusProductV0Schema.extend({
|
||||||
|
current_period_end: z.number().nullish(),
|
||||||
|
current_period_start: z.number().nullish(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ApiCusProductV1 = z.infer<typeof ApiCusProductV1Schema>;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { ApiProductItemSchema } from "@api/products/apiProductItem.js";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ApiCusProductV2Schema - Customer Product schema for API V1.1+ (post V0_2)
|
||||||
|
*
|
||||||
|
* Added in V0_2:
|
||||||
|
* - items: Array of product items (features, prices)
|
||||||
|
* - current_period_start: Billing period start timestamp
|
||||||
|
* - current_period_end: Billing period end timestamp
|
||||||
|
*/
|
||||||
|
export const ApiCusProductV2Schema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string().nullable(),
|
||||||
|
group: z.string().nullable(),
|
||||||
|
status: z.enum(["active", "expired", "scheduled", "trialing", "past_due"]),
|
||||||
|
|
||||||
|
canceled_at: z.number().nullish(),
|
||||||
|
started_at: z.number(),
|
||||||
|
is_default: z.boolean(),
|
||||||
|
is_add_on: z.boolean(),
|
||||||
|
version: z.number().nullish(),
|
||||||
|
|
||||||
|
current_period_start: z.number().nullish(),
|
||||||
|
current_period_end: z.number().nullish(),
|
||||||
|
|
||||||
|
entity_id: z.string().nullish(),
|
||||||
|
|
||||||
|
items: z.array(ApiProductItemSchema).nullish(),
|
||||||
|
|
||||||
|
quantity: z.number().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ApiCusProductV2 = z.infer<typeof ApiCusProductV2Schema>;
|
||||||
8
shared/api/customers/customerLegacyData.ts
Normal file
8
shared/api/customers/customerLegacyData.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { z } from "zod/v4";
|
||||||
|
import { CusProductLegacyDataSchema } from "./cusProducts/cusProductLegacyData.js";
|
||||||
|
|
||||||
|
export const CustomerLegacyDataSchema = z.object({
|
||||||
|
cusProductLegacyData: z.record(z.string(), CusProductLegacyDataSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CustomerLegacyData = z.infer<typeof CustomerLegacyDataSchema>;
|
||||||
@@ -1,62 +1,92 @@
|
|||||||
|
import { CusExpand } from "@models/cusModels/cusExpand.js";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
import { EntityDataSchema } from "../common/entityData.js";
|
import { EntityDataSchema } from "../common/entityData.js";
|
||||||
|
import { queryStringArray } from "../common/queryHelpers.js";
|
||||||
|
|
||||||
|
export const GetCustomerQuerySchema = z.object({
|
||||||
|
expand: queryStringArray(z.enum(CusExpand)).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CreateCustomerQuerySchema = z.object({
|
||||||
|
expand: queryStringArray(z.enum(CusExpand)).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const customerId = z.string().refine(
|
||||||
|
(val) => {
|
||||||
|
if (val === "") return false;
|
||||||
|
if (val.includes("@")) return false;
|
||||||
|
if (val.includes(" ")) return false;
|
||||||
|
if (val.includes(".")) return false;
|
||||||
|
return /^[a-zA-Z0-9_-]+$/.test(val);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
error: (issue) => {
|
||||||
|
const input = issue.input as string;
|
||||||
|
if (input === "") return { message: "can't be an empty string" };
|
||||||
|
if (input.includes("@"))
|
||||||
|
return {
|
||||||
|
message:
|
||||||
|
"cannot contain @ symbol. Use only letters, numbers, underscores, and hyphens.",
|
||||||
|
};
|
||||||
|
if (input.includes(" "))
|
||||||
|
return {
|
||||||
|
message:
|
||||||
|
"cannot contain spaces. Use only letters, numbers, underscores, and hyphens.",
|
||||||
|
};
|
||||||
|
if (input.includes("."))
|
||||||
|
return {
|
||||||
|
message:
|
||||||
|
"cannot contain periods. Use only letters, numbers, underscores, and hyphens.",
|
||||||
|
};
|
||||||
|
const invalidChar = input.match(/[^a-zA-Z0-9_-]/)?.[0];
|
||||||
|
return {
|
||||||
|
message: `cannot contain '${invalidChar}'. Use only letters, numbers, underscores, and hyphens.`,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Create Customer Params (based on handlePostCustomer logic)
|
// Create Customer Params (based on handlePostCustomer logic)
|
||||||
export const CreateCustomerParamsSchema = z.object({
|
export const CreateCustomerParamsSchema = z.object({
|
||||||
id: z
|
id: customerId.nullable().meta({
|
||||||
.string()
|
description: "Your unique identifier for the customer",
|
||||||
.refine(
|
example: "cus_123",
|
||||||
(val) => {
|
}),
|
||||||
if (val === "") return false;
|
|
||||||
if (val.includes("@")) return false;
|
|
||||||
if (val.includes(" ")) return false;
|
|
||||||
if (val.includes(".")) return false;
|
|
||||||
return /^[a-zA-Z0-9_-]+$/.test(val);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message:
|
|
||||||
"ID can only contain letters, numbers, underscores, and hyphens",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.nullish()
|
|
||||||
.meta({
|
|
||||||
description: "Your unique identifier for the customer",
|
|
||||||
example: "cus_123",
|
|
||||||
}),
|
|
||||||
name: z.string().nullish().meta({
|
name: z.string().nullish().meta({
|
||||||
description: "Customer's name",
|
description: "Customer's name",
|
||||||
example: "John Doe",
|
example: "John Doe",
|
||||||
}),
|
}),
|
||||||
email: z
|
|
||||||
.email({ message: "not a valid email address" })
|
email: z.email({ message: "not a valid email address" }).nullish().meta({
|
||||||
.or(z.literal(""))
|
description: "Customer's email address",
|
||||||
.nullish()
|
example: "john@example.com",
|
||||||
.meta({
|
}),
|
||||||
description: "Customer's email address",
|
|
||||||
example: "john@example.com",
|
fingerprint: z.string().optional().meta({
|
||||||
}),
|
|
||||||
fingerprint: z.string().nullish().meta({
|
|
||||||
description:
|
description:
|
||||||
"Unique identifier (eg, serial number) to detect duplicate customers and prevent free trial abuse",
|
"Unique identifier (eg, serial number) to detect duplicate customers and prevent free trial abuse",
|
||||||
example: "fp_123abc",
|
example: "fp_123abc",
|
||||||
}),
|
}),
|
||||||
|
|
||||||
metadata: z
|
metadata: z
|
||||||
.record(z.any(), z.any())
|
.record(z.string(), z.any())
|
||||||
.default({})
|
.default({})
|
||||||
.nullish()
|
|
||||||
.meta({
|
.meta({
|
||||||
description: "Additional metadata for the customer",
|
description: "Additional metadata for the customer",
|
||||||
example: { company: "Acme Inc" },
|
example: { company: "Acme Inc" },
|
||||||
}),
|
}),
|
||||||
stripe_id: z.string().nullish().meta({
|
|
||||||
|
stripe_id: z.string().optional().meta({
|
||||||
description: "Stripe customer ID if you already have one",
|
description: "Stripe customer ID if you already have one",
|
||||||
example: "cus_stripe123",
|
example: "cus_stripe123",
|
||||||
}),
|
}),
|
||||||
entity_id: z.string().nullish().meta({
|
|
||||||
|
entity_id: z.string().optional().meta({
|
||||||
description: "Entity ID to associate with the customer",
|
description: "Entity ID to associate with the customer",
|
||||||
example: "entity_123",
|
example: "entity_123",
|
||||||
}),
|
}),
|
||||||
entity_data: EntityDataSchema.nullish().meta({
|
entity_data: EntityDataSchema.optional().meta({
|
||||||
description: "Data for creating an entity",
|
description: "Data for creating an entity",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { CusExpand } from "@models/cusModels/cusExpand.js";
|
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
import { SuccessResponseSchema } from "../common/commonResponses.js";
|
import { SuccessResponseSchema } from "../common/commonResponses.js";
|
||||||
import { ApiCustomerSchema } from "./apiCustomer.js";
|
import { ApiCustomerSchema } from "./apiCustomer.js";
|
||||||
import {
|
import {
|
||||||
CreateCustomerParamsSchema,
|
CreateCustomerParamsSchema,
|
||||||
|
CreateCustomerQuerySchema,
|
||||||
|
GetCustomerQuerySchema,
|
||||||
ListCustomersResponseSchema,
|
ListCustomersResponseSchema,
|
||||||
UpdateCustomerParamsSchema,
|
UpdateCustomerParamsSchema,
|
||||||
} from "./customerOpModels.js";
|
} from "./customerOpModels.js";
|
||||||
@@ -38,9 +39,7 @@ export const customerOps = {
|
|||||||
summary: "Create Customer",
|
summary: "Create Customer",
|
||||||
tags: ["customers"],
|
tags: ["customers"],
|
||||||
requestParams: {
|
requestParams: {
|
||||||
query: z.object({
|
query: CreateCustomerQuerySchema,
|
||||||
expand: z.string().optional(),
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
requestBody: {
|
requestBody: {
|
||||||
content: {
|
content: {
|
||||||
@@ -63,9 +62,7 @@ export const customerOps = {
|
|||||||
path: z.object({
|
path: z.object({
|
||||||
customer_id: z.string(),
|
customer_id: z.string(),
|
||||||
}),
|
}),
|
||||||
query: z.object({
|
query: GetCustomerQuerySchema,
|
||||||
expand: z.array(z.enum(CusExpand)).optional(),
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
"200": {
|
"200": {
|
||||||
|
|||||||
55
shared/api/customers/previousVersions/apiCustomerV0.ts
Normal file
55
shared/api/customers/previousVersions/apiCustomerV0.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { ApiCusFeatureV0Schema } from "@api/customers/cusFeatures/previousVersions/apiCusFeatureV0.js";
|
||||||
|
import { ApiCusProductV0Schema } from "@api/customers/cusProducts/previousVersions/apiCusProductV0.js";
|
||||||
|
import { ApiInvoiceSchema } from "@api/others/apiInvoice.js";
|
||||||
|
import { AppEnv } from "@models/genModels/genEnums.js";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
import { ApiTrialsUsedSchema } from "../apiCustomer.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ApiCustomerV0Schema - Customer response format for API V0_1
|
||||||
|
*
|
||||||
|
* Structure: Split into separate objects
|
||||||
|
* - customer: Core customer fields
|
||||||
|
* - products: Array of non-add-on products
|
||||||
|
* - add_ons: Array of add-on products
|
||||||
|
* - entitlements: Array of customer features (V0 format - minimal fields)
|
||||||
|
* - invoices: Array of customer invoices
|
||||||
|
*
|
||||||
|
* Key differences from V1:
|
||||||
|
* - Features (entitlements) use V0 format with minimal fields (no next_reset_at, allowance, usage_limit)
|
||||||
|
* - Products don't have items or period tracking (same as V1)
|
||||||
|
* - Invoices always included (no expand parameter)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Core customer object (without products/features/invoices)
|
||||||
|
export const ApiCustomerV0CoreSchema = z.object({
|
||||||
|
// Internal fields
|
||||||
|
id: z.string().nullable(),
|
||||||
|
name: z.string().default(""),
|
||||||
|
email: z.string().default(""),
|
||||||
|
fingerprint: z.string().default(""),
|
||||||
|
created_at: z.number(),
|
||||||
|
env: z.enum(AppEnv),
|
||||||
|
|
||||||
|
processor: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
type: z.string(),
|
||||||
|
})
|
||||||
|
.nullish(),
|
||||||
|
|
||||||
|
metadata: z.record(z.any(), z.any()).default({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Split response structure
|
||||||
|
export const ApiCustomerV0Schema = z.object({
|
||||||
|
customer: ApiCustomerV0CoreSchema,
|
||||||
|
products: z.array(ApiCusProductV0Schema),
|
||||||
|
add_ons: z.array(ApiCusProductV0Schema),
|
||||||
|
entitlements: z.array(ApiCusFeatureV0Schema),
|
||||||
|
invoices: z.array(ApiInvoiceSchema),
|
||||||
|
trials_used: z.array(ApiTrialsUsedSchema).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ApiCustomerV0 = z.infer<typeof ApiCustomerV0Schema>;
|
||||||
|
export type ApiCustomerV0Core = z.infer<typeof ApiCustomerV0CoreSchema>;
|
||||||
58
shared/api/customers/previousVersions/apiCustomerV1.ts
Normal file
58
shared/api/customers/previousVersions/apiCustomerV1.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { ApiCusFeatureV1Schema } from "@api/customers/cusFeatures/previousVersions/apiCusFeatureV1.js";
|
||||||
|
import { ApiInvoiceSchema } from "@api/others/apiInvoice.js";
|
||||||
|
import { AppEnv } from "@models/genModels/genEnums.js";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
import { ApiTrialsUsedSchema } from "../apiCustomer.js";
|
||||||
|
import { ApiCusProductV1Schema } from "../cusProducts/previousVersions/apiCusProductV1.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ApiCustomerV1Schema - Customer response format for API V0_2/V1_0/V1_1 (pre V1_2)
|
||||||
|
*
|
||||||
|
* Structure: Split into separate objects
|
||||||
|
* - customer: Core customer fields
|
||||||
|
* - products: Array of non-add-on products
|
||||||
|
* - add_ons: Array of add-on products
|
||||||
|
* - entitlements: Array of customer features (V1 format)
|
||||||
|
* - invoices: Array of customer invoices
|
||||||
|
*
|
||||||
|
* Key differences from V2:
|
||||||
|
* - Split structure instead of merged
|
||||||
|
* - Uses "entitlements" instead of "features"
|
||||||
|
* - Features are ApiCusFeatureV1 (with used/allowance fields)
|
||||||
|
* - Products are ApiCusProductV2 (same as V2, with items/period tracking)
|
||||||
|
* - Invoices always included (no expand parameter)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Core customer object (without products/features/invoices)
|
||||||
|
export const ApiCustomerV1CoreSchema = z.object({
|
||||||
|
// Internal fields
|
||||||
|
id: z.string().nullable(),
|
||||||
|
name: z.string().nullable(),
|
||||||
|
email: z.string().nullable(),
|
||||||
|
fingerprint: z.string().nullable(),
|
||||||
|
internal_id: z.string(),
|
||||||
|
created_at: z.number(),
|
||||||
|
env: z.enum(AppEnv),
|
||||||
|
|
||||||
|
processor: z
|
||||||
|
.object({
|
||||||
|
id: z.string(),
|
||||||
|
type: z.string(),
|
||||||
|
})
|
||||||
|
.nullish(),
|
||||||
|
|
||||||
|
metadata: z.record(z.any(), z.any()).default({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Split response structure
|
||||||
|
export const ApiCustomerV1Schema = z.object({
|
||||||
|
customer: ApiCustomerV1CoreSchema,
|
||||||
|
products: z.array(ApiCusProductV1Schema),
|
||||||
|
add_ons: z.array(ApiCusProductV1Schema),
|
||||||
|
entitlements: z.array(ApiCusFeatureV1Schema),
|
||||||
|
invoices: z.array(ApiInvoiceSchema),
|
||||||
|
trials_used: z.array(ApiTrialsUsedSchema).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ApiCustomerV1 = z.infer<typeof ApiCustomerV1Schema>;
|
||||||
|
export type ApiCustomerV1Core = z.infer<typeof ApiCustomerV1CoreSchema>;
|
||||||
62
shared/api/customers/previousVersions/apiCustomerV2.ts
Normal file
62
shared/api/customers/previousVersions/apiCustomerV2.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { ApiCusReferralSchema } from "@api/customers/components/apiCusReferral.js";
|
||||||
|
import { ApiCusUpcomingInvoiceSchema } from "@api/customers/components/apiCusUpcomingInvoice.js";
|
||||||
|
import { ApiCusFeatureV2Schema } from "@api/customers/cusFeatures/previousVersions/apiCusFeatureV2.js";
|
||||||
|
import { ApiCusProductV2Schema } from "@api/customers/cusProducts/previousVersions/apiCusProductV2.js";
|
||||||
|
import { ApiEntitySchema } from "@api/entities/apiEntity.js";
|
||||||
|
import { ApiCusRewardsSchema } from "@api/models.js";
|
||||||
|
import { ApiInvoiceSchema } from "@api/others/apiInvoice.js";
|
||||||
|
import { AppEnv } from "@models/genModels/genEnums.js";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
export const ApiTrialsUsedV2Schema = z.object({
|
||||||
|
product_id: z.string(),
|
||||||
|
customer_id: z.string(),
|
||||||
|
fingerprint: z.string().nullish(),
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ApiCustomerV2Schema - Customer response format for API V1.1+ (merged format)
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Key differences from V1:
|
||||||
|
* - Merged structure (everything in one object)
|
||||||
|
* - Uses "features" instead of "entitlements"
|
||||||
|
* - Features are ApiCusFeatureV2 (with usage/included_usage fields)
|
||||||
|
* - Products are ApiCusProductV2 (with items field)
|
||||||
|
* - Invoices require explicit expand parameter
|
||||||
|
*/
|
||||||
|
export const ApiCustomerV2Schema = z.object({
|
||||||
|
// Internal fields
|
||||||
|
autumn_id: z.string().nullish(),
|
||||||
|
|
||||||
|
id: z.string().nullable().meta({
|
||||||
|
description: "Your internal ID for the customer",
|
||||||
|
example: "cus_123",
|
||||||
|
}),
|
||||||
|
|
||||||
|
created_at: z.number().meta({
|
||||||
|
description:
|
||||||
|
"The date and time the customer was created in milliseconds since epoch",
|
||||||
|
example: 1717000000,
|
||||||
|
}),
|
||||||
|
|
||||||
|
name: z.string().nullable(),
|
||||||
|
email: z.string().nullable(),
|
||||||
|
fingerprint: z.string().nullable(),
|
||||||
|
stripe_id: z.string().nullable().default(null),
|
||||||
|
env: z.enum(AppEnv),
|
||||||
|
|
||||||
|
products: z.array(ApiCusProductV2Schema),
|
||||||
|
features: z.array(ApiCusFeatureV2Schema),
|
||||||
|
invoices: z.array(ApiInvoiceSchema).optional(),
|
||||||
|
trials_used: z.array(ApiTrialsUsedV2Schema).optional(),
|
||||||
|
|
||||||
|
rewards: ApiCusRewardsSchema.nullish(),
|
||||||
|
metadata: z.record(z.any(), z.any()).default({}),
|
||||||
|
entities: z.array(ApiEntitySchema).optional(),
|
||||||
|
referrals: z.array(ApiCusReferralSchema).optional(),
|
||||||
|
upcoming_invoice: ApiCusUpcomingInvoiceSchema.nullish(),
|
||||||
|
payment_method: z.any().nullish(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type ApiCustomerV2 = z.infer<typeof ApiCustomerV2Schema>;
|
||||||
@@ -1,6 +1,24 @@
|
|||||||
import { EntityResponseSchema } from "@models/cusModels/entityModels/entityResModels.js";
|
import { ApiCusFeatureSchema } from "@api/customers/cusFeatures/apiCusFeature.js";
|
||||||
import type { z } from "zod/v4";
|
import { ApiCusProductSchema } from "@api/customers/cusProducts/apiCusProduct.js";
|
||||||
|
import { ApiInvoiceSchema } from "@api/others/apiInvoice.js";
|
||||||
|
import { AppEnv } from "@models/genModels/genEnums.js";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
// Base schema without .meta() to avoid side effects during imports
|
export const ApiBaseEntitySchema = z.object({
|
||||||
export const APIEntitySchema = EntityResponseSchema;
|
id: z.string().nullable(),
|
||||||
export type APIEntity = z.infer<typeof APIEntitySchema>;
|
name: z.string().nullable(),
|
||||||
|
|
||||||
|
customer_id: z.string().nullish(),
|
||||||
|
feature_id: z.string().nullish(),
|
||||||
|
|
||||||
|
created_at: z.number(),
|
||||||
|
env: z.enum(AppEnv),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ApiEntitySchema = ApiBaseEntitySchema.extend({
|
||||||
|
products: z.array(ApiCusProductSchema).optional(),
|
||||||
|
features: z.record(z.string(), ApiCusFeatureSchema).optional(),
|
||||||
|
invoices: z.array(ApiInvoiceSchema).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type EntityResponse = z.infer<typeof ApiEntitySchema>;
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { EntityExpand } from "@models/cusModels/entityModels/entityExpand.js";
|
import { EntityExpand } from "@models/cusModels/entityModels/entityExpand.js";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
import { SuccessResponseSchema } from "../common/commonResponses.js";
|
import { SuccessResponseSchema } from "../common/commonResponses.js";
|
||||||
import { APIEntitySchema } from "./apiEntity.js";
|
import { queryStringArray } from "../common/queryHelpers.js";
|
||||||
|
import { ApiEntitySchema } from "./apiEntity.js";
|
||||||
import { CreateEntityParamsSchema } from "./entityOpModels.js";
|
import { CreateEntityParamsSchema } from "./entityOpModels.js";
|
||||||
|
|
||||||
// Register schema with .meta() for OpenAPI spec generation
|
// Register schema with .meta() for OpenAPI spec generation
|
||||||
const APIEntityWithMeta = APIEntitySchema.meta({
|
const ApiEntityWithMeta = ApiEntitySchema.meta({
|
||||||
id: "Entity",
|
id: "Entity",
|
||||||
description: "Entity object returned by the API",
|
description: "Entity object returned by the API",
|
||||||
});
|
});
|
||||||
@@ -28,7 +29,7 @@ export const entityOps = {
|
|||||||
responses: {
|
responses: {
|
||||||
"200": {
|
"200": {
|
||||||
description: "200 OK",
|
description: "200 OK",
|
||||||
content: { "application/json": { schema: APIEntityWithMeta } },
|
content: { "application/json": { schema: ApiEntityWithMeta } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -43,13 +44,13 @@ export const entityOps = {
|
|||||||
entity_id: z.string(),
|
entity_id: z.string(),
|
||||||
}),
|
}),
|
||||||
query: z.object({
|
query: z.object({
|
||||||
expand: z.array(z.enum(EntityExpand)).optional(),
|
expand: queryStringArray(z.enum(EntityExpand)).optional(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
"200": {
|
"200": {
|
||||||
description: "200 OK",
|
description: "200 OK",
|
||||||
content: { "application/json": { schema: APIEntityWithMeta } },
|
content: { "application/json": { schema: ApiEntityWithMeta } },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user