working on get api cus feature
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
EntInterval,
|
||||
type FullCusEntWithProduct,
|
||||
type FullEntitlement,
|
||||
getStartingBalance,
|
||||
type Organization,
|
||||
type ResetCusEnt,
|
||||
} from "@autumn/shared";
|
||||
@@ -15,10 +16,7 @@ import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js";
|
||||
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
|
||||
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
|
||||
import {
|
||||
getRelatedCusPrice,
|
||||
getResetBalance,
|
||||
} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
|
||||
import { getRelatedCusPrice } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
|
||||
import { getRolloverUpdates } from "@/internal/customers/cusProducts/cusEnts/cusRollovers/rolloverUtils.js";
|
||||
import { getResetBalancesUpdate } from "@/internal/customers/cusProducts/cusEnts/groupByUtils.js";
|
||||
import { CusPriceService } from "@/internal/customers/cusProducts/cusPrices/CusPriceService.js";
|
||||
@@ -61,7 +59,10 @@ const checkSubAnchor = async ({
|
||||
const org = cusProduct.product.org as Organization;
|
||||
|
||||
const stripeCli = createStripeCli({ org, env });
|
||||
if (!cusProduct.subscription_ids || cusProduct.subscription_ids.length == 0) {
|
||||
if (
|
||||
!cusProduct.subscription_ids ||
|
||||
cusProduct.subscription_ids.length === 0
|
||||
) {
|
||||
return nextResetAt;
|
||||
}
|
||||
|
||||
@@ -164,7 +165,7 @@ export const resetCustomerEntitlement = async ({
|
||||
const ent = cusEnt.entitlement as FullEntitlement;
|
||||
|
||||
if (
|
||||
ent.allowance_type == AllowanceType.Fixed &&
|
||||
ent.allowance_type === AllowanceType.Fixed &&
|
||||
shortDurations.includes(ent.interval as EntInterval)
|
||||
) {
|
||||
return await handleShortDurationCusEnt({
|
||||
@@ -232,9 +233,9 @@ export const resetCustomerEntitlement = async ({
|
||||
return;
|
||||
}
|
||||
|
||||
const resetBalance = getResetBalance({
|
||||
const resetBalance = getStartingBalance({
|
||||
entitlement: cusEnt.entitlement,
|
||||
options: entOptions,
|
||||
options: entOptions || undefined,
|
||||
relatedPrice: undefined,
|
||||
productQuantity: cusEnt.customer_product.quantity,
|
||||
});
|
||||
|
||||
23
server/src/external/stripe/utils.ts
vendored
23
server/src/external/stripe/utils.ts
vendored
@@ -23,13 +23,13 @@ export const createStripeCli = ({
|
||||
legacyVersion?: boolean;
|
||||
}) => {
|
||||
const encrypted =
|
||||
env == AppEnv.Sandbox
|
||||
env === AppEnv.Sandbox
|
||||
? org.stripe_config?.test_api_key
|
||||
: org.stripe_config?.live_api_key;
|
||||
|
||||
if (!encrypted) {
|
||||
throw new RecaseError({
|
||||
message: `Please connect your Stripe ${env == AppEnv.Sandbox ? "test" : "live"} secret key. You can find it here: https://dashboard.stripe.com${env == AppEnv.Sandbox ? "/test" : ""}/apikeys`,
|
||||
message: `Please connect your Stripe ${env === AppEnv.Sandbox ? "test" : "live"} secret key. You can find it here: https://dashboard.stripe.com${env === AppEnv.Sandbox ? "/test" : ""}/apikeys`,
|
||||
code: ErrCode.StripeConfigNotFound,
|
||||
statusCode: 400,
|
||||
});
|
||||
@@ -66,8 +66,8 @@ export const calculateMetered1Price = ({
|
||||
for (let i = 0; i < usageConfig.usage_tiers.length; i++) {
|
||||
const tier = usageConfig.usage_tiers[i];
|
||||
|
||||
let amtUsed;
|
||||
if (tier.to == -1 || tier.to == Infinite) {
|
||||
let amtUsed: number;
|
||||
if (tier.to === -1 || tier.to === Infinite) {
|
||||
amtUsed = usage;
|
||||
} else {
|
||||
amtUsed = Math.min(usage, tier.to);
|
||||
@@ -89,14 +89,17 @@ export const subToAutumnInterval = (sub: Stripe.Subscription) => {
|
||||
};
|
||||
}
|
||||
|
||||
if (!recuringItem.price.recurring) {
|
||||
return {
|
||||
interval: BillingInterval.OneOff,
|
||||
intervalCount: 1,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
interval: recuringItem.price.recurring!.interval as BillingInterval,
|
||||
intervalCount: recuringItem.price.recurring!.interval_count || 1,
|
||||
interval: recuringItem.price.recurring.interval as BillingInterval,
|
||||
intervalCount: recuringItem.price.recurring.interval_count || 1,
|
||||
};
|
||||
// return stripeToAutumnInterval({
|
||||
// interval: recuringItem.price.recurring!.interval,
|
||||
// intervalCount: recuringItem.price.recurring!.interval_count,
|
||||
// });
|
||||
};
|
||||
|
||||
// export const stripeToAutumnInterval = ({
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import {
|
||||
type Customer,
|
||||
EntInterval,
|
||||
type FeatureOptions,
|
||||
type FullCusProduct,
|
||||
type FullCustomerPrice,
|
||||
type UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
import type Stripe from "stripe";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
|
||||
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
|
||||
import { RolloverService } from "@/internal/customers/cusProducts/cusEnts/cusRollovers/RolloverService.js";
|
||||
import { getRolloverUpdates } from "@/internal/customers/cusProducts/cusEnts/cusRollovers/rolloverUtils.js";
|
||||
import { getResetBalancesUpdate } from "@/internal/customers/cusProducts/cusEnts/groupByUtils.js";
|
||||
import { getRelatedCusEnt } from "@/internal/customers/cusProducts/cusPrices/cusPriceUtils.js";
|
||||
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
|
||||
import { getEntOptions } from "@/internal/products/prices/priceUtils.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import {
|
||||
Customer,
|
||||
EntInterval,
|
||||
FeatureOptions,
|
||||
FullCusProduct,
|
||||
FullCustomerPrice,
|
||||
UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
import Stripe from "stripe";
|
||||
import { subToPeriodStartEnd } from "../../stripeSubUtils/convertSubUtils.js";
|
||||
|
||||
export const handlePrepaidPrices = async ({
|
||||
@@ -58,20 +58,10 @@ export const handlePrepaidPrices = async ({
|
||||
|
||||
const options = getEntOptions(cusProduct.options, cusEnt.entitlement);
|
||||
|
||||
// const resetBalance = getResetBalance({
|
||||
// entitlement: cusEnt.entitlement,
|
||||
// options: notNullish(options?.upcoming_quantity)
|
||||
// ? {
|
||||
// feature_id: options?.feature_id!,
|
||||
// quantity: options?.upcoming_quantity!,
|
||||
// }
|
||||
// : options,
|
||||
// relatedPrice: cusPrice.price,
|
||||
// });
|
||||
let resetQuantity = options?.upcoming_quantity || options?.quantity!;
|
||||
let config = cusPrice.price.config as UsagePriceConfig;
|
||||
let billingUnits = config.billing_units || 1;
|
||||
let newAllowance =
|
||||
const resetQuantity = options?.upcoming_quantity || options?.quantity!;
|
||||
const config = cusPrice.price.config as UsagePriceConfig;
|
||||
const billingUnits = config.billing_units || 1;
|
||||
const newAllowance =
|
||||
resetQuantity * billingUnits + (cusEnt.entitlement.allowance || 0);
|
||||
|
||||
const resetUpdate = getResetBalancesUpdate({
|
||||
@@ -88,7 +78,7 @@ export const handlePrepaidPrices = async ({
|
||||
|
||||
const ent = cusEnt.entitlement;
|
||||
|
||||
let rolloverUpdate = getRolloverUpdates({
|
||||
const rolloverUpdate = getRolloverUpdates({
|
||||
cusEnt,
|
||||
nextResetAt: end * 1000,
|
||||
});
|
||||
@@ -114,7 +104,7 @@ export const handlePrepaidPrices = async ({
|
||||
});
|
||||
|
||||
if (ent.interval == EntInterval.Lifetime) {
|
||||
let difference = options?.quantity! - options?.upcoming_quantity!;
|
||||
const difference = options?.quantity! - options?.upcoming_quantity!;
|
||||
await CusEntService.decrement({
|
||||
db,
|
||||
id: cusEnt.id,
|
||||
|
||||
@@ -130,7 +130,7 @@ export const handleUsagePrices = async ({
|
||||
});
|
||||
}
|
||||
|
||||
if (relatedCusEnt.entitlement.interval == EntInterval.Lifetime) {
|
||||
if (relatedCusEnt.entitlement.interval === EntInterval.Lifetime) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ export const handleUsagePrices = async ({
|
||||
|
||||
const resetBalancesUpdate = getResetBalancesUpdate({
|
||||
cusEnt: relatedCusEnt,
|
||||
allowance: ent.interval == EntInterval.Lifetime ? 0 : ent.allowance!,
|
||||
allowance: ent.interval === EntInterval.Lifetime ? 0 : ent.allowance!,
|
||||
});
|
||||
|
||||
const { end } = subToPeriodStartEnd({ sub: usageSub });
|
||||
|
||||
@@ -5,6 +5,7 @@ import Stripe from "stripe";
|
||||
import { ZodError } from "zod/v4";
|
||||
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
||||
import RecaseError, { formatZodError } from "@/utils/errorUtils.js";
|
||||
import { matchRoute } from "./middlewareUtils.js";
|
||||
|
||||
/**
|
||||
* Handle special error cases that should use warn instead of error logging
|
||||
@@ -99,6 +100,28 @@ const handleSpecialErrorCases = (
|
||||
);
|
||||
}
|
||||
|
||||
// Special case 6: CustomerNotFound on customer routes
|
||||
const pathname = new URL(url).pathname;
|
||||
if (
|
||||
err instanceof RecaseError &&
|
||||
err.code === ErrCode.CustomerNotFound &&
|
||||
matchRoute({
|
||||
url: pathname,
|
||||
method: c.req.method,
|
||||
pattern: { url: "/customers/:customer_id", method: "GET" },
|
||||
})
|
||||
) {
|
||||
logger.warn(`${err.message}, org: ${ctx.org?.slug || "unknown"}`);
|
||||
return c.json(
|
||||
{
|
||||
message: err.message,
|
||||
code: err.code,
|
||||
env: ctx.env,
|
||||
},
|
||||
404,
|
||||
);
|
||||
}
|
||||
|
||||
// No special case matched
|
||||
return null;
|
||||
};
|
||||
@@ -170,6 +193,7 @@ export const errorMiddleware = (err: Error, c: Context<HonoEnv>) => {
|
||||
}
|
||||
|
||||
// 3. Handle Zod validation errors
|
||||
|
||||
if (err instanceof ZodError) {
|
||||
const formattedError = formatZodError(err);
|
||||
|
||||
|
||||
39
server/src/honoMiddlewares/middlewareUtils.ts
Normal file
39
server/src/honoMiddlewares/middlewareUtils.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Checks if an actual URL matches a route pattern with parameters
|
||||
*
|
||||
* @example
|
||||
* matchRoute({
|
||||
* url: "/customers/cus_123",
|
||||
* method: "GET",
|
||||
* pattern: { url: "/customers/:customer_id", method: "GET" }
|
||||
* }) // Returns true
|
||||
*
|
||||
* matchRoute({
|
||||
* url: "/customers/cus_123/events",
|
||||
* method: "GET",
|
||||
* pattern: { url: "/customers/:customer_id", method: "GET" }
|
||||
* }) // Returns false
|
||||
*/
|
||||
export const matchRoute = ({
|
||||
url,
|
||||
method,
|
||||
pattern,
|
||||
}: {
|
||||
url: string;
|
||||
method: string;
|
||||
pattern: { url: string; method: string };
|
||||
}): boolean => {
|
||||
// Check if method matches
|
||||
if (pattern.method !== method) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Convert route pattern to regex
|
||||
// "/customers/:customer_id" -> "^\/customers\/([^/]+)$"
|
||||
const regexPattern = pattern.url
|
||||
.replace(/:[^/]+/g, "([^/]+)") // Replace :param with capturing group
|
||||
.replace(/\//g, "\\/"); // Escape forward slashes
|
||||
|
||||
const regex = new RegExp(`^${regexPattern}$`);
|
||||
return regex.test(url);
|
||||
};
|
||||
115
server/src/honoMiddlewares/refreshCacheMiddleware.ts
Normal file
115
server/src/honoMiddlewares/refreshCacheMiddleware.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import type { Context, Next } from "hono";
|
||||
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
||||
import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js";
|
||||
import { matchRoute } from "./middlewareUtils.js";
|
||||
|
||||
/**
|
||||
* Route patterns that trigger customer cache deletion
|
||||
*/
|
||||
const cusPrefixedUrls = [
|
||||
{
|
||||
method: "POST",
|
||||
url: "/customers/:customer_id",
|
||||
},
|
||||
{
|
||||
method: "DELETE",
|
||||
url: "/customers/:customer_id",
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
url: "/customers/:customer_id/balances",
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
url: "/customers/:customer_id/entitlements/:customer_entitlement_id",
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
url: "/customers/:customer_id/entities",
|
||||
},
|
||||
{
|
||||
method: "DELETE",
|
||||
url: "/customers/:customer_id/entities/:entity_id",
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
url: "/customers/:customer_id/transfer_product",
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Core routes that trigger cache deletion when customer_id is in body
|
||||
*/
|
||||
const coreUrls = [
|
||||
{
|
||||
method: "POST",
|
||||
url: "/attach",
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
url: "/cancel",
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Hono middleware that clears customer cache after successful responses
|
||||
* for specific routes
|
||||
*/
|
||||
export const refreshCacheMiddleware = async (
|
||||
c: Context<HonoEnv>,
|
||||
next: Next,
|
||||
) => {
|
||||
// Continue with the request
|
||||
await next();
|
||||
|
||||
// Only clear cache on successful responses (2xx status codes)
|
||||
if (c.res.status < 200 || c.res.status >= 300) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = c.get("ctx");
|
||||
const { logger, db, org, env } = ctx;
|
||||
|
||||
const pathname = new URL(c.req.url).pathname.replace("/v1", "");
|
||||
const method = c.req.method;
|
||||
|
||||
// Check if URL matches customer-prefixed patterns
|
||||
const pathMatch = cusPrefixedUrls.find((pattern) =>
|
||||
matchRoute({ url: pathname, method, pattern }),
|
||||
);
|
||||
|
||||
if (pathMatch) {
|
||||
const customerId = c.req.param("customer_id");
|
||||
if (customerId) {
|
||||
logger.info(
|
||||
`Clearing cache for customer ${customerId}, url: ${pathname}`,
|
||||
);
|
||||
await deleteCusCache({
|
||||
db,
|
||||
customerId,
|
||||
org,
|
||||
env,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if URL matches core patterns (attach, cancel)
|
||||
const coreMatch = coreUrls.find((pattern) =>
|
||||
matchRoute({ url: pathname, method, pattern }),
|
||||
);
|
||||
|
||||
if (coreMatch) {
|
||||
// For core URLs, check body for customer_id
|
||||
const body = await c.req.json().catch(() => null);
|
||||
if (body?.customer_id) {
|
||||
logger.info(`Clearing cache for core url ${pathname}`);
|
||||
await deleteCusCache({
|
||||
db,
|
||||
customerId: body.customer_id,
|
||||
org,
|
||||
env,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -5,9 +5,11 @@ import { apiVersionMiddleware } from "./honoMiddlewares/apiVersionMiddleware.js"
|
||||
import { baseMiddleware } from "./honoMiddlewares/baseMiddleware.js";
|
||||
import { errorMiddleware } from "./honoMiddlewares/errorMiddleware.js";
|
||||
import { orgConfigMiddleware } from "./honoMiddlewares/orgConfigMiddleware.js";
|
||||
import { refreshCacheMiddleware } from "./honoMiddlewares/refreshCacheMiddleware.js";
|
||||
import { secretKeyMiddleware } from "./honoMiddlewares/secretKeyMiddleware.js";
|
||||
import { traceMiddleware } from "./honoMiddlewares/traceMiddleware.js";
|
||||
import type { HonoEnv } from "./honoUtils/HonoEnv.js";
|
||||
import { cusRouter } from "./internal/customers/cusRouter.js";
|
||||
import { honoProductRouter } from "./internal/products/productRouter.js";
|
||||
import { auth } from "./utils/auth.js";
|
||||
|
||||
@@ -75,7 +77,11 @@ export const createHonoApp = () => {
|
||||
// Step 5: Org config middleware - allows config overrides via header
|
||||
app.use("/v1/*", orgConfigMiddleware);
|
||||
|
||||
// Step 6: Add pricing middleware, analytics middleware, etc.
|
||||
// Step 6: Refresh cache middleware - clears customer cache after successful mutations
|
||||
app.use("/v1/*", refreshCacheMiddleware);
|
||||
|
||||
// Step 7: Add pricing middleware, analytics middleware, etc.
|
||||
app.route("v1/customers", cusRouter);
|
||||
app.route("v1/products", honoProductRouter);
|
||||
|
||||
// Error handler - must be defined after all routes and middleware
|
||||
|
||||
@@ -7,7 +7,7 @@ import { analyticsRouter } from "../analytics/analyticsRouter.js";
|
||||
import { attachRouter } from "../customers/attach/attachRouter.js";
|
||||
import { handleSetupPayment } from "../customers/attach/handleSetupPayment.js";
|
||||
import cancelRouter from "../customers/cancel/cancelRouter.js";
|
||||
import { cusRouter } from "../customers/cusRouter.js";
|
||||
import { expressCusRouter } from "../customers/cusRouter.js";
|
||||
import { handleCreateBillingPortal } from "../customers/handlers/handleCreateBillingPortal.js";
|
||||
import { featureRouter } from "../features/featureRouter.js";
|
||||
import { internalFeatureRouter } from "../features/internalFeatureRouter.js";
|
||||
@@ -34,7 +34,7 @@ apiRouter.use(pricingMiddleware);
|
||||
apiRouter.use(analyticsMiddleware);
|
||||
apiRouter.use(refreshCacheMiddleware);
|
||||
|
||||
apiRouter.use("/customers", cusRouter);
|
||||
apiRouter.use("/customers", expressCusRouter);
|
||||
apiRouter.use("/invoices", invoiceRouter);
|
||||
apiRouter.use("/products", productRouter);
|
||||
apiRouter.use("/products_beta", productBetaRouter);
|
||||
|
||||
@@ -77,7 +77,7 @@ export class CusService {
|
||||
|
||||
const result = await db.execute(query);
|
||||
|
||||
if (!result || result.length == 0) {
|
||||
if (!result || result.length === 0) {
|
||||
if (allowNotFound) {
|
||||
// @ts-expect-error
|
||||
return null as FullCustomer;
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type EntitlementWithFeature,
|
||||
type FeatureOptions,
|
||||
type FullCustomerEntitlement,
|
||||
getStartingBalance,
|
||||
type Organization,
|
||||
type Price,
|
||||
} from "@autumn/shared";
|
||||
@@ -17,7 +18,6 @@ import { nullish } from "@/utils/genUtils.js";
|
||||
import type { InsertCusProductParams } from "../cusProducts/AttachParams.js";
|
||||
import { CusProductService } from "../cusProducts/CusProductService.js";
|
||||
import { CusEntService } from "../cusProducts/cusEnts/CusEntitlementService.js";
|
||||
import { getResetBalance } from "../cusProducts/cusEnts/cusEntUtils.js";
|
||||
import { initCusEntitlement } from "./initCusEnt.js";
|
||||
|
||||
const updateOneOffExistingEntitlement = async ({
|
||||
@@ -51,7 +51,7 @@ const updateOneOffExistingEntitlement = async ({
|
||||
env: env,
|
||||
});
|
||||
|
||||
const resetBalance = getResetBalance({
|
||||
const resetBalance = getStartingBalance({
|
||||
entitlement,
|
||||
options,
|
||||
relatedPrice,
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
import {
|
||||
AllowanceType,
|
||||
AttachReplaceable,
|
||||
type AttachReplaceable,
|
||||
BillingType,
|
||||
Customer,
|
||||
Entity,
|
||||
EntityBalance,
|
||||
type Customer,
|
||||
type EntitlementWithFeature,
|
||||
type Entity,
|
||||
type EntityBalance,
|
||||
type FeatureOptions,
|
||||
FeatureType,
|
||||
FreeTrial,
|
||||
FullCusProduct,
|
||||
FullCustomerEntitlement,
|
||||
Price,
|
||||
ProductOptions,
|
||||
type FreeTrial,
|
||||
type FullCusProduct,
|
||||
type FullCustomerEntitlement,
|
||||
getStartingBalance,
|
||||
type Price,
|
||||
type ProductOptions,
|
||||
} from "@autumn/shared";
|
||||
|
||||
import { FeatureOptions } from "@autumn/shared";
|
||||
|
||||
import { EntitlementWithFeature } from "@autumn/shared";
|
||||
import { getResetBalance } from "../cusProducts/cusEnts/cusEntUtils.js";
|
||||
import { generateId, notNullish } from "@/utils/genUtils.js";
|
||||
import { getBillingType } from "@/internal/products/prices/priceUtils.js";
|
||||
import { entitlementLinkedToEntity } from "@/internal/api/entities/entityUtils.js";
|
||||
import { initNextResetAt } from "../cusProducts/insertCusProduct/initCusEnt/initNextResetAt.js";
|
||||
import { Decimal } from "decimal.js";
|
||||
import { entitlementLinkedToEntity } from "@/internal/api/entities/entityUtils.js";
|
||||
import { getBillingType } from "@/internal/products/prices/priceUtils.js";
|
||||
import { generateId, notNullish } from "@/utils/genUtils.js";
|
||||
import { initNextResetAt } from "../cusProducts/insertCusProduct/initCusEnt/initNextResetAt.js";
|
||||
|
||||
export const initCusEntEntities = ({
|
||||
entitlement,
|
||||
@@ -90,13 +88,13 @@ const initCusEntBalance = ({
|
||||
return { newBalance: null, newEntities: null };
|
||||
}
|
||||
|
||||
const resetBalance = getResetBalance({
|
||||
const resetBalance = getStartingBalance({
|
||||
entitlement,
|
||||
options,
|
||||
relatedPrice,
|
||||
});
|
||||
|
||||
let newEntities: Record<string, EntityBalance> | null = initCusEntEntities({
|
||||
const newEntities: Record<string, EntityBalance> | null = initCusEntEntities({
|
||||
entitlement,
|
||||
entities,
|
||||
resetBalance,
|
||||
@@ -157,7 +155,7 @@ export const initCusEntitlement = ({
|
||||
(newBalance || 0) -
|
||||
replaceables.filter((r) => r.ent.id === entitlement.id).length;
|
||||
|
||||
let nextResetAtValue = initNextResetAt({
|
||||
const nextResetAtValue = initNextResetAt({
|
||||
entitlement,
|
||||
nextResetAt,
|
||||
// keepResetIntervals,
|
||||
@@ -169,7 +167,7 @@ export const initCusEntitlement = ({
|
||||
});
|
||||
|
||||
// 3. Define expires at (TODO next time...)
|
||||
let isBooleanFeature = entitlement.feature.type === FeatureType.Boolean;
|
||||
const isBooleanFeature = entitlement.feature.type === FeatureType.Boolean;
|
||||
let usageAllowed = false;
|
||||
|
||||
if (
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
cusProductToPrices,
|
||||
ErrCode,
|
||||
type FullCusProduct,
|
||||
getStartingBalance,
|
||||
type UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
import { Decimal } from "decimal.js";
|
||||
@@ -22,7 +23,6 @@ import {
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { notNullish, nullOrUndefined } from "@/utils/genUtils.js";
|
||||
import type { AttachParams } from "../../cusProducts/AttachParams.js";
|
||||
import { getResetBalance } from "../../cusProducts/cusEnts/cusEntUtils.js";
|
||||
import type { AttachFlags } from "../models/AttachFlags.js";
|
||||
import { attachParamToCusProducts } from "./convertAttachParams.js";
|
||||
import { handleMultiAttachErrors } from "./handleAttachErrors/handleMultiAttachErrors.js";
|
||||
@@ -177,7 +177,7 @@ const handleUpdateQuantityErrors = async ({
|
||||
if (
|
||||
curr.entitlement.internal_feature_id === option.internal_feature_id
|
||||
) {
|
||||
const allowance = getResetBalance({
|
||||
const allowance = getStartingBalance({
|
||||
entitlement: curr.entitlement,
|
||||
options: cusProduct.options.find(
|
||||
(o) => o.internal_feature_id === option.internal_feature_id,
|
||||
|
||||
@@ -1,38 +1,27 @@
|
||||
import {
|
||||
AllowanceType,
|
||||
AppEnv,
|
||||
BillingType,
|
||||
type AppEnv,
|
||||
CusProductStatus,
|
||||
Customer,
|
||||
EntInterval,
|
||||
entIntervalsDifferent,
|
||||
entIntervalToValue,
|
||||
Entitlement,
|
||||
EntitlementWithFeature,
|
||||
Entity,
|
||||
Feature,
|
||||
FeatureOptions,
|
||||
type Customer,
|
||||
type EntitlementWithFeature,
|
||||
type Entity,
|
||||
type Feature,
|
||||
FeatureType,
|
||||
FullCusProduct,
|
||||
FullCustomerEntitlement,
|
||||
FullCustomerPrice,
|
||||
Organization,
|
||||
Price,
|
||||
UsagePriceConfig,
|
||||
type FullCusProduct,
|
||||
type FullCustomerEntitlement,
|
||||
type FullCustomerPrice,
|
||||
getStartingBalance,
|
||||
type Organization,
|
||||
} from "@autumn/shared";
|
||||
import {
|
||||
getBillingType,
|
||||
getEntOptions,
|
||||
} from "@/internal/products/prices/priceUtils.js";
|
||||
import { logger } from "better-auth";
|
||||
import { Decimal } from "decimal.js";
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
import { getEntOptions } from "@/internal/products/prices/priceUtils.js";
|
||||
import { notNullish, nullish } from "@/utils/genUtils.js";
|
||||
|
||||
import {
|
||||
getEntityBalance,
|
||||
getSummedEntityBalances,
|
||||
} from "./entBalanceUtils.js";
|
||||
import { Decimal } from "decimal.js";
|
||||
import { logger } from "better-auth";
|
||||
|
||||
export const getCusEntMasterBalance = ({
|
||||
cusEnt,
|
||||
@@ -41,18 +30,18 @@ export const getCusEntMasterBalance = ({
|
||||
cusEnt: FullCustomerEntitlement;
|
||||
entities: Entity[];
|
||||
}) => {
|
||||
let ent = cusEnt.entitlement;
|
||||
let feature = ent.feature;
|
||||
const ent = cusEnt.entitlement;
|
||||
const feature = ent.feature;
|
||||
|
||||
if (notNullish(ent.entity_feature_id)) {
|
||||
let totalBalance = Object.values(cusEnt.entities || {}).reduce(
|
||||
const totalBalance = Object.values(cusEnt.entities || {}).reduce(
|
||||
(acc, curr) => {
|
||||
return acc + curr.balance;
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
let totalAdjustment = Object.values(cusEnt.entities || {}).reduce(
|
||||
const totalAdjustment = Object.values(cusEnt.entities || {}).reduce(
|
||||
(acc, curr) => {
|
||||
return acc + curr.adjustment;
|
||||
},
|
||||
@@ -68,7 +57,7 @@ export const getCusEntMasterBalance = ({
|
||||
|
||||
// Get unused count
|
||||
|
||||
let unusedCount =
|
||||
const unusedCount =
|
||||
entities &&
|
||||
entities.filter(
|
||||
(entity) =>
|
||||
@@ -90,9 +79,9 @@ export const getCusEntBalance = ({
|
||||
cusEnt: FullCustomerEntitlement;
|
||||
entityId?: string | null;
|
||||
}) => {
|
||||
let entitlement = cusEnt.entitlement;
|
||||
let ent = cusEnt.entitlement;
|
||||
let feature = ent.feature;
|
||||
const entitlement = cusEnt.entitlement;
|
||||
const ent = cusEnt.entitlement;
|
||||
const feature = ent.feature;
|
||||
|
||||
if (notNullish(entitlement.entity_feature_id)) {
|
||||
if (nullish(entityId)) {
|
||||
@@ -125,10 +114,10 @@ export const getRelatedCusPrice = (
|
||||
cusPrices: FullCustomerPrice[],
|
||||
) => {
|
||||
return cusPrices.find((cusPrice) => {
|
||||
let productMatch =
|
||||
const productMatch =
|
||||
cusPrice.customer_product_id == cusEnt.customer_product_id;
|
||||
|
||||
let entMatch = cusPrice.price.entitlement_id == cusEnt.entitlement.id;
|
||||
const entMatch = cusPrice.price.entitlement_id == cusEnt.entitlement.id;
|
||||
|
||||
return productMatch && entMatch;
|
||||
});
|
||||
@@ -176,44 +165,44 @@ export const updateCusEntInStripe = async ({
|
||||
};
|
||||
|
||||
// Get balance
|
||||
export const getResetBalance = ({
|
||||
entitlement,
|
||||
options,
|
||||
relatedPrice,
|
||||
productQuantity,
|
||||
}: {
|
||||
entitlement: Entitlement;
|
||||
options: FeatureOptions | undefined | null;
|
||||
relatedPrice?: Price | null;
|
||||
productQuantity?: number;
|
||||
}) => {
|
||||
// 1. No related price
|
||||
if (!relatedPrice) {
|
||||
return (entitlement.allowance || 0) * (productQuantity || 1);
|
||||
}
|
||||
// export const getResetBalance = ({
|
||||
// entitlement,
|
||||
// options,
|
||||
// relatedPrice,
|
||||
// productQuantity,
|
||||
// }: {
|
||||
// entitlement: Entitlement;
|
||||
// options: FeatureOptions | undefined | null;
|
||||
// relatedPrice?: Price | null;
|
||||
// productQuantity?: number;
|
||||
// }) => {
|
||||
// // 1. No related price
|
||||
// if (!relatedPrice) {
|
||||
// return (entitlement.allowance || 0) * (productQuantity || 1);
|
||||
// }
|
||||
|
||||
let config = relatedPrice.config as UsagePriceConfig;
|
||||
// let config = relatedPrice.config as UsagePriceConfig;
|
||||
|
||||
let billingType = getBillingType(config);
|
||||
if (billingType != BillingType.UsageInAdvance) {
|
||||
return entitlement.allowance || 0;
|
||||
}
|
||||
// let billingType = getBillingType(config);
|
||||
// if (billingType != BillingType.UsageInAdvance) {
|
||||
// return entitlement.allowance || 0;
|
||||
// }
|
||||
|
||||
let quantity = options?.quantity;
|
||||
let billingUnits = (relatedPrice.config as UsagePriceConfig).billing_units;
|
||||
if (nullish(quantity) || nullish(billingUnits)) {
|
||||
return entitlement.allowance || 0;
|
||||
}
|
||||
// let quantity = options?.quantity;
|
||||
// let billingUnits = (relatedPrice.config as UsagePriceConfig).billing_units;
|
||||
// if (nullish(quantity) || nullish(billingUnits)) {
|
||||
// return entitlement.allowance || 0;
|
||||
// }
|
||||
|
||||
try {
|
||||
return (entitlement.allowance || 0) + quantity! * billingUnits!;
|
||||
} catch (error) {
|
||||
console.log(
|
||||
"WARNING: Failed to return quantity * billing units, returning allowance...",
|
||||
);
|
||||
return entitlement.allowance || 0;
|
||||
}
|
||||
};
|
||||
// try {
|
||||
// return (entitlement.allowance || 0) + quantity! * billingUnits!;
|
||||
// } catch (error) {
|
||||
// console.log(
|
||||
// "WARNING: Failed to return quantity * billing units, returning allowance...",
|
||||
// );
|
||||
// return entitlement.allowance || 0;
|
||||
// }
|
||||
// };
|
||||
|
||||
export const getUnlimitedAndUsageAllowed = ({
|
||||
cusEnts,
|
||||
@@ -276,17 +265,17 @@ export const getFeatureBalance = ({
|
||||
let cusEntBalance = cusEnt.balance!;
|
||||
|
||||
// If entity feature id exists, then it is grouped...
|
||||
let entityFeatureId = cusEnt.entitlement.entity_feature_id;
|
||||
const entityFeatureId = cusEnt.entitlement.entity_feature_id;
|
||||
|
||||
if (notNullish(entityFeatureId)) {
|
||||
if (notNullish(entityId)) {
|
||||
let { balance: entityBalance } = getEntityBalance({
|
||||
const { balance: entityBalance } = getEntityBalance({
|
||||
cusEnt,
|
||||
entityId: entityId!,
|
||||
});
|
||||
cusEntBalance = entityBalance!;
|
||||
} else {
|
||||
let summed = getSummedEntityBalances({
|
||||
const summed = getSummedEntityBalances({
|
||||
cusEnt,
|
||||
});
|
||||
cusEntBalance = summed.balance;
|
||||
@@ -350,7 +339,7 @@ export const getTotalNegativeBalance = ({
|
||||
entities: Record<string, { balance: number; adjustment: number }>;
|
||||
billingUnits?: number;
|
||||
}) => {
|
||||
let entityFeatureId = cusEnt.entitlement.entity_feature_id;
|
||||
const entityFeatureId = cusEnt.entitlement.entity_feature_id;
|
||||
|
||||
if (nullish(entityFeatureId)) {
|
||||
return balance;
|
||||
@@ -373,7 +362,7 @@ export const getTotalNegativeBalance = ({
|
||||
|
||||
if (totalNegative == 0) {
|
||||
if (Object.values(entities).length > 0) {
|
||||
let entityBalances = Object.values(entities).map((e) => e.balance || 0);
|
||||
const entityBalances = Object.values(entities).map((e) => e.balance || 0);
|
||||
return Math.min(...entityBalances);
|
||||
} else {
|
||||
return cusEnt.entitlement.allowance || 0;
|
||||
@@ -402,7 +391,7 @@ export const getExistingUsageFromCusProducts = ({
|
||||
}
|
||||
|
||||
// Existing usage should also include entities
|
||||
let entityUsage = entities.reduce((acc, entity) => {
|
||||
const entityUsage = entities.reduce((acc, entity) => {
|
||||
if (entity.internal_feature_id !== entitlement.internal_feature_id) {
|
||||
return acc;
|
||||
}
|
||||
@@ -417,7 +406,7 @@ export const getExistingUsageFromCusProducts = ({
|
||||
let existingUsage = 0;
|
||||
|
||||
// NOTE: Assuming that feature entitlements are unique to each main product...
|
||||
let existingCusEnt = cusProducts
|
||||
const existingCusEnt = cusProducts
|
||||
?.filter(
|
||||
(cp) =>
|
||||
(cp.status === CusProductStatus.Active ||
|
||||
@@ -445,24 +434,24 @@ export const getExistingUsageFromCusProducts = ({
|
||||
}
|
||||
|
||||
// Get options
|
||||
let cusProduct = cusProducts?.find(
|
||||
const cusProduct = cusProducts?.find(
|
||||
(cp) => cp.id === existingCusEnt.customer_product_id,
|
||||
);
|
||||
let options = getEntOptions(
|
||||
const options = getEntOptions(
|
||||
cusProduct?.options || [],
|
||||
existingCusEnt.entitlement,
|
||||
);
|
||||
let price = getRelatedCusPrice(
|
||||
const price = getRelatedCusPrice(
|
||||
existingCusEnt,
|
||||
cusProduct?.customer_prices || [],
|
||||
);
|
||||
let existingAllowance = getResetBalance({
|
||||
const existingAllowance = getStartingBalance({
|
||||
entitlement: existingCusEnt.entitlement,
|
||||
options: options,
|
||||
options: options || undefined,
|
||||
relatedPrice: price?.price,
|
||||
});
|
||||
|
||||
let { balance, adjustment, count, unused } = getCusEntMasterBalance({
|
||||
const { balance, adjustment, count, unused } = getCusEntMasterBalance({
|
||||
cusEnt: existingCusEnt as any,
|
||||
entities: entities,
|
||||
});
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import {
|
||||
CustomerEntitlement,
|
||||
type CustomerEntitlement,
|
||||
CustomerEntitlementSchema,
|
||||
EntInterval,
|
||||
EntitlementWithFeature,
|
||||
Entity,
|
||||
Feature,
|
||||
type EntitlementWithFeature,
|
||||
type Entity,
|
||||
type Feature,
|
||||
FeatureType,
|
||||
FullCusEntWithFullCusProduct,
|
||||
FullCusProduct,
|
||||
Price,
|
||||
type FullCusEntWithFullCusProduct,
|
||||
type FullCusProduct,
|
||||
getStartingBalance,
|
||||
type Price,
|
||||
sortCusEntsForDeduction,
|
||||
} from "@autumn/shared";
|
||||
|
||||
import { getEntOptions } from "@/internal/products/prices/priceUtils.js";
|
||||
import { performDeductionOnCusEnt } from "@/trigger/updateBalanceTask.js";
|
||||
import { notNullish, nullish } from "@/utils/genUtils.js";
|
||||
import {
|
||||
getRelatedCusPrice,
|
||||
getResetBalance,
|
||||
getUnlimitedAndUsageAllowed,
|
||||
} from "../cusEntUtils.js";
|
||||
import { getEntOptions } from "@/internal/products/prices/priceUtils.js";
|
||||
import { notNullish, nullish } from "@/utils/genUtils.js";
|
||||
import { performDeductionOnCusEnt } from "@/trigger/updateBalanceTask.js";
|
||||
import { sortCusEntsForDeduction } from "@autumn/shared";
|
||||
|
||||
export const getExistingCusEntAndUsage = async ({
|
||||
curCusProduct,
|
||||
@@ -35,7 +34,7 @@ export const getExistingCusEntAndUsage = async ({
|
||||
}
|
||||
|
||||
// 1. If there is only one cus ent, return it and usage
|
||||
let similarCusEnts = curCusProduct.customer_entitlements.filter(
|
||||
const similarCusEnts = curCusProduct.customer_entitlements.filter(
|
||||
(ce) => ce.internal_feature_id === entitlement.internal_feature_id,
|
||||
// &&
|
||||
// ce.entitlement.interval === entitlement.interval
|
||||
@@ -64,7 +63,7 @@ export const getExistingUsages = ({
|
||||
entities: Entity[];
|
||||
features: Feature[];
|
||||
}) => {
|
||||
let usages: Record<
|
||||
const usages: Record<
|
||||
string,
|
||||
{
|
||||
feature_id: string;
|
||||
@@ -76,14 +75,14 @@ export const getExistingUsages = ({
|
||||
}
|
||||
> = {};
|
||||
|
||||
let cusPrices = curCusProduct?.customer_prices || [];
|
||||
const cusPrices = curCusProduct?.customer_prices || [];
|
||||
|
||||
// Get entityUsage
|
||||
for (const entity of entities) {
|
||||
let feature = features.find(
|
||||
const feature = features.find(
|
||||
(f) => f.internal_id === entity.internal_feature_id,
|
||||
);
|
||||
let key = `${feature?.id}-${EntInterval.Lifetime}-1`;
|
||||
const key = `${feature?.id}-${EntInterval.Lifetime}-1`;
|
||||
|
||||
if (!usages[key]) {
|
||||
usages[key] = {
|
||||
@@ -100,12 +99,12 @@ export const getExistingUsages = ({
|
||||
}
|
||||
|
||||
for (const cusEnt of curCusProduct?.customer_entitlements || []) {
|
||||
let ent = cusEnt.entitlement;
|
||||
let key = `${ent.feature_id}-${ent.interval}-${ent.interval_count || 1}`;
|
||||
let feature = ent.feature;
|
||||
const ent = cusEnt.entitlement;
|
||||
const key = `${ent.feature_id}-${ent.interval}-${ent.interval_count || 1}`;
|
||||
const feature = ent.feature;
|
||||
if (feature.type == FeatureType.Boolean) continue;
|
||||
|
||||
let { unlimited, usageAllowed } = getUnlimitedAndUsageAllowed({
|
||||
const { unlimited, usageAllowed } = getUnlimitedAndUsageAllowed({
|
||||
cusEnts: curCusProduct.customer_entitlements,
|
||||
internalFeatureId: ent.internal_feature_id!,
|
||||
});
|
||||
@@ -128,12 +127,12 @@ export const getExistingUsages = ({
|
||||
}
|
||||
|
||||
// 1. To check, does ent options work with multiple features?
|
||||
let relatedCusPrice = getRelatedCusPrice(cusEnt, cusPrices);
|
||||
let options = getEntOptions(curCusProduct.options, ent);
|
||||
const relatedCusPrice = getRelatedCusPrice(cusEnt, cusPrices);
|
||||
const options = getEntOptions(curCusProduct.options, ent);
|
||||
|
||||
let resetBalance = getResetBalance({
|
||||
const resetBalance = getStartingBalance({
|
||||
entitlement: ent,
|
||||
options,
|
||||
options: options || undefined,
|
||||
relatedPrice: relatedCusPrice?.price,
|
||||
// productQuantity: curCusProduct.quantity,
|
||||
});
|
||||
@@ -182,14 +181,14 @@ export const addExistingUsagesToCusEnts = ({
|
||||
return cusEnts;
|
||||
}
|
||||
|
||||
let existingUsages = getExistingUsages({
|
||||
const existingUsages = getExistingUsages({
|
||||
curCusProduct,
|
||||
entities,
|
||||
features,
|
||||
});
|
||||
|
||||
let fullCusEnts = cusEnts.map((ce) => {
|
||||
let entitlement = entitlements.find((e) => e.id === ce.entitlement_id!);
|
||||
const fullCusEnts = cusEnts.map((ce) => {
|
||||
const entitlement = entitlements.find((e) => e.id === ce.entitlement_id!);
|
||||
return { ...ce, entitlement, customer_product: curCusProduct };
|
||||
}) as FullCusEntWithFullCusProduct[];
|
||||
|
||||
@@ -211,7 +210,7 @@ export const addExistingUsagesToCusEnts = ({
|
||||
|
||||
for (const key in existingUsages) {
|
||||
let usage = existingUsages[key].usage;
|
||||
let entityUsages = existingUsages[key].entityUsages;
|
||||
const entityUsages = existingUsages[key].entityUsages;
|
||||
|
||||
const {
|
||||
feature_id = "",
|
||||
@@ -220,22 +219,22 @@ export const addExistingUsagesToCusEnts = ({
|
||||
} = existingUsages[key] || {};
|
||||
|
||||
for (const cusEnt of fullCusEnts) {
|
||||
let ent = cusEnt.entitlement;
|
||||
let fromEntities = existingUsages[key].fromEntities;
|
||||
const ent = cusEnt.entitlement;
|
||||
const fromEntities = existingUsages[key].fromEntities;
|
||||
|
||||
// if (cusEntKey !== key) continue;
|
||||
const isSameFeature = cusEnt.feature_id == feature_id;
|
||||
|
||||
if (!isSameFeature) continue;
|
||||
|
||||
let shouldCarry =
|
||||
const shouldCarry =
|
||||
ent.carry_from_previous || carryExistingUsages || fromEntities;
|
||||
|
||||
if (!shouldCarry) continue;
|
||||
|
||||
if (notNullish(entityUsages)) {
|
||||
for (const entityId in entityUsages) {
|
||||
let { toDeduct, newEntities } = performDeductionOnCusEnt({
|
||||
const { toDeduct, newEntities } = performDeductionOnCusEnt({
|
||||
cusEnt,
|
||||
toDeduct: entityUsages[entityId],
|
||||
allowNegativeBalance: cusEnt.usage_allowed ?? false,
|
||||
@@ -255,7 +254,7 @@ export const addExistingUsagesToCusEnts = ({
|
||||
cusEnt.entities![entityId]!.balance = newEntities![entityId]!.balance;
|
||||
}
|
||||
} else {
|
||||
let { newBalance, toDeduct } = performDeductionOnCusEnt({
|
||||
const { newBalance, toDeduct } = performDeductionOnCusEnt({
|
||||
cusEnt,
|
||||
toDeduct: usage,
|
||||
allowNegativeBalance: cusEnt.usage_allowed ?? false,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { RolloverDeductParams } from "@/trigger/updateBalanceTask.js";
|
||||
import { FullCusEntWithFullCusProduct, Rollover } from "@autumn/shared";
|
||||
import type { FullCusEntWithFullCusProduct, Rollover } from "@autumn/shared";
|
||||
import type { RolloverDeductParams } from "@/trigger/updateBalanceTask.js";
|
||||
import { RolloverService } from "./RolloverService.js";
|
||||
|
||||
export const deductFromCusRollovers = async ({
|
||||
export const deductFromApiCusRollovers = async ({
|
||||
toDeduct,
|
||||
deductParams,
|
||||
cusEnt,
|
||||
@@ -15,19 +15,19 @@ export const deductFromCusRollovers = async ({
|
||||
return toDeduct;
|
||||
}
|
||||
|
||||
let updates = {
|
||||
const updates = {
|
||||
toInsert: [] as Rollover[],
|
||||
toUpdate: [] as Rollover[],
|
||||
};
|
||||
let rollovers = getSortedRollovers({
|
||||
const rollovers = getSortedRollovers({
|
||||
cusEnts: [cusEnt],
|
||||
featureId: deductParams.feature.id,
|
||||
entityId: deductParams.entity?.id,
|
||||
});
|
||||
|
||||
if (deductParams.entity) {
|
||||
for (let rollover of rollovers) {
|
||||
let entityRollover = rollover.entities[deductParams.entity.id];
|
||||
for (const rollover of rollovers) {
|
||||
const entityRollover = rollover.entities[deductParams.entity.id];
|
||||
if (entityRollover) {
|
||||
if (entityRollover.balance >= toDeduct) {
|
||||
entityRollover.balance -= toDeduct;
|
||||
@@ -38,7 +38,7 @@ export const deductFromCusRollovers = async ({
|
||||
break;
|
||||
} else {
|
||||
if (entityRollover.balance > 0) {
|
||||
let deductedAmount = entityRollover.balance;
|
||||
const deductedAmount = entityRollover.balance;
|
||||
toDeduct -= entityRollover.balance;
|
||||
entityRollover.balance = 0;
|
||||
entityRollover.usage += deductedAmount;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FullCustomerEntitlement } from "@autumn/shared";
|
||||
import type { FullCustomerEntitlement } from "@autumn/shared";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
|
||||
export const getResetBalancesUpdate = ({
|
||||
@@ -9,14 +9,14 @@ export const getResetBalancesUpdate = ({
|
||||
allowance?: number;
|
||||
}) => {
|
||||
let update = {};
|
||||
let newBalance = notNullish(allowance)
|
||||
const newBalance = notNullish(allowance)
|
||||
? allowance!
|
||||
: cusEnt.entitlement.allowance || 0;
|
||||
|
||||
let entitlement = cusEnt.entitlement;
|
||||
const entitlement = cusEnt.entitlement;
|
||||
|
||||
if (notNullish(entitlement.entity_feature_id)) {
|
||||
let newEntities = { ...cusEnt.entities };
|
||||
const newEntities = { ...cusEnt.entities };
|
||||
for (const entityId in newEntities) {
|
||||
newEntities[entityId].balance = newBalance;
|
||||
newEntities[entityId].adjustment = 0;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { ErrCode, LegacyVersion } from "@autumn/shared";
|
||||
import { Router } from "express";
|
||||
import { Hono } from "hono";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import { createStripeCusIfNotExists } from "@/external/stripe/stripeCusUtils.js";
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
||||
import { CusSearchService } from "@/internal/customers/CusSearchService.js";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
|
||||
@@ -13,18 +15,18 @@ import { CusService } from "./CusService.js";
|
||||
import { handleAddCouponToCus } from "./handlers/handleAddCouponToCus.js";
|
||||
import { handleCreateBillingPortal } from "./handlers/handleCreateBillingPortal.js";
|
||||
import { handleDeleteCustomer } from "./handlers/handleDeleteCustomer.js";
|
||||
import { handleGetCustomer } from "./handlers/handleGetCustomer.js";
|
||||
import { handleGetCustomerV2 } from "./handlers/handleGetCustomerV2.js";
|
||||
import { handlePostCustomerRequest } from "./handlers/handlePostCustomer.js";
|
||||
import { handleTransferProduct } from "./handlers/handleTransferProduct.js";
|
||||
import { handleUpdateBalances } from "./handlers/handleUpdateBalances.js";
|
||||
import { handleUpdateCustomer } from "./handlers/handleUpdateCustomer.js";
|
||||
import { handleUpdateEntitlement } from "./handlers/handleUpdateEntitlement.js";
|
||||
|
||||
export const cusRouter: Router = Router();
|
||||
export const expressCusRouter: Router = Router();
|
||||
|
||||
cusRouter.get("", handleBatchCustomers);
|
||||
expressCusRouter.get("", handleBatchCustomers);
|
||||
|
||||
cusRouter.post("/all/search", async (req: any, res: any) => {
|
||||
expressCusRouter.post("/all/search", async (req: any, res: any) => {
|
||||
try {
|
||||
const { search, page_size = 50, page = 1, last_item, filters } = req.body;
|
||||
|
||||
@@ -45,120 +47,130 @@ cusRouter.post("/all/search", async (req: any, res: any) => {
|
||||
}
|
||||
});
|
||||
|
||||
cusRouter.post("", handlePostCustomerRequest);
|
||||
expressCusRouter.post("", handlePostCustomerRequest);
|
||||
|
||||
cusRouter.get("/:customer_id", handleGetCustomer);
|
||||
// cusRouter.get("/:customer_id", handleGetCustomer);
|
||||
|
||||
cusRouter.delete("/:customer_id", handleDeleteCustomer);
|
||||
expressCusRouter.delete("/:customer_id", handleDeleteCustomer);
|
||||
|
||||
cusRouter.post("/:customer_id", handleUpdateCustomer);
|
||||
expressCusRouter.post("/:customer_id", handleUpdateCustomer);
|
||||
|
||||
// Update customer entitlement directly
|
||||
cusRouter.post(
|
||||
expressCusRouter.post(
|
||||
"/:customer_id/entitlements/:customer_entitlement_id",
|
||||
handleUpdateEntitlement,
|
||||
);
|
||||
|
||||
cusRouter.post("/:customer_id/balances", handleUpdateBalances);
|
||||
expressCusRouter.post("/:customer_id/balances", handleUpdateBalances);
|
||||
|
||||
// cusRouter.post(
|
||||
// "/customer_products/:customer_product_id",
|
||||
// handleCusProductExpired
|
||||
// );
|
||||
|
||||
cusRouter.get("/:customer_id/billing_portal", async (req: any, res: any) => {
|
||||
try {
|
||||
const returnUrl = req.query.return_url;
|
||||
expressCusRouter.get(
|
||||
"/:customer_id/billing_portal",
|
||||
async (req: any, res: any) => {
|
||||
try {
|
||||
const returnUrl = req.query.return_url;
|
||||
|
||||
const customerId = req.params.customer_id;
|
||||
const customerId = req.params.customer_id;
|
||||
|
||||
const [org, customer] = await Promise.all([
|
||||
OrgService.getFromReq(req),
|
||||
CusService.get({
|
||||
db: req.db,
|
||||
idOrInternalId: customerId,
|
||||
orgId: req.orgId,
|
||||
env: req.env,
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!customer) {
|
||||
throw new RecaseError({
|
||||
message: `Customer ${customerId} not found`,
|
||||
code: ErrCode.CustomerNotFound,
|
||||
statusCode: StatusCodes.NOT_FOUND,
|
||||
});
|
||||
}
|
||||
|
||||
const stripeCli = createStripeCli({ org, env: req.env });
|
||||
|
||||
if (!customer.processor?.id) {
|
||||
let newCus;
|
||||
try {
|
||||
newCus = await createStripeCusIfNotExists({
|
||||
const [org, customer] = await Promise.all([
|
||||
OrgService.getFromReq(req),
|
||||
CusService.get({
|
||||
db: req.db,
|
||||
org,
|
||||
idOrInternalId: customerId,
|
||||
orgId: req.orgId,
|
||||
env: req.env,
|
||||
customer,
|
||||
logger: req.logtail,
|
||||
});
|
||||
} catch (error: any) {
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!customer) {
|
||||
throw new RecaseError({
|
||||
message: `Failed to create Stripe customer`,
|
||||
code: ErrCode.StripeError,
|
||||
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
message: `Customer ${customerId} not found`,
|
||||
code: ErrCode.CustomerNotFound,
|
||||
statusCode: StatusCodes.NOT_FOUND,
|
||||
});
|
||||
} finally {
|
||||
if (!newCus) {
|
||||
}
|
||||
|
||||
const stripeCli = createStripeCli({ org, env: req.env });
|
||||
|
||||
if (!customer.processor?.id) {
|
||||
let newCus;
|
||||
try {
|
||||
newCus = await createStripeCusIfNotExists({
|
||||
db: req.db,
|
||||
org,
|
||||
env: req.env,
|
||||
customer,
|
||||
logger: req.logtail,
|
||||
});
|
||||
} catch (error: any) {
|
||||
throw new RecaseError({
|
||||
message: `Failed to create Stripe customer`,
|
||||
code: ErrCode.StripeError,
|
||||
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
if (!newCus) {
|
||||
throw new RecaseError({
|
||||
message: `Failed to create Stripe customer`,
|
||||
code: ErrCode.StripeError,
|
||||
statusCode: StatusCodes.INTERNAL_SERVER_ERROR,
|
||||
});
|
||||
}
|
||||
|
||||
const portal = await stripeCli.billingPortal.sessions.create({
|
||||
customer: newCus.id,
|
||||
return_url: returnUrl || toSuccessUrl({ org, env: req.env }),
|
||||
});
|
||||
const portal = await stripeCli.billingPortal.sessions.create({
|
||||
customer: newCus.id,
|
||||
return_url: returnUrl || toSuccessUrl({ org, env: req.env }),
|
||||
});
|
||||
|
||||
if (org.api_version >= LegacyVersion.v1_1) {
|
||||
return res.status(200).json({
|
||||
customer_id: customer.id,
|
||||
url: portal.url,
|
||||
});
|
||||
} else {
|
||||
return res.status(200).json({
|
||||
url: portal.url,
|
||||
});
|
||||
if (org.api_version >= LegacyVersion.v1_1) {
|
||||
return res.status(200).json({
|
||||
customer_id: customer.id,
|
||||
url: portal.url,
|
||||
});
|
||||
} else {
|
||||
return res.status(200).json({
|
||||
url: portal.url,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const portal = await stripeCli.billingPortal.sessions.create({
|
||||
customer: customer.processor.id,
|
||||
return_url: returnUrl || toSuccessUrl({ org, env: req.env }),
|
||||
});
|
||||
|
||||
if (org.api_version >= LegacyVersion.v1_1) {
|
||||
res.status(200).json({
|
||||
customer_id: customer.id,
|
||||
url: portal.url,
|
||||
});
|
||||
} else {
|
||||
res.status(200).json({
|
||||
url: portal.url,
|
||||
const portal = await stripeCli.billingPortal.sessions.create({
|
||||
customer: customer.processor.id,
|
||||
return_url: returnUrl || toSuccessUrl({ org, env: req.env }),
|
||||
});
|
||||
|
||||
if (org.api_version >= LegacyVersion.v1_1) {
|
||||
res.status(200).json({
|
||||
customer_id: customer.id,
|
||||
url: portal.url,
|
||||
});
|
||||
} else {
|
||||
res.status(200).json({
|
||||
url: portal.url,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
handleRequestError({ req, error, res, action: "get billing portal" });
|
||||
}
|
||||
} catch (error) {
|
||||
handleRequestError({ req, error, res, action: "get billing portal" });
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
cusRouter.post("/:customer_id/billing_portal", handleCreateBillingPortal);
|
||||
expressCusRouter.post(
|
||||
"/:customer_id/billing_portal",
|
||||
handleCreateBillingPortal,
|
||||
);
|
||||
|
||||
cusRouter.post("/:customer_id/coupons/:coupon_id", handleAddCouponToCus);
|
||||
expressCusRouter.post("/:customer_id/coupons/:coupon_id", handleAddCouponToCus);
|
||||
|
||||
cusRouter.use("/:customer_id/entities", entityRouter);
|
||||
expressCusRouter.use("/:customer_id/entities", entityRouter);
|
||||
|
||||
cusRouter.post("/:customer_id/transfer", handleTransferProduct);
|
||||
expressCusRouter.post("/:customer_id/transfer", handleTransferProduct);
|
||||
|
||||
export const cusRouter = new Hono<HonoEnv>();
|
||||
|
||||
cusRouter.get("/:customer_id", ...handleGetCustomerV2);
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import {
|
||||
AffectedResource,
|
||||
type ApiVersion,
|
||||
ApiVersionClass,
|
||||
applyResponseVersionChanges,
|
||||
CusEntResponseSchema,
|
||||
type Feature,
|
||||
FeatureType,
|
||||
LATEST_VERSION,
|
||||
} from "@autumn/shared";
|
||||
|
||||
/**
|
||||
* Transforms feature balances to API format for requested version
|
||||
*
|
||||
* Latest format (V1_2):
|
||||
* - Object keyed by feature_id
|
||||
* - Has usage/included_usage fields (not used/allowance)
|
||||
*
|
||||
* V1_1:
|
||||
* - Array format
|
||||
* - Has usage/included_usage fields
|
||||
*
|
||||
* V1_0:
|
||||
* - Array format (split response, not in customer object)
|
||||
* - Has used/allowance fields
|
||||
*/
|
||||
export const getApiCusFeature = ({
|
||||
balances,
|
||||
features,
|
||||
apiVersion,
|
||||
}: {
|
||||
balances: any[]; // Raw balances from getCusBalances
|
||||
features: Feature[]; // Feature definitions
|
||||
apiVersion: ApiVersion;
|
||||
}): any => {
|
||||
// Transform balances to latest format (V1_1+: usage/included_usage)
|
||||
const transformedBalances = balances.map((b) => {
|
||||
const isBoolean =
|
||||
features.find((f: Feature) => f.id === b.feature_id)?.type ===
|
||||
FeatureType.Boolean;
|
||||
|
||||
if (b.unlimited || isBoolean) {
|
||||
return b;
|
||||
}
|
||||
|
||||
return CusEntResponseSchema.parse({
|
||||
...b,
|
||||
usage: b.used,
|
||||
included_usage: b.allowance,
|
||||
});
|
||||
});
|
||||
|
||||
// Build in latest format (V1_2: object keyed by feature_id)
|
||||
const featuresObject: Record<string, any> = {};
|
||||
for (const balance of transformedBalances) {
|
||||
featuresObject[balance.feature_id] = balance;
|
||||
}
|
||||
|
||||
// Apply version changes to transform to requested version
|
||||
// V1_2 stays as object
|
||||
// V1_1 → array (via V1_2_FeaturesArrayToObject transform)
|
||||
return applyResponseVersionChanges({
|
||||
input: featuresObject,
|
||||
currentVersion: new ApiVersionClass(LATEST_VERSION),
|
||||
targetVersion: new ApiVersionClass(apiVersion),
|
||||
resource: AffectedResource.CusFeature,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
type ApiCusFeature,
|
||||
ApiFeatureType,
|
||||
type FullCusEntWithFullCusProduct,
|
||||
} from "@autumn/shared";
|
||||
import { getCusFeatureType } from "@/internal/features/featureUtils.js";
|
||||
|
||||
export const getBooleanApiCusFeature = ({
|
||||
cusEnts,
|
||||
}: {
|
||||
cusEnts: FullCusEntWithFullCusProduct[];
|
||||
}): ApiCusFeature => {
|
||||
const feature = cusEnts[0].entitlement.feature;
|
||||
return {
|
||||
id: feature.id,
|
||||
type: ApiFeatureType.Static,
|
||||
name: feature.name,
|
||||
balance: 0,
|
||||
usage: 0,
|
||||
included_usage: 0,
|
||||
next_reset_at: null,
|
||||
unlimited: false,
|
||||
overage_allowed: false,
|
||||
};
|
||||
};
|
||||
|
||||
export const getUnlimitedApiCusFeature = ({
|
||||
cusEnts,
|
||||
unlimited,
|
||||
usageAllowed,
|
||||
}: {
|
||||
cusEnts: FullCusEntWithFullCusProduct[];
|
||||
unlimited: boolean;
|
||||
usageAllowed: boolean;
|
||||
}): ApiCusFeature => {
|
||||
const feature = cusEnts[0].entitlement.feature;
|
||||
|
||||
return {
|
||||
id: feature.id,
|
||||
type: getCusFeatureType({ feature }),
|
||||
name: feature.name,
|
||||
balance: 0,
|
||||
usage: 0,
|
||||
included_usage: 0,
|
||||
next_reset_at: null,
|
||||
unlimited,
|
||||
overage_allowed: usageAllowed,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,259 @@
|
||||
import {
|
||||
AffectedResource,
|
||||
type ApiCusFeatureBreakdown,
|
||||
ApiCusFeatureBreakdownSchema,
|
||||
ApiCusFeatureSchema,
|
||||
type ApiCusRollover,
|
||||
applyResponseVersionChanges,
|
||||
type EntInterval,
|
||||
type Feature,
|
||||
FeatureType,
|
||||
getCusEntBalance,
|
||||
} from "@autumn/shared";
|
||||
import type { FullCustomer } from "@shared/models/cusModels/fullCusModel.js";
|
||||
import type { FullCusEntWithFullCusProduct } from "@shared/models/cusProductModels/cusEntModels/cusEntWithProduct.js";
|
||||
import {
|
||||
cusEntToBalance,
|
||||
cusEntToIncludedUsage,
|
||||
cusEntToKey,
|
||||
cusEntToUsageLimit,
|
||||
} from "@shared/utils/cusEntUtils/convertCusEntUtils.js";
|
||||
import { toApiFeature } from "@shared/utils/featureUtils.js";
|
||||
import { notNullish, sumValues } from "@shared/utils/utils.js";
|
||||
import { Decimal } from "decimal.js";
|
||||
import type { RequestContext } from "@/honoUtils/HonoEnv.js";
|
||||
import { getUnlimitedAndUsageAllowed } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
|
||||
import { getCusFeatureType } from "@/internal/features/featureUtils.js";
|
||||
import { getRolloverFields } from "../../cusFeatureResponseUtils/getCusBalances.js";
|
||||
import {
|
||||
getBooleanApiCusFeature,
|
||||
getUnlimitedApiCusFeature,
|
||||
} from "./apiCusFeatureUtils.js";
|
||||
|
||||
export const cusEntsToInterval = ({
|
||||
cusEnts,
|
||||
}: {
|
||||
cusEnts: FullCusEntWithFullCusProduct[];
|
||||
}): {
|
||||
interval: EntInterval | "multiple" | null;
|
||||
interval_count: number;
|
||||
} => {
|
||||
const cusEntKeys = cusEnts.map((cusEnt) => cusEntToKey({ cusEnt }));
|
||||
const uniqueCusEntKeys = [...new Set(cusEntKeys)];
|
||||
if (uniqueCusEntKeys.length === 1) {
|
||||
return {
|
||||
interval: cusEnts[0].entitlement.interval || null,
|
||||
interval_count: cusEnts[0].entitlement.interval_count,
|
||||
};
|
||||
}
|
||||
|
||||
return { interval: "multiple", interval_count: 0 };
|
||||
};
|
||||
|
||||
const cusEntsToNextResetAt = ({
|
||||
cusEnts,
|
||||
}: {
|
||||
cusEnts: FullCusEntWithFullCusProduct[];
|
||||
}) => {
|
||||
const result = cusEnts.reduce((acc, curr) => {
|
||||
if (curr.next_reset_at && curr.next_reset_at < acc) {
|
||||
return curr.next_reset_at;
|
||||
}
|
||||
return acc;
|
||||
}, Infinity);
|
||||
|
||||
if (result === Infinity) return null;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const cusEntsToRollovers = ({
|
||||
cusEnts,
|
||||
entityId,
|
||||
}: {
|
||||
cusEnts: FullCusEntWithFullCusProduct[];
|
||||
entityId?: string;
|
||||
}): ApiCusRollover[] | undefined => {
|
||||
// If all cus ents no rollover, return undefined
|
||||
|
||||
if (cusEnts.every((cusEnt) => !cusEnt.entitlement.rollover)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return cusEnts
|
||||
.map((cusEnt) => {
|
||||
const rolloverFields = getRolloverFields({ cusEnt, entityId });
|
||||
if (rolloverFields)
|
||||
return rolloverFields.rollovers.map((rollover) => ({
|
||||
balance: rollover.balance,
|
||||
expires_at: rollover.expires_at || 0,
|
||||
}));
|
||||
return [];
|
||||
})
|
||||
.filter(notNullish)
|
||||
.flat();
|
||||
};
|
||||
|
||||
const cusEntsToBreakdown = ({
|
||||
ctx,
|
||||
fullCus,
|
||||
cusEnts,
|
||||
}: {
|
||||
ctx: RequestContext;
|
||||
cusEnts: FullCusEntWithFullCusProduct[];
|
||||
fullCus: FullCustomer;
|
||||
}): ApiCusFeatureBreakdown[] | undefined => {
|
||||
const keyToCusEnts: Record<string, FullCusEntWithFullCusProduct[]> = {};
|
||||
for (const cusEnt of cusEnts) {
|
||||
const key = cusEntToKey({ cusEnt });
|
||||
keyToCusEnts[key] = [...(keyToCusEnts[key] || []), cusEnt];
|
||||
}
|
||||
|
||||
if (Object.keys(keyToCusEnts).length === 1) return undefined;
|
||||
|
||||
const breakdown: ApiCusFeatureBreakdown[] = [];
|
||||
|
||||
for (const key in keyToCusEnts) {
|
||||
const cusEnts = keyToCusEnts[key];
|
||||
|
||||
const feature = cusEnts[0].entitlement.feature;
|
||||
const { interval, interval_count } = cusEntsToInterval({ cusEnts });
|
||||
|
||||
const breakdownItem = getApiCusFeature({
|
||||
ctx,
|
||||
fullCus,
|
||||
cusEnts,
|
||||
feature,
|
||||
});
|
||||
breakdown.push(
|
||||
ApiCusFeatureBreakdownSchema.parse({
|
||||
...breakdownItem,
|
||||
interval,
|
||||
interval_count,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return breakdown;
|
||||
};
|
||||
|
||||
export const getApiCusFeature = ({
|
||||
ctx,
|
||||
fullCus,
|
||||
cusEnts,
|
||||
feature,
|
||||
}: {
|
||||
ctx: RequestContext;
|
||||
fullCus: FullCustomer;
|
||||
cusEnts: FullCusEntWithFullCusProduct[];
|
||||
feature: Feature;
|
||||
}) => {
|
||||
const entityId = fullCus.entity?.id;
|
||||
|
||||
// 1. If feature is boolean
|
||||
if (feature.type === FeatureType.Boolean) {
|
||||
return getBooleanApiCusFeature({
|
||||
cusEnts,
|
||||
});
|
||||
}
|
||||
|
||||
const { unlimited, usageAllowed } = getUnlimitedAndUsageAllowed({
|
||||
cusEnts: cusEnts,
|
||||
internalFeatureId: feature.internal_id,
|
||||
});
|
||||
|
||||
// 2. If feature is unlimited
|
||||
if (unlimited || usageAllowed) {
|
||||
return getUnlimitedApiCusFeature({
|
||||
cusEnts: cusEnts,
|
||||
unlimited,
|
||||
usageAllowed,
|
||||
});
|
||||
}
|
||||
|
||||
const totalBalanceWithRollovers = sumValues(
|
||||
cusEnts
|
||||
.map((cusEnt) =>
|
||||
cusEntToBalance({
|
||||
cusEnt,
|
||||
entityId,
|
||||
withRollovers: true,
|
||||
}),
|
||||
)
|
||||
.filter(notNullish),
|
||||
);
|
||||
|
||||
const totalAdjustment = sumValues(
|
||||
cusEnts.map((cusEnt) => {
|
||||
const { adjustment } = getCusEntBalance({ cusEnt, entityId });
|
||||
return adjustment;
|
||||
}),
|
||||
);
|
||||
|
||||
const totalUnused = sumValues(
|
||||
cusEnts.map((cusEnt) => {
|
||||
const { unused } = getCusEntBalance({ cusEnt, entityId });
|
||||
return unused;
|
||||
}),
|
||||
);
|
||||
|
||||
const nextResetAt = cusEntsToNextResetAt({ cusEnts });
|
||||
|
||||
const totalUsageLimit = sumValues(
|
||||
cusEnts.map((cusEnt) => cusEntToUsageLimit({ cusEnt })),
|
||||
);
|
||||
|
||||
const totalIncludedUsage = sumValues(
|
||||
cusEnts.map((cusEnt) => cusEntToIncludedUsage({ cusEnt, entityId })),
|
||||
);
|
||||
|
||||
const totalIncludedUsageWithRollovers = sumValues(
|
||||
cusEnts.map((cusEnt) =>
|
||||
cusEntToIncludedUsage({ cusEnt, entityId, withRollovers: true }),
|
||||
),
|
||||
);
|
||||
|
||||
const totalUsage = new Decimal(totalIncludedUsageWithRollovers)
|
||||
.add(totalAdjustment)
|
||||
.sub(totalBalanceWithRollovers)
|
||||
.sub(totalUnused)
|
||||
.toNumber();
|
||||
|
||||
const { interval, interval_count } = cusEntsToInterval({ cusEnts });
|
||||
|
||||
const rollovers = cusEntsToRollovers({ cusEnts, entityId });
|
||||
|
||||
const apiFeature = toApiFeature({ feature });
|
||||
|
||||
const { data: apiCusFeature, error } = ApiCusFeatureSchema.safeParse({
|
||||
id: feature.id,
|
||||
name: feature.name,
|
||||
type: getCusFeatureType({ feature }),
|
||||
balance: totalBalanceWithRollovers,
|
||||
usage: totalUsage,
|
||||
included_usage: totalIncludedUsage,
|
||||
usage_limit:
|
||||
totalUsageLimit === totalIncludedUsage ? undefined : totalUsageLimit,
|
||||
next_reset_at: nextResetAt,
|
||||
unlimited: false,
|
||||
overage_allowed: false,
|
||||
interval,
|
||||
interval_count,
|
||||
rollovers,
|
||||
credit_schema:
|
||||
apiFeature.credit_schema?.map((credit) => ({
|
||||
feature_id: credit.metered_feature_id,
|
||||
credit_amount: credit.credit_cost,
|
||||
})) || undefined,
|
||||
|
||||
breakdown: cusEntsToBreakdown({ ctx, fullCus, cusEnts }),
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
return applyResponseVersionChanges({
|
||||
input: apiCusFeature,
|
||||
targetVersion: ctx.apiVersion,
|
||||
resource: AffectedResource.CusFeature,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
import {
|
||||
type ApiCusFeature,
|
||||
CusProductStatus,
|
||||
cusEntToKey,
|
||||
cusProductsToCusEnts,
|
||||
type FullCusEntWithFullCusProduct,
|
||||
type FullCustomer,
|
||||
} from "@autumn/shared";
|
||||
import { V1_2_FeaturesArrayToObject } from "@shared/api/customers/cusFeatures/changes/V1_2_FeaturesArrayToObject.js";
|
||||
import { isBeforeChange } from "@shared/api/versionUtils/versionChangeUtils/applyVersionChanges.js";
|
||||
import type { RequestContext } from "@/honoUtils/HonoEnv.js";
|
||||
import { getApiCusFeature } from "./getApiCusFeature.js";
|
||||
|
||||
// V0 of customer, get list
|
||||
export const getApiCusFeaturesList = async ({
|
||||
ctx,
|
||||
fullCus,
|
||||
}: {
|
||||
ctx: RequestContext;
|
||||
fullCus: FullCustomer;
|
||||
}) => {
|
||||
const { org } = ctx;
|
||||
|
||||
const cusEntsWithCusProduct = cusProductsToCusEnts({
|
||||
cusProducts: fullCus.customer_products,
|
||||
inStatuses: org.config.include_past_due
|
||||
? [CusProductStatus.Active, CusProductStatus.PastDue]
|
||||
: [CusProductStatus.Active],
|
||||
});
|
||||
|
||||
const keyToCusEnts: Record<string, FullCusEntWithFullCusProduct[]> = {};
|
||||
for (const cusEnt of cusEntsWithCusProduct) {
|
||||
const key = cusEntToKey({ cusEnt });
|
||||
keyToCusEnts[key] = [...(keyToCusEnts[key] || []), cusEnt];
|
||||
}
|
||||
|
||||
const apiCusFeatures: ApiCusFeature[] = [];
|
||||
for (const key in keyToCusEnts) {
|
||||
const feature = keyToCusEnts[key][0].entitlement.feature;
|
||||
const cusEnts = keyToCusEnts[key];
|
||||
const apiCusFeature = getApiCusFeature({
|
||||
ctx,
|
||||
fullCus,
|
||||
cusEnts,
|
||||
feature,
|
||||
});
|
||||
apiCusFeatures.push(apiCusFeature);
|
||||
}
|
||||
|
||||
return apiCusFeatures;
|
||||
};
|
||||
|
||||
export const getApiCusFeaturesObject = async ({
|
||||
ctx,
|
||||
fullCus,
|
||||
}: {
|
||||
ctx: RequestContext;
|
||||
fullCus: FullCustomer;
|
||||
}) => {
|
||||
const { org } = ctx;
|
||||
|
||||
const cusEntsWithCusProduct = cusProductsToCusEnts({
|
||||
cusProducts: fullCus.customer_products,
|
||||
inStatuses: org.config.include_past_due
|
||||
? [CusProductStatus.Active, CusProductStatus.PastDue]
|
||||
: [CusProductStatus.Active],
|
||||
});
|
||||
|
||||
const featureToCusEnt: Record<string, FullCusEntWithFullCusProduct[]> = {};
|
||||
for (const cusEnt of cusEntsWithCusProduct) {
|
||||
const featureId = cusEnt.entitlement.feature.id;
|
||||
featureToCusEnt[featureId] = [
|
||||
...(featureToCusEnt[featureId] || []),
|
||||
cusEnt,
|
||||
];
|
||||
}
|
||||
|
||||
const apiCusFeatures: Record<string, ApiCusFeature> = {};
|
||||
for (const key in featureToCusEnt) {
|
||||
const feature = featureToCusEnt[key][0].entitlement.feature;
|
||||
const cusEnts = featureToCusEnt[key];
|
||||
|
||||
// 1. Get cus feature for each breakdown
|
||||
const apiCusFeature = getApiCusFeature({
|
||||
ctx,
|
||||
fullCus,
|
||||
cusEnts,
|
||||
feature,
|
||||
});
|
||||
|
||||
// Otherwise...
|
||||
apiCusFeatures[feature.id] = apiCusFeature;
|
||||
}
|
||||
|
||||
return apiCusFeatures;
|
||||
};
|
||||
|
||||
export const getApiCusFeatures = async ({
|
||||
ctx,
|
||||
fullCus,
|
||||
}: {
|
||||
ctx: RequestContext;
|
||||
fullCus: FullCustomer;
|
||||
}) => {
|
||||
console.log("MADE IT HERE!!");
|
||||
if (
|
||||
isBeforeChange({
|
||||
targetVersion: ctx.apiVersion,
|
||||
versionChange: V1_2_FeaturesArrayToObject,
|
||||
})
|
||||
) {
|
||||
return getApiCusFeaturesList({ ctx, fullCus });
|
||||
}
|
||||
|
||||
return getApiCusFeaturesObject({ ctx, fullCus });
|
||||
};
|
||||
@@ -1,153 +1,20 @@
|
||||
import {
|
||||
ACTIVE_STATUSES,
|
||||
AffectedResource,
|
||||
type APICusProduct,
|
||||
APICustomerSchema,
|
||||
type ApiVersion,
|
||||
ApiVersionClass,
|
||||
applyResponseVersionChanges,
|
||||
type CusProductStatus,
|
||||
type Feature,
|
||||
type FullCusProduct,
|
||||
type FullCustomer,
|
||||
LATEST_VERSION,
|
||||
} from "@autumn/shared";
|
||||
import { getApiCusFeature } from "./getApiCusFeature.js";
|
||||
import { getApiCusProduct } from "./getApiCusProduct.js";
|
||||
|
||||
/**
|
||||
* Merges customer products by id and status
|
||||
* This is how V1_1+ handles multiple subscriptions to the same product
|
||||
*/
|
||||
const mergeApiCusProducts = ({
|
||||
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);
|
||||
};
|
||||
import { type FullCustomer } from "@autumn/shared";
|
||||
import type { RequestContext } from "@/honoUtils/HonoEnv.js";
|
||||
import { getApiCusFeatures } from "./getApiCusFeature/getApiCusFeatures.js";
|
||||
|
||||
export const getApiCustomer = async ({
|
||||
customer,
|
||||
cusProducts,
|
||||
balances,
|
||||
features,
|
||||
apiVersion,
|
||||
invoices,
|
||||
trialsUsed,
|
||||
rewards,
|
||||
entities,
|
||||
referrals,
|
||||
upcomingInvoice,
|
||||
paymentMethod,
|
||||
withAutumnId = false,
|
||||
ctx,
|
||||
fullCus,
|
||||
}: {
|
||||
customer: FullCustomer;
|
||||
cusProducts: FullCusProduct[];
|
||||
balances: any[];
|
||||
features: Feature[];
|
||||
apiVersion: ApiVersion;
|
||||
invoices?: any[];
|
||||
trialsUsed?: any[];
|
||||
rewards?: any;
|
||||
entities?: any[];
|
||||
referrals?: any[];
|
||||
upcomingInvoice?: any;
|
||||
paymentMethod?: any;
|
||||
withAutumnId?: boolean;
|
||||
}): Promise<any> => {
|
||||
const subs = customer.subscriptions || [];
|
||||
|
||||
// Process each product using getApiCusProduct (builds latest format + applies transforms)
|
||||
let main: APICusProduct[] = [];
|
||||
let addOns: APICusProduct[] = [];
|
||||
|
||||
for (const cusProduct of cusProducts) {
|
||||
const processed = await getApiCusProduct({
|
||||
cusProduct,
|
||||
subs,
|
||||
features,
|
||||
apiVersion,
|
||||
});
|
||||
|
||||
const isAddOn = cusProduct.product.is_add_on;
|
||||
if (isAddOn) {
|
||||
addOns.push(processed);
|
||||
} else {
|
||||
main.push(processed);
|
||||
}
|
||||
}
|
||||
|
||||
// Merge products (V1_1+ behavior, always do this in latest format)
|
||||
main = mergeApiCusProducts({ cusProductResponses: main });
|
||||
addOns = mergeApiCusProducts({ cusProductResponses: addOns });
|
||||
|
||||
// Merge main and addOns into single products array (V1_1+ behavior)
|
||||
const allProducts = [...main, ...addOns];
|
||||
|
||||
// Get versioned features (handles field mapping + object vs array format)
|
||||
const apiFeatures = getApiCusFeature({
|
||||
balances,
|
||||
features,
|
||||
apiVersion,
|
||||
});
|
||||
|
||||
// Build customer in latest format (V1_1+: merged response with features/products)
|
||||
const latestCustomer = APICustomerSchema.parse({
|
||||
autumn_id: withAutumnId ? customer.internal_id : undefined,
|
||||
id: customer.id,
|
||||
email: customer.email,
|
||||
name: customer.name,
|
||||
fingerprint: customer.fingerprint,
|
||||
stripe_id: customer.processor?.id,
|
||||
env: customer.env,
|
||||
created_at: customer.created_at,
|
||||
features: apiFeatures, // Already versioned (object for V1_2, array for V1_1)
|
||||
products: allProducts, // Merged products (V1_1+ format)
|
||||
invoices,
|
||||
trials_used: trialsUsed,
|
||||
rewards,
|
||||
metadata: customer.metadata || {},
|
||||
entities,
|
||||
referrals,
|
||||
upcoming_invoice: upcomingInvoice,
|
||||
payment_method: paymentMethod,
|
||||
});
|
||||
|
||||
// Apply customer-level version changes (e.g., V1_1_MergedResponse)
|
||||
// This will split the merged response for V1_0 users
|
||||
return applyResponseVersionChanges({
|
||||
input: latestCustomer,
|
||||
currentVersion: new ApiVersionClass(LATEST_VERSION),
|
||||
targetVersion: new ApiVersionClass(apiVersion),
|
||||
resource: AffectedResource.Customer,
|
||||
ctx: RequestContext;
|
||||
fullCus: FullCustomer;
|
||||
}) => {
|
||||
console.log(`[getApiCustomer] Getting features for customer ${fullCus.id}`);
|
||||
const apiCusFeatures = await getApiCusFeatures({
|
||||
ctx,
|
||||
fullCus,
|
||||
});
|
||||
return {
|
||||
features: apiCusFeatures,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
type ApiCusFeature,
|
||||
type ApiCusRollover,
|
||||
type CreditSchemaItem,
|
||||
type CusEntResponse,
|
||||
CusEntResponseSchema,
|
||||
type CusEntResponseV2,
|
||||
type CusRollover,
|
||||
type Feature,
|
||||
FeatureType,
|
||||
type FullCustomerEntitlement,
|
||||
@@ -37,7 +37,7 @@ export const getEarliestNextResetAt = (entList: CusEntResponse[]) => {
|
||||
return acc;
|
||||
}, Infinity);
|
||||
|
||||
return earliest == Infinity ? null : earliest;
|
||||
return earliest === Infinity ? null : earliest;
|
||||
};
|
||||
|
||||
export const featuresToObject = ({
|
||||
@@ -47,15 +47,15 @@ export const featuresToObject = ({
|
||||
features: Feature[];
|
||||
entList: CusEntResponse[];
|
||||
}) => {
|
||||
const featureObject: Record<string, CusEntResponseV2> = {};
|
||||
const featureObject: Record<string, ApiCusFeature> = {};
|
||||
|
||||
for (const entRes of entList) {
|
||||
const feature = features.find((f) => f.id == entRes.feature_id)!;
|
||||
const feature = features.find((f) => f.id === entRes.feature_id)!;
|
||||
const featureType = getCusFeatureType({ feature });
|
||||
|
||||
const featureId = feature.id;
|
||||
const unlimited = entRes.unlimited;
|
||||
const relatedEnts = entList.filter((e) => e.feature_id == featureId);
|
||||
const relatedEnts = entList.filter((e) => e.feature_id === featureId);
|
||||
|
||||
if (featureObject[featureId]) {
|
||||
continue;
|
||||
@@ -73,9 +73,9 @@ export const featuresToObject = ({
|
||||
const rollovers = hasRollovers
|
||||
? (relatedEnts
|
||||
.flatMap((e) => e.rollovers)
|
||||
.filter(notNullish) as CusRollover[])
|
||||
.filter(notNullish) as ApiCusRollover[])
|
||||
: undefined;
|
||||
const cusFeature: CusEntResponseV2 = {
|
||||
const cusFeature: ApiCusFeature = {
|
||||
id: featureId,
|
||||
name: feature.name,
|
||||
type: featureType,
|
||||
@@ -86,9 +86,9 @@ export const featuresToObject = ({
|
||||
usage_limit: usageLimit,
|
||||
|
||||
next_reset_at: getEarliestNextResetAt(relatedEnts),
|
||||
interval: relatedEnts.length == 1 ? relatedEnts[0].interval : "multiple",
|
||||
interval: relatedEnts.length === 1 ? relatedEnts[0].interval : "multiple",
|
||||
interval_count:
|
||||
relatedEnts.length == 1 ? relatedEnts[0].interval_count : null,
|
||||
relatedEnts.length === 1 ? relatedEnts[0].interval_count : null,
|
||||
overage_allowed: relatedEnts.some((e) => e.overage_allowed),
|
||||
breakdown:
|
||||
!unlimited && relatedEnts.length > 1
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type FullCustomerEntitlement,
|
||||
type FullCustomerPrice,
|
||||
getCusEntBalance,
|
||||
getStartingBalance,
|
||||
LegacyVersion,
|
||||
type Organization,
|
||||
} from "@autumn/shared";
|
||||
@@ -16,7 +17,6 @@ import { BREAK_API_VERSION } from "@/utils/constants.js";
|
||||
import { notNullish, notNullOrUndefined } from "@/utils/genUtils.js";
|
||||
import {
|
||||
getRelatedCusPrice,
|
||||
getResetBalance,
|
||||
getUnlimitedAndUsageAllowed,
|
||||
} from "../../cusProducts/cusEnts/cusEntUtils.js";
|
||||
|
||||
@@ -274,9 +274,9 @@ export const getCusBalances = async ({
|
||||
data[key].adjustment += adjustment || 0;
|
||||
|
||||
const total =
|
||||
(getResetBalance({
|
||||
(getStartingBalance({
|
||||
entitlement: ent,
|
||||
options: getEntOptions(cusProduct.options, ent),
|
||||
options: getEntOptions(cusProduct.options, ent) || undefined,
|
||||
relatedPrice: getRelatedCusPrice(cusEnt, cusPrices)?.price,
|
||||
productQuantity: cusProduct.quantity || 1,
|
||||
}) || 0) * count;
|
||||
@@ -303,9 +303,9 @@ export const getCusBalances = async ({
|
||||
data[key].next_reset_at = cusEnt.next_reset_at;
|
||||
}
|
||||
|
||||
const resetBalance = getResetBalance({
|
||||
const resetBalance = getStartingBalance({
|
||||
entitlement: ent,
|
||||
options: getEntOptions(cusProduct.options, ent),
|
||||
options: getEntOptions(cusProduct.options, ent) || undefined,
|
||||
relatedPrice: getRelatedCusPrice(cusEnt, cusPrices)?.price,
|
||||
productQuantity: cusProduct.quantity || 1,
|
||||
});
|
||||
@@ -342,19 +342,19 @@ export const getCusBalances = async ({
|
||||
}
|
||||
|
||||
// Sort balances
|
||||
if (org.api_version == LegacyVersion.v1) {
|
||||
if (org.api_version === LegacyVersion.v1) {
|
||||
balances.sort((a: any, b: any) => {
|
||||
const featureA = features.find((f) => f.id == a.feature_id);
|
||||
const featureB = features.find((f) => f.id == b.feature_id);
|
||||
const featureA = features.find((f) => f.id === a.feature_id);
|
||||
const featureB = features.find((f) => f.id === b.feature_id);
|
||||
|
||||
if (
|
||||
featureA?.type == FeatureType.Boolean &&
|
||||
featureB?.type != FeatureType.Boolean
|
||||
featureA?.type === FeatureType.Boolean &&
|
||||
featureB?.type !== FeatureType.Boolean
|
||||
) {
|
||||
return -1;
|
||||
} else if (
|
||||
featureA?.type != FeatureType.Boolean &&
|
||||
featureB?.type == FeatureType.Boolean
|
||||
featureA?.type !== FeatureType.Boolean &&
|
||||
featureB?.type === FeatureType.Boolean
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
APICustomerSchema,
|
||||
ApiCustomerSchema,
|
||||
type AppEnv,
|
||||
CusEntResponseSchema,
|
||||
CusExpand,
|
||||
@@ -142,7 +142,7 @@ export const getCustomerDetails = async ({
|
||||
});
|
||||
|
||||
const cusResponse = {
|
||||
...APICustomerSchema.parse({
|
||||
...ApiCustomerSchema.parse({
|
||||
...customer,
|
||||
stripe_id: customer.processor?.id,
|
||||
features: entList,
|
||||
|
||||
150
server/src/internal/customers/handlers/handleGetCustomerV2.ts
Normal file
150
server/src/internal/customers/handlers/handleGetCustomerV2.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { ApiVersion, CusExpand, CusProductStatus } from "@autumn/shared";
|
||||
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
|
||||
import { getCusWithCache } from "../cusCache/getCusWithCache.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({
|
||||
handler: async (c) => {
|
||||
const ctx = c.get("ctx");
|
||||
const customerId = c.req.param("customer_id");
|
||||
const { env, db, logger, org, features } = ctx;
|
||||
const { expand } = c.req.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}`);
|
||||
const startTime = Date.now();
|
||||
|
||||
const fullCus = await getCusWithCache({
|
||||
db,
|
||||
idOrInternalId: customerId,
|
||||
org,
|
||||
env,
|
||||
expand: expandArray,
|
||||
logger,
|
||||
allowNotFound: false,
|
||||
});
|
||||
|
||||
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({
|
||||
ctx,
|
||||
fullCus: fullCus,
|
||||
});
|
||||
|
||||
return c.json(customer);
|
||||
|
||||
// // Use getApiCustomer - it handles all versioning internally!
|
||||
// // - Calls getApiCusProduct for each product (builds latest + applies transforms)
|
||||
// // - Calls getApiCusFeature for features (field mapping + object↔array)
|
||||
// // - Applies customer-level changes (splits response for V1_0)
|
||||
// const customerResponse = await getApiCustomer({
|
||||
// customer,
|
||||
// cusProducts: customer.customer_products,
|
||||
// balances,
|
||||
// features,
|
||||
// apiVersion: ctx.apiVersion.semver,
|
||||
// invoices,
|
||||
// trialsUsed,
|
||||
// rewards,
|
||||
// entities,
|
||||
// referrals,
|
||||
// upcomingInvoice,
|
||||
// paymentMethod,
|
||||
// withAutumnId: with_autumn_id === "true",
|
||||
// });
|
||||
|
||||
return c.json(customerResponse);
|
||||
},
|
||||
});
|
||||
|
||||
// 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();
|
||||
@@ -1,24 +1,20 @@
|
||||
import {
|
||||
AttachScenario,
|
||||
Entity,
|
||||
ErrCode,
|
||||
cusProductToProduct,
|
||||
type Entity,
|
||||
FeatureType,
|
||||
FullCusProduct,
|
||||
FullCustomer,
|
||||
type FullCusProduct,
|
||||
type FullCustomer,
|
||||
getStartingBalance,
|
||||
} from "@autumn/shared";
|
||||
import { createFullCusProduct } from "../../add-product/createFullCusProduct.js";
|
||||
import { attachToInsertParams } from "@/internal/products/productUtils.js";
|
||||
import { cusProductToProduct } from "@autumn/shared";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import {
|
||||
getRelatedCusPrice,
|
||||
getResetBalance,
|
||||
} from "../../cusProducts/cusEnts/cusEntUtils.js";
|
||||
import { getEntOptions } from "@/internal/products/prices/priceUtils.js";
|
||||
import { CusEntService } from "../../cusProducts/cusEnts/CusEntitlementService.js";
|
||||
import { attachToInsertParams } from "@/internal/products/productUtils.js";
|
||||
import type { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { createFullCusProduct } from "../../add-product/createFullCusProduct.js";
|
||||
import { CusProductService } from "../../cusProducts/CusProductService.js";
|
||||
import { CusEntService } from "../../cusProducts/cusEnts/CusEntitlementService.js";
|
||||
import { getRelatedCusPrice } from "../../cusProducts/cusEnts/cusEntUtils.js";
|
||||
|
||||
export const handleDecreaseAndTransfer = async ({
|
||||
req,
|
||||
@@ -41,14 +37,14 @@ export const handleDecreaseAndTransfer = async ({
|
||||
const batchDecrement = [];
|
||||
for (const cusEnt of cusProduct.customer_entitlements) {
|
||||
const feature = cusEnt.entitlement.feature;
|
||||
if (feature.type == FeatureType.Boolean) continue;
|
||||
if (feature.type === FeatureType.Boolean) continue;
|
||||
|
||||
const cusPrice = getRelatedCusPrice(cusEnt, cusProduct.customer_prices);
|
||||
|
||||
const options = getEntOptions(cusProduct.options, cusEnt.entitlement);
|
||||
const resetBalance = getResetBalance({
|
||||
const resetBalance = getStartingBalance({
|
||||
entitlement: cusEnt.entitlement,
|
||||
options: options,
|
||||
options: options || undefined,
|
||||
relatedPrice: cusPrice?.price,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
import {
|
||||
ApiVersion,
|
||||
CusExpand,
|
||||
CusProductStatus,
|
||||
cusProductsToCusEnts,
|
||||
cusProductsToCusPrices,
|
||||
ErrCode,
|
||||
} from "@autumn/shared";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
|
||||
import { invoicesToResponse } from "@/internal/invoices/invoiceUtils.js";
|
||||
import { getCusWithCache } from "../../cusCache/getCusWithCache.js";
|
||||
import { getApiCustomer } from "../../cusUtils/apiCusUtils/getApiCustomer.js";
|
||||
import { getCusBalances } from "../../cusUtils/cusFeatureResponseUtils/getCusBalances.js";
|
||||
import { getCusPaymentMethodRes } from "../../cusUtils/cusResponseUtils/getCusPaymentMethodRes.js";
|
||||
import { getCusReferrals } from "../../cusUtils/cusResponseUtils/getCusReferrals.js";
|
||||
import { getCusRewards } from "../../cusUtils/cusResponseUtils/getCusRewards.js";
|
||||
import { getCusUpcomingInvoice } from "../../cusUtils/cusResponseUtils/getCusUpcomingInvoice.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({
|
||||
handler: async (c) => {
|
||||
const ctx = c.get("ctx");
|
||||
const customerId = c.req.param("customer_id");
|
||||
const { env, db, logger, org, features } = ctx;
|
||||
const { expand } = c.req.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}`);
|
||||
const startTime = Date.now();
|
||||
|
||||
const customer = await getCusWithCache({
|
||||
db,
|
||||
idOrInternalId: customerId,
|
||||
org,
|
||||
env,
|
||||
expand: expandArray,
|
||||
allowNotFound: true,
|
||||
logger,
|
||||
});
|
||||
|
||||
logger.info(`[V2] Get customer took ${Date.now() - startTime}ms`);
|
||||
|
||||
if (!customer) {
|
||||
logger.warn(`[V2] Customer ${customerId} not found | Org: ${org.slug}`);
|
||||
return c.json(
|
||||
{
|
||||
message: `Customer ${customerId} not found`,
|
||||
code: ErrCode.CustomerNotFound,
|
||||
},
|
||||
StatusCodes.NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
// Get feature balances
|
||||
const inStatuses = org.config.include_past_due
|
||||
? [CusProductStatus.Active, CusProductStatus.PastDue]
|
||||
: [CusProductStatus.Active];
|
||||
|
||||
const cusEnts = cusProductsToCusEnts({
|
||||
cusProducts: customer.customer_products,
|
||||
inStatuses,
|
||||
});
|
||||
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();
|
||||
|
||||
// Use getApiCustomer - it handles all versioning internally!
|
||||
// - Calls getApiCusProduct for each product (builds latest + applies transforms)
|
||||
// - Calls getApiCusFeature for features (field mapping + object↔array)
|
||||
// - Applies customer-level changes (splits response for V1_0)
|
||||
const customerResponse = await getApiCustomer({
|
||||
customer,
|
||||
cusProducts: customer.customer_products,
|
||||
balances,
|
||||
features,
|
||||
apiVersion: ctx.apiVersion.semver,
|
||||
invoices,
|
||||
trialsUsed,
|
||||
rewards,
|
||||
entities,
|
||||
referrals,
|
||||
upcomingInvoice,
|
||||
paymentMethod,
|
||||
withAutumnId: with_autumn_id === "true",
|
||||
});
|
||||
|
||||
return c.json(customerResponse);
|
||||
},
|
||||
});
|
||||
@@ -1,27 +1,23 @@
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
|
||||
import {
|
||||
getRelatedCusPrice,
|
||||
getResetBalance,
|
||||
} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
|
||||
type CreateEntity,
|
||||
ErrCode,
|
||||
type FullCusProduct,
|
||||
type FullCustomer,
|
||||
type FullCustomerEntitlement,
|
||||
type Replaceable,
|
||||
} from "@autumn/shared";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
|
||||
import {
|
||||
findLinkedCusEnts,
|
||||
findMainCusEntForFeature,
|
||||
} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.js";
|
||||
import { getRelatedCusPrice } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
|
||||
import { adjustAllowance } from "@/trigger/adjustAllowance.js";
|
||||
import { getReps } from "@/trigger/arrearProratedUsage/handleProratedUpgrade.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import {
|
||||
CreateEntity,
|
||||
ErrCode,
|
||||
Feature,
|
||||
FullCusProduct,
|
||||
FullCustomer,
|
||||
FullCustomerEntitlement,
|
||||
Replaceable,
|
||||
} from "@autumn/shared";
|
||||
import type { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
|
||||
export const updateLinkedCusEnt = async ({
|
||||
db,
|
||||
@@ -34,10 +30,10 @@ export const updateLinkedCusEnt = async ({
|
||||
inputEntities: CreateEntity[];
|
||||
entityToReplacement: Record<string, string>;
|
||||
}) => {
|
||||
let newEntities = structuredClone(linkedCusEnt.entities) || {};
|
||||
const newEntities = structuredClone(linkedCusEnt.entities) || {};
|
||||
for (const entity of inputEntities) {
|
||||
let replaceableId = entityToReplacement[entity.id];
|
||||
let replaceableInEntities = replaceableId
|
||||
const replaceableId = entityToReplacement[entity.id];
|
||||
const replaceableInEntities = replaceableId
|
||||
? newEntities[replaceableId]
|
||||
: null;
|
||||
|
||||
@@ -48,7 +44,7 @@ export const updateLinkedCusEnt = async ({
|
||||
};
|
||||
delete newEntities[replaceableId];
|
||||
} else {
|
||||
let balance = linkedCusEnt.entitlement.allowance!;
|
||||
const balance = linkedCusEnt.entitlement.allowance!;
|
||||
newEntities[entity.id] = {
|
||||
id: entity.id,
|
||||
balance,
|
||||
@@ -120,7 +116,7 @@ export const createEntityForCusProduct = async ({
|
||||
const originalBalance = mainCusEnt.balance || 0;
|
||||
const newBalance = originalBalance - inputEntities.length;
|
||||
|
||||
let repsLength = getReps({
|
||||
const repsLength = getReps({
|
||||
cusEnt: mainCusEnt as any,
|
||||
prevBalance: originalBalance,
|
||||
newBalance,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
APIFeatureSchema,
|
||||
APIFeatureType,
|
||||
ApiFeatureSchema,
|
||||
ApiFeatureType,
|
||||
ErrCode,
|
||||
type Feature,
|
||||
FeatureType,
|
||||
@@ -19,7 +19,7 @@ import { validateFeatureId } from "./featureUtils.js";
|
||||
import { handleDeleteFeature } from "./handlers/handleDeleteFeature.js";
|
||||
import { handleGetFeatureDeletionInfo } from "./handlers/handleGetFeatureDeletionInfo.js";
|
||||
import { handleUpdateFeature } from "./handlers/handleUpdateFeature.js";
|
||||
import { fromAPIFeature, toAPIFeature } from "./utils/mapFeatureUtils.js";
|
||||
import { fromApiFeature, toApiFeature } from "./utils/mapFeatureUtils.js";
|
||||
|
||||
export const featureRouter: Router = express.Router();
|
||||
|
||||
@@ -41,7 +41,7 @@ featureRouter.get("", async (req: any, res: any) =>
|
||||
|
||||
res
|
||||
.status(200)
|
||||
.json({ list: features.map((feature) => toAPIFeature({ feature })) });
|
||||
.json({ list: features.map((feature) => toApiFeature({ feature })) });
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -64,7 +64,7 @@ featureRouter.get("/:featureId", async (req: any, res: any) =>
|
||||
});
|
||||
}
|
||||
|
||||
res.status(200).json(toAPIFeature({ feature }));
|
||||
res.status(200).json(toApiFeature({ feature }));
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -75,14 +75,14 @@ featureRouter.post("", async (req: any, res: any) =>
|
||||
res,
|
||||
action: "Create feature",
|
||||
handler: async () => {
|
||||
const apiFeature = APIFeatureSchema.parse(req.body);
|
||||
const apiFeature = ApiFeatureSchema.parse(req.body);
|
||||
if (!apiFeature.name) {
|
||||
apiFeature.name = keyToTitle(apiFeature.id);
|
||||
}
|
||||
|
||||
validateFeatureId(apiFeature.id);
|
||||
|
||||
const feature = fromAPIFeature({
|
||||
const feature = fromApiFeature({
|
||||
apiFeature,
|
||||
orgId: req.orgId,
|
||||
env: req.env,
|
||||
@@ -138,8 +138,8 @@ featureRouter.post("/:feature_id", async (req: any, res: any) =>
|
||||
let featureType = apiFeature.type as unknown as FeatureType;
|
||||
let usageType: FeatureUsageType | undefined;
|
||||
if (
|
||||
apiFeature.type === APIFeatureType.SingleUsage ||
|
||||
apiFeature.type === APIFeatureType.ContinuousUse
|
||||
apiFeature.type === ApiFeatureType.SingleUsage ||
|
||||
apiFeature.type === ApiFeatureType.ContinuousUse
|
||||
) {
|
||||
featureType = FeatureType.Metered;
|
||||
usageType = apiFeature.type as unknown as FeatureUsageType;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
AggregateType,
|
||||
ApiFeatureType,
|
||||
type CreditSystemConfig,
|
||||
cusProductsToCusPrices,
|
||||
ErrCode,
|
||||
@@ -8,7 +9,6 @@ import {
|
||||
FeatureUsageType,
|
||||
type FullCustomer,
|
||||
type MeteredConfig,
|
||||
ProductItemFeatureType,
|
||||
type UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
@@ -195,15 +195,15 @@ export const runSaveFeatureDisplayTask = async ({
|
||||
|
||||
export const getCusFeatureType = ({ feature }: { feature: Feature }) => {
|
||||
if (feature.type === FeatureType.Boolean) {
|
||||
return ProductItemFeatureType.Static;
|
||||
return ApiFeatureType.Static;
|
||||
} else if (feature.type === FeatureType.Metered) {
|
||||
if (feature.config.usage_type === FeatureUsageType.Single) {
|
||||
return ProductItemFeatureType.SingleUse;
|
||||
return ApiFeatureType.SingleUsage;
|
||||
} else {
|
||||
return ProductItemFeatureType.ContinuousUse;
|
||||
return ApiFeatureType.ContinuousUse;
|
||||
}
|
||||
} else {
|
||||
return ProductItemFeatureType.SingleUse;
|
||||
return ApiFeatureType.SingleUsage;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import { addTaskToQueue } from "@/queue/queueUtils.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { keyToTitle } from "@/utils/genUtils.js";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import { toAPIFeature } from "../utils/mapFeatureUtils.js";
|
||||
import { toApiFeature } from "../utils/mapFeatureUtils.js";
|
||||
|
||||
const handleFeatureIdChanged = async ({
|
||||
db,
|
||||
@@ -291,7 +291,7 @@ export const handleUpdateFeature = async (
|
||||
.status(200)
|
||||
.json(
|
||||
updatedFeature
|
||||
? toAPIFeature({ feature: updatedFeature })
|
||||
? toApiFeature({ feature: updatedFeature })
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
@@ -403,7 +403,7 @@ export const handleUpdateFeature = async (
|
||||
.status(200)
|
||||
.json(
|
||||
updatedFeature
|
||||
? toAPIFeature({ feature: updatedFeature })
|
||||
? toApiFeature({ feature: updatedFeature })
|
||||
: undefined,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import {
|
||||
APIFeature,
|
||||
APIFeatureType,
|
||||
AppEnv,
|
||||
CreditSchemaItem,
|
||||
Feature,
|
||||
type ApiFeature,
|
||||
ApiFeatureSchema,
|
||||
ApiFeatureType,
|
||||
type AppEnv,
|
||||
type CreditSchemaItem,
|
||||
type Feature,
|
||||
FeatureType,
|
||||
FeatureUsageType,
|
||||
type FeatureUsageType,
|
||||
} from "@autumn/shared";
|
||||
import { APIFeatureSchema } from "@autumn/shared";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import {
|
||||
constructBooleanFeature,
|
||||
constructCreditSystem,
|
||||
constructMeteredFeature,
|
||||
} from "./constructFeatureUtils.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
|
||||
export const toAPIFeature = ({ feature }: { feature: Feature }) => {
|
||||
export const toApiFeature = ({ feature }: { feature: Feature }) => {
|
||||
// return FeatureResponseSchema.parse(feature);
|
||||
// 1. Get feature type
|
||||
let featureType = feature.type;
|
||||
@@ -23,7 +23,7 @@ export const toAPIFeature = ({ feature }: { feature: Feature }) => {
|
||||
featureType = feature.config.usage_type;
|
||||
}
|
||||
|
||||
let creditSchema = undefined;
|
||||
let creditSchema;
|
||||
if (feature.type == FeatureType.CreditSystem) {
|
||||
creditSchema = feature.config.schema.map((s: CreditSchemaItem) => ({
|
||||
metered_feature_id: s.metered_feature_id,
|
||||
@@ -31,7 +31,7 @@ export const toAPIFeature = ({ feature }: { feature: Feature }) => {
|
||||
}));
|
||||
}
|
||||
|
||||
return APIFeatureSchema.parse({
|
||||
return ApiFeatureSchema.parse({
|
||||
id: feature.id,
|
||||
name: feature.name,
|
||||
type: featureType,
|
||||
@@ -44,20 +44,20 @@ export const toAPIFeature = ({ feature }: { feature: Feature }) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const fromAPIFeature = ({
|
||||
export const fromApiFeature = ({
|
||||
apiFeature,
|
||||
orgId,
|
||||
env,
|
||||
}: {
|
||||
apiFeature: APIFeature;
|
||||
apiFeature: ApiFeature;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
}) => {
|
||||
let isMetered =
|
||||
apiFeature.type == APIFeatureType.SingleUsage ||
|
||||
apiFeature.type == APIFeatureType.ContinuousUse;
|
||||
const isMetered =
|
||||
apiFeature.type == ApiFeatureType.SingleUsage ||
|
||||
apiFeature.type == ApiFeatureType.ContinuousUse;
|
||||
|
||||
let featureType: FeatureType = isMetered
|
||||
const featureType: FeatureType = isMetered
|
||||
? FeatureType.Metered
|
||||
: (apiFeature.type as unknown as FeatureType);
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {
|
||||
type APIFreeTrial,
|
||||
APIFreeTrialSchema,
|
||||
APIProductItemSchema,
|
||||
APIProductPropertiesSchema,
|
||||
APIProductSchema,
|
||||
ApiProductItemSchema,
|
||||
ApiProductPropertiesSchema,
|
||||
ApiProductSchema,
|
||||
AttachScenario,
|
||||
BillingInterval,
|
||||
type Feature,
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
UsageModel,
|
||||
} from "@autumn/shared";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { toAPIFeature } from "@/internal/features/utils/mapFeatureUtils.js";
|
||||
import { toApiFeature } from "@/internal/features/utils/mapFeatureUtils.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import { getFreeTrialAfterFingerprint } from "../../free-trials/freeTrialUtils.js";
|
||||
import { sortProductItems } from "../../pricecn/pricecnUtils.js";
|
||||
@@ -71,10 +71,10 @@ export const getProductItemResponse = ({
|
||||
}
|
||||
|
||||
const feature = features.find((f) => f.id === item.feature_id);
|
||||
return APIProductItemSchema.parse({
|
||||
return ApiProductItemSchema.parse({
|
||||
type,
|
||||
...item,
|
||||
feature: feature ? toAPIFeature({ feature }) : null,
|
||||
feature: feature ? toApiFeature({ feature }) : null,
|
||||
display: withDisplay ? display : undefined,
|
||||
...priceData,
|
||||
quantity,
|
||||
@@ -142,7 +142,7 @@ export const getProductProperties = ({
|
||||
const hasFreeTrial =
|
||||
notNullish(freeTrial) && freeTrial?.trial_available !== false;
|
||||
|
||||
return APIProductPropertiesSchema.parse({
|
||||
return ApiProductPropertiesSchema.parse({
|
||||
is_free: isFreeProduct(product.prices) || false,
|
||||
is_one_off: isOneOff(product.prices) || false,
|
||||
interval_group: largestInterval?.interval,
|
||||
@@ -207,7 +207,7 @@ export const getProductResponse = async ({
|
||||
attachScenario,
|
||||
})) as APIFreeTrial;
|
||||
|
||||
return APIProductSchema.parse({
|
||||
return ApiProductSchema.parse({
|
||||
...product,
|
||||
name: product.name || null,
|
||||
group: product.group || null,
|
||||
|
||||
@@ -13,7 +13,7 @@ import { sendSvixEvent } from "@/external/svix/svixHelpers.js";
|
||||
import { getSingleEntityResponse } from "@/internal/api/entities/getEntityUtils.js";
|
||||
import { getV2CheckResponse } from "@/internal/api/entitled/checkUtils/getV2CheckResponse.js";
|
||||
import { getCustomerDetails } from "@/internal/customers/cusUtils/getCustomerDetails.js";
|
||||
import { toAPIFeature } from "@/internal/features/utils/mapFeatureUtils.js";
|
||||
import { toApiFeature } from "@/internal/features/utils/mapFeatureUtils.js";
|
||||
|
||||
export const mergeNewCusEntsIntoCusProducts = ({
|
||||
cusProducts,
|
||||
@@ -86,7 +86,7 @@ export const sendSvixThresholdReachedEvent = async ({
|
||||
data: {
|
||||
threshold_type: thresholdType,
|
||||
customer: cusDetails,
|
||||
feature: toAPIFeature({ feature }),
|
||||
feature: toApiFeature({ feature }),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -252,7 +252,7 @@ export const handleThresholdReached = async ({
|
||||
data: {
|
||||
threshold_type: "limit_reached",
|
||||
customer: cusDetails,
|
||||
feature: toAPIFeature({ feature }),
|
||||
feature: toApiFeature({ feature }),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
import {
|
||||
AllowanceType,
|
||||
AppEnv,
|
||||
CusProductStatus,
|
||||
Entity,
|
||||
Event,
|
||||
Feature,
|
||||
FullCustomerEntitlement,
|
||||
FullCustomerPrice,
|
||||
Organization,
|
||||
FullCusEntWithFullCusProduct,
|
||||
type AppEnv,
|
||||
BillingType,
|
||||
CusProductStatus,
|
||||
type Customer,
|
||||
type Entity,
|
||||
type Event,
|
||||
type Feature,
|
||||
FeatureType,
|
||||
FeatureUsageType,
|
||||
type FullCusEntWithFullCusProduct,
|
||||
type FullCustomerEntitlement,
|
||||
type FullCustomerPrice,
|
||||
getStartingBalance,
|
||||
type Organization,
|
||||
} from "@autumn/shared";
|
||||
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
|
||||
import { Customer, FeatureType } from "@autumn/shared";
|
||||
import { getCusEntsInFeatures } from "@/internal/customers/cusUtils/cusUtils.js";
|
||||
import { Decimal } from "decimal.js";
|
||||
import { adjustAllowance } from "./adjustAllowance.js";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { entityFeatureIdExists } from "@/internal/api/entities/entityUtils.js";
|
||||
import { CusService } from "@/internal/customers/CusService.js";
|
||||
import { refreshCusCache } from "@/internal/customers/cusCache/updateCachedCus.js";
|
||||
import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js";
|
||||
import { findCusEnt } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.js";
|
||||
import {
|
||||
getMeteredDeduction,
|
||||
getCreditSystemDeduction,
|
||||
performDeduction,
|
||||
} from "./deductUtils.js";
|
||||
|
||||
import { notNullish, nullish } from "@/utils/genUtils.js";
|
||||
getCusEntMasterBalance,
|
||||
getRelatedCusPrice,
|
||||
getTotalNegativeBalance,
|
||||
} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
|
||||
import { deductFromApiCusRollovers } from "@/internal/customers/cusProducts/cusEnts/cusRollovers/rolloverDeductionUtils.js";
|
||||
import { getCusEntsInFeatures } from "@/internal/customers/cusUtils/cusUtils.js";
|
||||
import {
|
||||
creditSystemContainsFeature,
|
||||
featureToCreditSystem,
|
||||
} from "@/internal/features/creditSystemUtils.js";
|
||||
import {
|
||||
getCusEntMasterBalance,
|
||||
getRelatedCusPrice,
|
||||
getResetBalance,
|
||||
getTotalNegativeBalance,
|
||||
} from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
|
||||
import { entityFeatureIdExists } from "@/internal/api/entities/entityUtils.js";
|
||||
import { CusService } from "@/internal/customers/CusService.js";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { findCusEnt } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.js";
|
||||
import {
|
||||
getBillingType,
|
||||
getEntOptions,
|
||||
} from "@/internal/products/prices/priceUtils.js";
|
||||
import { deductFromCusRollovers } from "@/internal/customers/cusProducts/cusEnts/cusRollovers/rolloverDeductionUtils.js";
|
||||
import { refreshCusCache } from "@/internal/customers/cusCache/updateCachedCus.js";
|
||||
import { notNullish, nullish } from "@/utils/genUtils.js";
|
||||
import { adjustAllowance } from "./adjustAllowance.js";
|
||||
import {
|
||||
getCreditSystemDeduction,
|
||||
getMeteredDeduction,
|
||||
performDeduction,
|
||||
} from "./deductUtils.js";
|
||||
import { handleThresholdReached } from "./handleThresholdReached.js";
|
||||
|
||||
// Decimal.set({ precision: 12 }); // 12 DP precision
|
||||
@@ -93,7 +93,7 @@ const getFeatureDeductions = ({
|
||||
}
|
||||
|
||||
// Check if unlimited exists
|
||||
let unlimitedExists = cusEnts.some(
|
||||
const unlimitedExists = cusEnts.some(
|
||||
(cusEnt) =>
|
||||
cusEnt.entitlement.allowance_type === AllowanceType.Unlimited &&
|
||||
cusEnt.entitlement.internal_feature_id == feature.internal_id,
|
||||
@@ -215,16 +215,16 @@ export const performDeductionOnCusEnt = ({
|
||||
let newAdjustment = structuredClone(cusEnt.adjustment);
|
||||
let deducted = 0;
|
||||
|
||||
let cusProduct = cusEnt.customer_product;
|
||||
let options = notNullish(cusProduct)
|
||||
const cusProduct = cusEnt.customer_product;
|
||||
const options = notNullish(cusProduct)
|
||||
? getEntOptions(cusProduct.options, cusEnt.entitlement)
|
||||
: undefined;
|
||||
let cusPrice = notNullish(cusProduct)
|
||||
const cusPrice = notNullish(cusProduct)
|
||||
? getRelatedCusPrice(cusEnt, cusProduct.customer_prices)
|
||||
: undefined;
|
||||
let resetBalance = notNullish(cusProduct)
|
||||
? getResetBalance({
|
||||
options,
|
||||
const resetBalance = notNullish(cusProduct)
|
||||
? getStartingBalance({
|
||||
options: options || undefined,
|
||||
relatedPrice: cusPrice?.price,
|
||||
entitlement: cusEnt.entitlement,
|
||||
})
|
||||
@@ -243,9 +243,9 @@ export const performDeductionOnCusEnt = ({
|
||||
break;
|
||||
}
|
||||
|
||||
let entityBalance = cusEnt.entities[entityId].balance;
|
||||
const entityBalance = cusEnt.entities[entityId].balance;
|
||||
|
||||
let {
|
||||
const {
|
||||
newBalance: newEntityBalance,
|
||||
deducted: newDeducted,
|
||||
toDeduct: newToDeduct,
|
||||
@@ -261,7 +261,7 @@ export const performDeductionOnCusEnt = ({
|
||||
newEntities[entityId].balance = newEntityBalance!;
|
||||
|
||||
if (addAdjustment) {
|
||||
let adjustment = newEntities![entityId!]!.adjustment || 0;
|
||||
const adjustment = newEntities![entityId!]!.adjustment || 0;
|
||||
newEntities![entityId!]!.adjustment = adjustment - newDeducted!;
|
||||
}
|
||||
|
||||
@@ -276,9 +276,9 @@ export const performDeductionOnCusEnt = ({
|
||||
toDeduct = toDeductCursor;
|
||||
} else {
|
||||
// 2. If entity ID, deduct from that entity
|
||||
let currentEntityBalance = cusEnt.entities?.[entityId!]?.balance;
|
||||
const currentEntityBalance = cusEnt.entities?.[entityId!]?.balance;
|
||||
|
||||
let {
|
||||
const {
|
||||
newBalance: newEntityBalance,
|
||||
deducted: newDeducted,
|
||||
toDeduct: newToDeduct,
|
||||
@@ -294,7 +294,7 @@ export const performDeductionOnCusEnt = ({
|
||||
newEntities![entityId!]!.balance = newEntityBalance!;
|
||||
|
||||
if (addAdjustment) {
|
||||
let adjustment = newEntities![entityId!]!.adjustment || 0;
|
||||
const adjustment = newEntities![entityId!]!.adjustment || 0;
|
||||
newEntities![entityId!]!.adjustment = adjustment - newDeducted!;
|
||||
}
|
||||
|
||||
@@ -306,7 +306,7 @@ export const performDeductionOnCusEnt = ({
|
||||
deducted += newDeducted!;
|
||||
}
|
||||
} else {
|
||||
let {
|
||||
const {
|
||||
newBalance: newBalance_,
|
||||
deducted: deducted_,
|
||||
toDeduct: newToDeduct_,
|
||||
@@ -324,7 +324,7 @@ export const performDeductionOnCusEnt = ({
|
||||
toDeduct = newToDeduct_;
|
||||
|
||||
if (addAdjustment) {
|
||||
let adjustment = cusEnt.adjustment || 0;
|
||||
const adjustment = cusEnt.adjustment || 0;
|
||||
newAdjustment = adjustment - deducted!;
|
||||
}
|
||||
}
|
||||
@@ -358,7 +358,7 @@ export const deductAllowanceFromCusEnt = async ({
|
||||
)
|
||||
return toDeduct;
|
||||
|
||||
let {
|
||||
const {
|
||||
newBalance,
|
||||
newEntities,
|
||||
deducted,
|
||||
@@ -371,19 +371,19 @@ export const deductAllowanceFromCusEnt = async ({
|
||||
setZeroAdjustment,
|
||||
});
|
||||
|
||||
let originalGrpBalance = getTotalNegativeBalance({
|
||||
const originalGrpBalance = getTotalNegativeBalance({
|
||||
cusEnt,
|
||||
balance: cusEnt.balance!,
|
||||
entities: cusEnt.entities!,
|
||||
});
|
||||
|
||||
let newGrpBalance = getTotalNegativeBalance({
|
||||
const newGrpBalance = getTotalNegativeBalance({
|
||||
cusEnt,
|
||||
balance: newBalance!,
|
||||
entities: newEntities!,
|
||||
});
|
||||
|
||||
let updates: any = {
|
||||
const updates: any = {
|
||||
balance: newBalance,
|
||||
entities: newEntities,
|
||||
};
|
||||
@@ -419,7 +419,7 @@ export const deductAllowanceFromCusEnt = async ({
|
||||
// Deduct credit amounts too
|
||||
if (feature.type === FeatureType.Metered && willDeductCredits) {
|
||||
for (let i = 0; i < featureDeductions.length; i++) {
|
||||
let { feature: creditSystem, deduction } = featureDeductions[i];
|
||||
const { feature: creditSystem, deduction } = featureDeductions[i];
|
||||
|
||||
if (
|
||||
creditSystem.type === FeatureType.CreditSystem &&
|
||||
@@ -429,12 +429,12 @@ export const deductAllowanceFromCusEnt = async ({
|
||||
})
|
||||
) {
|
||||
// toDeduct -= deduction;
|
||||
let creditAmount = featureToCreditSystem({
|
||||
const creditAmount = featureToCreditSystem({
|
||||
featureId: feature.id!,
|
||||
creditSystem: creditSystem,
|
||||
amount: deducted,
|
||||
});
|
||||
let newDeduction = new Decimal(deduction)
|
||||
const newDeduction = new Decimal(deduction)
|
||||
.minus(creditAmount)
|
||||
.toNumber();
|
||||
|
||||
@@ -497,14 +497,14 @@ export const deductFromUsageBasedCusEnt = async ({
|
||||
return;
|
||||
}
|
||||
|
||||
let cusPrice = getRelatedCusPrice(usageBasedEnt, cusPrices);
|
||||
let billingType = cusPrice?.price
|
||||
const cusPrice = getRelatedCusPrice(usageBasedEnt, cusPrices);
|
||||
const billingType = cusPrice?.price
|
||||
? getBillingType(cusPrice?.price.config!)
|
||||
: undefined;
|
||||
let blockUsageLimit =
|
||||
const blockUsageLimit =
|
||||
billingType === BillingType.InArrearProrated ? false : true;
|
||||
|
||||
let { newBalance, newEntities, deducted } = performDeductionOnCusEnt({
|
||||
const { newBalance, newEntities, deducted } = performDeductionOnCusEnt({
|
||||
cusEnt: usageBasedEnt,
|
||||
toDeduct,
|
||||
allowNegativeBalance: true,
|
||||
@@ -513,13 +513,13 @@ export const deductFromUsageBasedCusEnt = async ({
|
||||
blockUsageLimit,
|
||||
});
|
||||
|
||||
let oldGrpBalance = getTotalNegativeBalance({
|
||||
const oldGrpBalance = getTotalNegativeBalance({
|
||||
cusEnt: usageBasedEnt,
|
||||
balance: usageBasedEnt.balance!,
|
||||
entities: usageBasedEnt.entities!,
|
||||
});
|
||||
|
||||
let newGrpBalance = getTotalNegativeBalance({
|
||||
const newGrpBalance = getTotalNegativeBalance({
|
||||
cusEnt: usageBasedEnt,
|
||||
balance: newBalance!,
|
||||
entities: newEntities!,
|
||||
@@ -532,7 +532,7 @@ export const deductFromUsageBasedCusEnt = async ({
|
||||
usageBasedEnt.adjustment = 0;
|
||||
}
|
||||
|
||||
let updates: any = {
|
||||
const updates: any = {
|
||||
balance: newBalance,
|
||||
entities: newEntities,
|
||||
};
|
||||
@@ -650,7 +650,7 @@ export const updateCustomerBalance = async ({
|
||||
continue;
|
||||
}
|
||||
|
||||
toDeduct = await deductFromCusRollovers({
|
||||
toDeduct = await deductFromApiCusRollovers({
|
||||
toDeduct,
|
||||
cusEnt,
|
||||
deductParams: {
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
import {
|
||||
AllowanceType,
|
||||
AppEnv,
|
||||
type AppEnv,
|
||||
CusProductStatus,
|
||||
Customer,
|
||||
Feature,
|
||||
type Customer,
|
||||
type Feature,
|
||||
FeatureType,
|
||||
FullCustomerEntitlement,
|
||||
Organization,
|
||||
type FullCustomerEntitlement,
|
||||
type Organization,
|
||||
} from "@autumn/shared";
|
||||
import { getCusEntsInFeatures } from "@/internal/customers/cusUtils/cusUtils.js";
|
||||
|
||||
import { featureToCreditSystem } from "@/internal/features/creditSystemUtils.js";
|
||||
import { getFeatureBalance } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
|
||||
import { Decimal } from "decimal.js";
|
||||
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { CusService } from "@/internal/customers/CusService.js";
|
||||
import { refreshCusCache } from "@/internal/customers/cusCache/updateCachedCus.js";
|
||||
import { getFeatureBalance } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
|
||||
import { deductFromApiCusRollovers } from "@/internal/customers/cusProducts/cusEnts/cusRollovers/rolloverDeductionUtils.js";
|
||||
import { getCusEntsInFeatures } from "@/internal/customers/cusUtils/cusUtils.js";
|
||||
import { featureToCreditSystem } from "@/internal/features/creditSystemUtils.js";
|
||||
import { handleThresholdReached } from "./handleThresholdReached.js";
|
||||
import {
|
||||
deductAllowanceFromCusEnt,
|
||||
deductFromUsageBasedCusEnt,
|
||||
} from "./updateBalanceTask.js";
|
||||
import { CusService } from "@/internal/customers/CusService.js";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { deductFromCusRollovers } from "@/internal/customers/cusProducts/cusEnts/cusRollovers/rolloverDeductionUtils.js";
|
||||
import { refreshCusCache } from "@/internal/customers/cusCache/updateCachedCus.js";
|
||||
import { handleThresholdReached } from "./handleThresholdReached.js";
|
||||
|
||||
// 2. Get deductions for each feature
|
||||
const getFeatureDeductions = ({
|
||||
@@ -36,13 +34,13 @@ const getFeatureDeductions = ({
|
||||
features: Feature[];
|
||||
shouldSet: boolean;
|
||||
}) => {
|
||||
let meteredFeature =
|
||||
const meteredFeature =
|
||||
features.find((f) => f.type === FeatureType.Metered) || features[0];
|
||||
|
||||
const featureDeductions = [];
|
||||
for (const feature of features) {
|
||||
let newValue = value;
|
||||
let unlimitedExists = cusEnts.some(
|
||||
const unlimitedExists = cusEnts.some(
|
||||
(cusEnt) =>
|
||||
cusEnt.entitlement.allowance_type === AllowanceType.Unlimited &&
|
||||
cusEnt.entitlement.internal_feature_id == feature.internal_id,
|
||||
@@ -64,13 +62,13 @@ const getFeatureDeductions = ({
|
||||
let deduction = newValue;
|
||||
|
||||
if (shouldSet) {
|
||||
let totalAllowance = cusEnts.reduce((acc, curr) => {
|
||||
const totalAllowance = cusEnts.reduce((acc, curr) => {
|
||||
return acc + (curr.entitlement.allowance || 0);
|
||||
}, 0);
|
||||
|
||||
let targetBalance = new Decimal(totalAllowance).sub(value).toNumber();
|
||||
const targetBalance = new Decimal(totalAllowance).sub(value).toNumber();
|
||||
|
||||
let totalBalance = getFeatureBalance({
|
||||
const totalBalance = getFeatureBalance({
|
||||
cusEnts,
|
||||
internalFeatureId: feature.internal_id!,
|
||||
})!;
|
||||
@@ -236,7 +234,7 @@ export const updateUsage = async ({
|
||||
continue;
|
||||
}
|
||||
|
||||
toDeduct = await deductFromCusRollovers({
|
||||
toDeduct = await deductFromApiCusRollovers({
|
||||
toDeduct,
|
||||
cusEnt,
|
||||
deductParams: {
|
||||
|
||||
@@ -139,3 +139,7 @@ export const slugify = (
|
||||
export const getUnique = (vals: string[]) => {
|
||||
return Array.from(new Set(vals));
|
||||
};
|
||||
|
||||
export const sumValues = (vals: number[]) => {
|
||||
return vals.reduce((acc, curr) => acc + curr, 0);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,29 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const SuccessResponseSchema = z
|
||||
.object({
|
||||
success: z.boolean(),
|
||||
})
|
||||
.meta({
|
||||
id: "SuccessResponse",
|
||||
// Base schema without .meta() to avoid side effects during imports
|
||||
export const SuccessResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
});
|
||||
|
||||
export const getListResponseSchema = ({
|
||||
schema,
|
||||
id,
|
||||
description,
|
||||
}: {
|
||||
schema: z.ZodType;
|
||||
id?: string;
|
||||
description?: string;
|
||||
}) => {
|
||||
const listResponse = z.object({
|
||||
list: z.array(schema),
|
||||
});
|
||||
|
||||
if (id || description) {
|
||||
listResponse.meta({
|
||||
id,
|
||||
description,
|
||||
});
|
||||
}
|
||||
|
||||
return listResponse;
|
||||
};
|
||||
|
||||
@@ -1,35 +1,32 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const CustomerDataSchema = z
|
||||
.object({
|
||||
name: z.string().nullish().meta({
|
||||
description: "Customer's name",
|
||||
example: "John Doe",
|
||||
// Base schema without top-level .meta() to avoid side effects during imports
|
||||
// Individual field descriptions are kept as they don't cause registry conflicts
|
||||
export const CustomerDataSchema = z.object({
|
||||
name: z.string().nullish().meta({
|
||||
description: "Customer's name",
|
||||
example: "John Doe",
|
||||
}),
|
||||
email: z.string().nullish().meta({
|
||||
description: "Customer's email address",
|
||||
example: "john@example.com",
|
||||
}),
|
||||
fingerprint: z.string().nullish().meta({
|
||||
description:
|
||||
"Unique identifier (eg, serial number) to detect duplicate customers and prevent free trial abuse",
|
||||
example: "fp_123abc",
|
||||
}),
|
||||
metadata: z
|
||||
.record(z.any(), z.any())
|
||||
.nullish()
|
||||
.meta({
|
||||
description: "Additional metadata for the customer",
|
||||
example: { company: "Acme Inc" },
|
||||
}),
|
||||
email: z.string().nullish().meta({
|
||||
description: "Customer's email address",
|
||||
example: "john@example.com",
|
||||
}),
|
||||
fingerprint: z.string().nullish().meta({
|
||||
description:
|
||||
"Unique identifier (eg, serial number) to detect duplicate customers and prevent free trial abuse",
|
||||
example: "fp_123abc",
|
||||
}),
|
||||
metadata: z
|
||||
.record(z.any(), z.any())
|
||||
.nullish()
|
||||
.meta({
|
||||
description: "Additional metadata for the customer",
|
||||
example: { company: "Acme Inc" },
|
||||
}),
|
||||
stripe_id: z.string().nullish().meta({
|
||||
description: "Stripe customer ID if you already have one",
|
||||
example: "cus_stripe123",
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
id: "CustomerData",
|
||||
description: "Customer data for creating or updating a customer",
|
||||
});
|
||||
stripe_id: z.string().nullish().meta({
|
||||
description: "Stripe customer ID if you already have one",
|
||||
example: "cus_stripe123",
|
||||
}),
|
||||
});
|
||||
|
||||
export type CustomerData = z.infer<typeof CustomerDataSchema>;
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const EntityDataSchema = z
|
||||
.object({
|
||||
feature_id: z.string().meta({
|
||||
description: "The feature ID that this entity is associated with",
|
||||
example: "seats",
|
||||
}),
|
||||
name: z.string().optional().meta({
|
||||
description: "Name of the entity",
|
||||
example: "Team Alpha",
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
id: "EntityData",
|
||||
description: "Entity data for creating an entity",
|
||||
});
|
||||
// Base schema without top-level .meta() to avoid side effects during imports
|
||||
export const EntityDataSchema = z.object({
|
||||
feature_id: z.string().meta({
|
||||
description: "The feature ID that this entity is associated with",
|
||||
example: "seats",
|
||||
}),
|
||||
name: z.string().optional().meta({
|
||||
description: "Name of the entity",
|
||||
example: "Team Alpha",
|
||||
}),
|
||||
});
|
||||
|
||||
export type EntityData = z.infer<typeof EntityDataSchema>;
|
||||
|
||||
@@ -1,107 +1,77 @@
|
||||
import { EntityDataSchema } from "@api/common/entityData.js";
|
||||
import { APIProductSchema } from "@api/products/apiProduct.js";
|
||||
import { ApiProductSchema } from "@api/products/apiProduct.js";
|
||||
import { z } from "zod/v4";
|
||||
import { CustomerDataSchema } from "../common/customerData.js";
|
||||
import { CoreCusFeatureSchema } from "../customers/cusFeatures/apiCusFeature.js";
|
||||
|
||||
// Check Feature Enums
|
||||
export const CheckFeatureScenarioSchema = z
|
||||
.enum(["usage_limit", "feature_flag"])
|
||||
.meta({
|
||||
id: "CheckFeatureScenario",
|
||||
description: "Scenario type for feature check",
|
||||
});
|
||||
|
||||
export const ProductScenarioSchema = z
|
||||
.enum([
|
||||
"scheduled",
|
||||
"active",
|
||||
"new",
|
||||
"renew",
|
||||
"upgrade",
|
||||
"downgrade",
|
||||
"cancel",
|
||||
])
|
||||
.meta({
|
||||
id: "ProductScenario",
|
||||
description: "Scenario type for product attachment",
|
||||
});
|
||||
export const CheckFeatureScenarioSchema = z.enum([
|
||||
"usage_limit",
|
||||
"feature_flag",
|
||||
]);
|
||||
|
||||
// Check Feature Schemas
|
||||
export const CheckParamsSchema = z
|
||||
.object({
|
||||
customer_id: z.string().meta({
|
||||
description: "The ID of the customer to check",
|
||||
example: "cus_123",
|
||||
}),
|
||||
feature_id: z.string().optional().meta({
|
||||
description: "The ID of the feature to check access for",
|
||||
example: "api_calls",
|
||||
}),
|
||||
product_id: z.string().optional().meta({
|
||||
description: "The ID of the product to check",
|
||||
example: "pro_plan",
|
||||
}),
|
||||
entity_id: z.string().optional().meta({
|
||||
description: "The ID of the entity (optional)",
|
||||
example: "entity_123",
|
||||
}),
|
||||
customer_data: CustomerDataSchema.optional().meta({
|
||||
description:
|
||||
"Customer data to create or update the customer if they don't exist",
|
||||
}),
|
||||
required_balance: z.number().optional().meta({
|
||||
description: "The required balance for the check",
|
||||
example: 1,
|
||||
}),
|
||||
send_event: z.boolean().optional().meta({
|
||||
description: "Whether to send a usage event if allowed",
|
||||
example: true,
|
||||
}),
|
||||
with_preview: z.boolean().optional().meta({
|
||||
description: "Whether to include preview information in the response",
|
||||
example: true,
|
||||
}),
|
||||
entity_data: EntityDataSchema.optional().meta({
|
||||
description: "Entity data to create the entity if it doesn't exist",
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
id: "CheckParams",
|
||||
description: "Parameters for checking feature or product access",
|
||||
});
|
||||
export const CheckParamsSchema = z.object({
|
||||
customer_id: z.string().meta({
|
||||
description: "The ID of the customer to check",
|
||||
example: "cus_123",
|
||||
}),
|
||||
feature_id: z.string().optional().meta({
|
||||
description: "The ID of the feature to check access for",
|
||||
example: "api_calls",
|
||||
}),
|
||||
product_id: z.string().optional().meta({
|
||||
description: "The ID of the product to check",
|
||||
example: "pro_plan",
|
||||
}),
|
||||
entity_id: z.string().optional().meta({
|
||||
description: "The ID of the entity (optional)",
|
||||
example: "entity_123",
|
||||
}),
|
||||
customer_data: CustomerDataSchema.optional().meta({
|
||||
description:
|
||||
"Customer data to create or update the customer if they don't exist",
|
||||
}),
|
||||
required_balance: z.number().optional().meta({
|
||||
description: "The required balance for the check",
|
||||
example: 1,
|
||||
}),
|
||||
send_event: z.boolean().optional().meta({
|
||||
description: "Whether to send a usage event if allowed",
|
||||
example: true,
|
||||
}),
|
||||
with_preview: z.boolean().optional().meta({
|
||||
description: "Whether to include preview information in the response",
|
||||
example: true,
|
||||
}),
|
||||
entity_data: EntityDataSchema.optional().meta({
|
||||
description: "Entity data to create the entity if it doesn't exist",
|
||||
}),
|
||||
});
|
||||
|
||||
// Check Feature Preview Schemas
|
||||
export const CheckFeaturePreviewSchema = z
|
||||
.object({
|
||||
scenario: CheckFeatureScenarioSchema.meta({
|
||||
description: "The scenario type for this feature preview",
|
||||
example: "usage_limit",
|
||||
}),
|
||||
title: z.string().meta({
|
||||
description: "Title for the preview message",
|
||||
example: "Usage Limit Reached",
|
||||
}),
|
||||
message: z.string().meta({
|
||||
description: "Detailed message explaining the check result",
|
||||
example: "You've reached your usage limit. Upgrade to continue.",
|
||||
}),
|
||||
feature_id: z.string().meta({
|
||||
description: "The ID of the feature",
|
||||
example: "api_calls",
|
||||
}),
|
||||
feature_name: z.string().meta({
|
||||
description: "The name of the feature",
|
||||
example: "API Calls",
|
||||
}),
|
||||
products: z.array(APIProductSchema).meta({
|
||||
description: "Available products that include this feature",
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
id: "CheckFeaturePreview",
|
||||
description: "Preview information for a feature check",
|
||||
});
|
||||
export const CheckFeaturePreviewSchema = z.object({
|
||||
scenario: CheckFeatureScenarioSchema,
|
||||
title: z.string().meta({
|
||||
description: "Title for the preview message",
|
||||
example: "Usage Limit Reached",
|
||||
}),
|
||||
message: z.string().meta({
|
||||
description: "Detailed message explaining the check result",
|
||||
example: "You've reached your usage limit. Upgrade to continue.",
|
||||
}),
|
||||
feature_id: z.string().meta({
|
||||
description: "The ID of the feature",
|
||||
example: "api_calls",
|
||||
}),
|
||||
feature_name: z.string().meta({
|
||||
description: "The name of the feature",
|
||||
example: "API Calls",
|
||||
}),
|
||||
products: z.array(ApiProductSchema).meta({
|
||||
description: "Available products that include this feature",
|
||||
}),
|
||||
});
|
||||
|
||||
export const CheckResultSchema = z
|
||||
.object({
|
||||
@@ -133,15 +103,9 @@ export const CheckResultSchema = z
|
||||
description: "Preview information if with_preview was true",
|
||||
}),
|
||||
})
|
||||
.extend(CoreCusFeatureSchema.shape)
|
||||
.meta({
|
||||
id: "CheckResult",
|
||||
description: "Result of a feature check",
|
||||
});
|
||||
.extend(CoreCusFeatureSchema.shape);
|
||||
|
||||
// Export Types
|
||||
export type CheckParams = z.infer<typeof CheckParamsSchema>;
|
||||
export type CheckResponse = z.infer<typeof CheckResultSchema>;
|
||||
export type CheckFeatureScenario = z.infer<typeof CheckFeatureScenarioSchema>;
|
||||
// export type CheckFeaturePreview = z.infer<typeof CheckFeaturePreviewSchema>;
|
||||
export type ProductScenario = z.infer<typeof ProductScenarioSchema>;
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { z } from "zod/v4";
|
||||
import { CustomerDataSchema } from "../common/customerData.js";
|
||||
import { EntityDataSchema } from "../common/entityData.js";
|
||||
import { ProductScenarioSchema } from "./checkModels.js";
|
||||
|
||||
export const ProductScenarioSchema = z.enum([
|
||||
"scheduled",
|
||||
"active",
|
||||
"new",
|
||||
"renew",
|
||||
"upgrade",
|
||||
"downgrade",
|
||||
"cancel",
|
||||
]);
|
||||
|
||||
// Check Product Schemas
|
||||
export const CheckProductParamsSchema = z
|
||||
@@ -98,10 +107,7 @@ export const CheckProductPreviewOptionSchema = z
|
||||
|
||||
export const CheckProductPreviewSchema = z
|
||||
.object({
|
||||
scenario: ProductScenarioSchema.meta({
|
||||
description: "The scenario type for this product preview",
|
||||
example: "upgrade",
|
||||
}),
|
||||
scenario: ProductScenarioSchema,
|
||||
product_id: z.string().meta({
|
||||
description: "The ID of the product",
|
||||
example: "pro_plan",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { APIProductSchema } from "@api/products/apiProduct.js";
|
||||
import { APIProductItemSchema } from "@api/products/apiProductItem.js";
|
||||
import { ApiProductSchema } from "@api/products/apiProduct.js";
|
||||
import { ApiProductItemSchema } from "@api/products/apiProductItem.js";
|
||||
import { FeatureOptionsSchema } from "@models/cusProductModels/cusProductModels.js";
|
||||
import { z } from "zod/v4";
|
||||
import { AttachBodySchema, ExtAttachBodySchema } from "./attachModels.js";
|
||||
@@ -17,15 +17,15 @@ export const CheckoutParamsSchema = AttachBodySchema.extend({
|
||||
export const CheckoutLineSchema = z.object({
|
||||
description: z.string(),
|
||||
amount: z.number(),
|
||||
item: APIProductItemSchema.nullish(),
|
||||
item: ApiProductItemSchema.nullish(),
|
||||
});
|
||||
|
||||
export const CheckoutResponseSchema = z.object({
|
||||
url: z.string().nullish(),
|
||||
customer_id: z.string(),
|
||||
lines: z.array(CheckoutLineSchema),
|
||||
product: APIProductSchema.nullish(),
|
||||
current_product: APIProductSchema.nullish(),
|
||||
product: ApiProductSchema.nullish(),
|
||||
current_product: ApiProductSchema.nullish(),
|
||||
options: z.array(FeatureOptionsSchema).nullish(),
|
||||
total: z.number().nullish(),
|
||||
currency: z.string().nullish(),
|
||||
|
||||
@@ -3,248 +3,197 @@ import { CustomerDataSchema } from "../common/customerData.js";
|
||||
import { EntityDataSchema } from "../common/entityData.js";
|
||||
|
||||
// Cancel Schemas
|
||||
export const CancelBodySchema = z
|
||||
.object({
|
||||
customer_id: z.string().meta({
|
||||
description: "The ID of the customer",
|
||||
example: "cus_123",
|
||||
}),
|
||||
product_id: z.string().meta({
|
||||
description: "The ID of the product to cancel",
|
||||
example: "pro_plan",
|
||||
}),
|
||||
entity_id: z.string().nullish().meta({
|
||||
description: "The ID of the entity (optional)",
|
||||
example: "entity_123",
|
||||
}),
|
||||
cancel_immediately: z.boolean().optional().meta({
|
||||
description: "Whether to cancel the product immediately or at period end",
|
||||
example: false,
|
||||
}),
|
||||
prorate: z.boolean().nullish().meta({
|
||||
description:
|
||||
"Whether to prorate the cancellation (defaults to true if not specified)",
|
||||
example: true,
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
id: "CancelBody",
|
||||
description: "Parameters for canceling a customer's product",
|
||||
});
|
||||
export const CancelBodySchema = z.object({
|
||||
customer_id: z.string().meta({
|
||||
description: "The ID of the customer",
|
||||
example: "cus_123",
|
||||
}),
|
||||
product_id: z.string().meta({
|
||||
description: "The ID of the product to cancel",
|
||||
example: "pro_plan",
|
||||
}),
|
||||
entity_id: z.string().nullish().meta({
|
||||
description: "The ID of the entity (optional)",
|
||||
example: "entity_123",
|
||||
}),
|
||||
cancel_immediately: z.boolean().optional().meta({
|
||||
description: "Whether to cancel the product immediately or at period end",
|
||||
example: false,
|
||||
}),
|
||||
prorate: z.boolean().nullish().meta({
|
||||
description:
|
||||
"Whether to prorate the cancellation (defaults to true if not specified)",
|
||||
example: true,
|
||||
}),
|
||||
});
|
||||
|
||||
export const CancelResultSchema = z
|
||||
.object({
|
||||
success: z.boolean().meta({
|
||||
description: "Whether the cancellation was successful",
|
||||
example: true,
|
||||
}),
|
||||
customer_id: z.string().meta({
|
||||
description: "The ID of the customer",
|
||||
example: "cus_123",
|
||||
}),
|
||||
product_id: z.string().meta({
|
||||
description: "The ID of the canceled product",
|
||||
example: "pro_plan",
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
id: "CancelResult",
|
||||
description: "Result of a product cancellation",
|
||||
});
|
||||
export const CancelResultSchema = z.object({
|
||||
success: z.boolean().meta({
|
||||
description: "Whether the cancellation was successful",
|
||||
example: true,
|
||||
}),
|
||||
customer_id: z.string().meta({
|
||||
description: "The ID of the customer",
|
||||
example: "cus_123",
|
||||
}),
|
||||
product_id: z.string().meta({
|
||||
description: "The ID of the canceled product",
|
||||
example: "pro_plan",
|
||||
}),
|
||||
});
|
||||
|
||||
// Track Schemas
|
||||
export const TrackParamsSchema = z
|
||||
.object({
|
||||
customer_id: z.string().nonempty().meta({
|
||||
description: "The ID of the customer",
|
||||
example: "cus_123",
|
||||
export const TrackParamsSchema = z.object({
|
||||
customer_id: z.string().nonempty().meta({
|
||||
description: "The ID of the customer",
|
||||
example: "cus_123",
|
||||
}),
|
||||
customer_data: CustomerDataSchema.nullish().meta({
|
||||
description:
|
||||
"Customer data to create or update the customer if they don't exist",
|
||||
}),
|
||||
event_name: z.string().nonempty().optional().meta({
|
||||
description: "The name of the event to track",
|
||||
example: "api_call",
|
||||
}),
|
||||
feature_id: z.string().optional().meta({
|
||||
description:
|
||||
"The ID of the feature (alternative to event_name for usage events)",
|
||||
example: "api_calls",
|
||||
}),
|
||||
properties: z
|
||||
.record(z.string(), z.any())
|
||||
.nullish()
|
||||
.meta({
|
||||
description: "Additional properties for the event",
|
||||
example: { endpoint: "/api/users" },
|
||||
}),
|
||||
customer_data: CustomerDataSchema.nullish().meta({
|
||||
description:
|
||||
"Customer data to create or update the customer if they don't exist",
|
||||
}),
|
||||
event_name: z.string().nonempty().optional().meta({
|
||||
description: "The name of the event to track",
|
||||
example: "api_call",
|
||||
}),
|
||||
feature_id: z.string().optional().meta({
|
||||
description:
|
||||
"The ID of the feature (alternative to event_name for usage events)",
|
||||
example: "api_calls",
|
||||
}),
|
||||
properties: z
|
||||
.record(z.string(), z.any())
|
||||
.nullish()
|
||||
.meta({
|
||||
description: "Additional properties for the event",
|
||||
example: { endpoint: "/api/users" },
|
||||
}),
|
||||
timestamp: z.number().nullish().meta({
|
||||
description: "Unix timestamp in milliseconds when the event occurred",
|
||||
example: 1717000000000,
|
||||
}),
|
||||
idempotency_key: z.string().nullish().meta({
|
||||
description: "Idempotency key to prevent duplicate events",
|
||||
example: "evt_abc123",
|
||||
}),
|
||||
value: z.number().nullish().meta({
|
||||
description: "The value/count of the event",
|
||||
example: 1,
|
||||
}),
|
||||
set_usage: z.boolean().nullish().meta({
|
||||
description:
|
||||
"Whether to set the usage to this value instead of increment",
|
||||
example: false,
|
||||
}),
|
||||
entity_id: z.string().nullish().meta({
|
||||
description: "The ID of the entity this event is associated with",
|
||||
example: "entity_123",
|
||||
}),
|
||||
entity_data: EntityDataSchema.nullish().meta({
|
||||
description: "Data for creating the entity if it doesn't exist",
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
id: "TrackParams",
|
||||
description: "Parameters for tracking an event",
|
||||
});
|
||||
timestamp: z.number().nullish().meta({
|
||||
description: "Unix timestamp in milliseconds when the event occurred",
|
||||
example: 1717000000000,
|
||||
}),
|
||||
idempotency_key: z.string().nullish().meta({
|
||||
description: "Idempotency key to prevent duplicate events",
|
||||
example: "evt_abc123",
|
||||
}),
|
||||
value: z.number().nullish().meta({
|
||||
description: "The value/count of the event",
|
||||
example: 1,
|
||||
}),
|
||||
set_usage: z.boolean().nullish().meta({
|
||||
description: "Whether to set the usage to this value instead of increment",
|
||||
example: false,
|
||||
}),
|
||||
entity_id: z.string().nullish().meta({
|
||||
description: "The ID of the entity this event is associated with",
|
||||
example: "entity_123",
|
||||
}),
|
||||
entity_data: EntityDataSchema.nullish().meta({
|
||||
description: "Data for creating the entity if it doesn't exist",
|
||||
}),
|
||||
});
|
||||
|
||||
export const TrackResultSchema = z
|
||||
.object({
|
||||
id: z.string().meta({
|
||||
description: "The ID of the created event",
|
||||
example: "evt_123",
|
||||
}),
|
||||
code: z.string().meta({
|
||||
description: "Response code",
|
||||
example: "event_received",
|
||||
}),
|
||||
customer_id: z.string().meta({
|
||||
description: "The ID of the customer",
|
||||
example: "cus_123",
|
||||
}),
|
||||
entity_id: z.string().optional().meta({
|
||||
description: "The ID of the entity (if provided)",
|
||||
example: "entity_123",
|
||||
}),
|
||||
event_name: z.string().optional().meta({
|
||||
description: "The name of the event",
|
||||
example: "api_call",
|
||||
}),
|
||||
feature_id: z.string().optional().meta({
|
||||
description: "The ID of the feature (if provided)",
|
||||
example: "api_calls",
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
id: "TrackResult",
|
||||
description: "Result of tracking an event",
|
||||
});
|
||||
export const TrackResultSchema = z.object({
|
||||
id: z.string().meta({
|
||||
description: "The ID of the created event",
|
||||
example: "evt_123",
|
||||
}),
|
||||
code: z.string().meta({
|
||||
description: "Response code",
|
||||
example: "event_received",
|
||||
}),
|
||||
customer_id: z.string().meta({
|
||||
description: "The ID of the customer",
|
||||
example: "cus_123",
|
||||
}),
|
||||
entity_id: z.string().optional().meta({
|
||||
description: "The ID of the entity (if provided)",
|
||||
example: "entity_123",
|
||||
}),
|
||||
event_name: z.string().optional().meta({
|
||||
description: "The name of the event",
|
||||
example: "api_call",
|
||||
}),
|
||||
feature_id: z.string().optional().meta({
|
||||
description: "The ID of the feature (if provided)",
|
||||
example: "api_calls",
|
||||
}),
|
||||
});
|
||||
|
||||
// Query Schemas
|
||||
export const QueryParamsSchema = z
|
||||
.object({
|
||||
customer_id: z.string().meta({
|
||||
description: "The ID of the customer to query analytics for",
|
||||
example: "cus_123",
|
||||
}),
|
||||
feature_id: z.union([z.string(), z.array(z.string())]).meta({
|
||||
description: "The feature ID(s) to query",
|
||||
example: "api_calls",
|
||||
}),
|
||||
range: z
|
||||
.enum(["24h", "7d", "30d", "90d", "last_cycle"] as const)
|
||||
.nullish()
|
||||
.meta({
|
||||
description:
|
||||
"Time range for the query (defaults to last_cycle if not provided)",
|
||||
example: "7d",
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
id: "QueryParams",
|
||||
description: "Parameters for querying analytics data",
|
||||
});
|
||||
|
||||
export const QueryResultSchema = z
|
||||
.object({
|
||||
list: z.array(z.any()).meta({
|
||||
description: "List of usage data points",
|
||||
example: [{ period: 1717000000000, count: 100 }],
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
id: "QueryResult",
|
||||
description: "Result of an analytics query",
|
||||
});
|
||||
|
||||
export const SetupPaymentParamsSchema = z
|
||||
.object({
|
||||
customer_id: z.string().meta({
|
||||
description: "The ID of the customer",
|
||||
example: "cus_123",
|
||||
}),
|
||||
success_url: z.string().optional().meta({
|
||||
description: "URL to redirect to after successful payment setup",
|
||||
example: "https://example.com/success",
|
||||
}),
|
||||
checkout_session_params: z.record(z.any(), z.any()).optional().meta({
|
||||
description: "Additional parameters for the checkout session",
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
id: "SetupPaymentParams",
|
||||
description: "Parameters for setting up a payment method",
|
||||
});
|
||||
|
||||
export const SetupPaymentResultSchema = z
|
||||
.object({
|
||||
customer_id: z.string().meta({
|
||||
description: "The ID of the customer",
|
||||
example: "cus_123",
|
||||
}),
|
||||
url: z.string().meta({
|
||||
description: "URL to the payment setup page",
|
||||
example: "https://checkout.stripe.com/...",
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
id: "SetupPaymentResult",
|
||||
description: "Result of setting up a payment method",
|
||||
});
|
||||
|
||||
export const BillingPortalParamsSchema = z
|
||||
.object({
|
||||
customer_id: z.string().meta({
|
||||
description: "The ID of the customer",
|
||||
example: "cus_123",
|
||||
}),
|
||||
return_url: z.string().optional().meta({
|
||||
export const QueryParamsSchema = z.object({
|
||||
customer_id: z.string().meta({
|
||||
description: "The ID of the customer to query analytics for",
|
||||
example: "cus_123",
|
||||
}),
|
||||
feature_id: z.union([z.string(), z.array(z.string())]).meta({
|
||||
description: "The feature ID(s) to query",
|
||||
example: "api_calls",
|
||||
}),
|
||||
range: z
|
||||
.enum(["24h", "7d", "30d", "90d", "last_cycle"] as const)
|
||||
.nullish()
|
||||
.meta({
|
||||
description:
|
||||
"URL to return to after exiting the billing portal. Must include http:// or https://",
|
||||
example: "https://example.com/dashboard",
|
||||
"Time range for the query (defaults to last_cycle if not provided)",
|
||||
example: "7d",
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
id: "BillingPortalParams",
|
||||
description: "Parameters for accessing the billing portal",
|
||||
});
|
||||
});
|
||||
|
||||
export const BillingPortalResultSchema = z
|
||||
.object({
|
||||
customer_id: z.string().meta({
|
||||
description: "The ID of the customer",
|
||||
example: "cus_123",
|
||||
}),
|
||||
url: z.string().meta({
|
||||
description: "URL to the billing portal",
|
||||
example: "https://billing.stripe.com/...",
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
id: "BillingPortalResult",
|
||||
description: "Result of creating a billing portal session",
|
||||
});
|
||||
export const QueryResultSchema = z.object({
|
||||
list: z.array(z.any()).meta({
|
||||
description: "List of usage data points",
|
||||
example: [{ period: 1717000000000, count: 100 }],
|
||||
}),
|
||||
});
|
||||
|
||||
export const SetupPaymentParamsSchema = z.object({
|
||||
customer_id: z.string().meta({
|
||||
description: "The ID of the customer",
|
||||
example: "cus_123",
|
||||
}),
|
||||
success_url: z.string().optional().meta({
|
||||
description: "URL to redirect to after successful payment setup",
|
||||
example: "https://example.com/success",
|
||||
}),
|
||||
checkout_session_params: z.record(z.any(), z.any()).optional().meta({
|
||||
description: "Additional parameters for the checkout session",
|
||||
}),
|
||||
});
|
||||
|
||||
export const SetupPaymentResultSchema = z.object({
|
||||
customer_id: z.string().meta({
|
||||
description: "The ID of the customer",
|
||||
example: "cus_123",
|
||||
}),
|
||||
url: z.string().meta({
|
||||
description: "URL to the payment setup page",
|
||||
example: "https://checkout.stripe.com/...",
|
||||
}),
|
||||
});
|
||||
|
||||
export const BillingPortalParamsSchema = z.object({
|
||||
customer_id: z.string().meta({
|
||||
description: "The ID of the customer",
|
||||
example: "cus_123",
|
||||
}),
|
||||
return_url: z.string().optional().meta({
|
||||
description:
|
||||
"URL to return to after exiting the billing portal. Must include http:// or https://",
|
||||
example: "https://example.com/dashboard",
|
||||
}),
|
||||
});
|
||||
|
||||
export const BillingPortalResultSchema = z.object({
|
||||
customer_id: z.string().meta({
|
||||
description: "The ID of the customer",
|
||||
example: "cus_123",
|
||||
}),
|
||||
url: z.string().meta({
|
||||
description: "URL to the billing portal",
|
||||
example: "https://billing.stripe.com/...",
|
||||
}),
|
||||
});
|
||||
|
||||
export type CancelBody = z.infer<typeof CancelBodySchema>;
|
||||
export type CancelResult = z.infer<typeof CancelResultSchema>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { APICusProductSchema } from "@api/customers/components/apiCusProduct.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 { APIInvoiceSchema } from "@api/others/apiInvoice.js";
|
||||
import { EntityResponseSchema } from "@models/cusModels/entityModels/entityResModels.js";
|
||||
@@ -14,43 +14,38 @@ export const APITrialsUsedSchema = z.object({
|
||||
fingerprint: z.string().nullish(),
|
||||
});
|
||||
|
||||
export const APICustomerSchema = z
|
||||
.object({
|
||||
// Internal fields
|
||||
autumn_id: z.string().nullish(),
|
||||
export const ApiCustomerSchema = z.object({
|
||||
// Internal fields
|
||||
autumn_id: z.string().nullish(),
|
||||
|
||||
id: z.string().nullable().meta({
|
||||
description: "Your internal ID for the customer",
|
||||
example: "cus_123",
|
||||
}),
|
||||
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,
|
||||
}),
|
||||
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),
|
||||
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(APICusProductSchema),
|
||||
features: z.record(z.string(), APICusFeatureSchema),
|
||||
invoices: z.array(APIInvoiceSchema).optional(),
|
||||
trials_used: z.array(APITrialsUsedSchema).optional(),
|
||||
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({}),
|
||||
entities: z.array(EntityResponseSchema).optional(),
|
||||
referrals: z.array(APICusReferralSchema).optional(),
|
||||
upcoming_invoice: APICusUpcomingInvoiceSchema.nullish(),
|
||||
payment_method: z.any().nullish(),
|
||||
})
|
||||
.meta({
|
||||
id: "Customer",
|
||||
description: "Customer object returned by the API",
|
||||
});
|
||||
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(),
|
||||
});
|
||||
|
||||
export type APICustomer = z.infer<typeof APICustomerSchema>;
|
||||
export type ApiCustomer = z.infer<typeof ApiCustomerSchema>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { APICustomerSchema } from "@api/customers/apiCustomer.js";
|
||||
import { ApiCustomerSchema } from "@api/customers/apiCustomer.js";
|
||||
import { APICusProductSchema } from "@api/customers/components/apiCusProduct.js";
|
||||
import { APICusFeatureSchema } from "@api/customers/cusFeatures/apiCusFeature.js";
|
||||
import { ApiCusFeatureSchema } from "@api/customers/cusFeatures/apiCusFeature.js";
|
||||
import { ApiVersion } from "@api/versionUtils/ApiVersion.js";
|
||||
import {
|
||||
AffectedResource,
|
||||
@@ -16,11 +16,11 @@ import { z } from "zod/v4";
|
||||
*/
|
||||
|
||||
// V1_1+ merged response schema
|
||||
const V1_1_CustomerResponseSchema = APICustomerSchema;
|
||||
const V1_1_CustomerResponseSchema = ApiCustomerSchema;
|
||||
|
||||
// V1_0 split response schema
|
||||
const V1_0_CustomerResponseSchema = z.object({
|
||||
customer: APICustomerSchema.omit({
|
||||
customer: ApiCustomerSchema.omit({
|
||||
features: true,
|
||||
products: true,
|
||||
invoices: true,
|
||||
@@ -28,7 +28,7 @@ const V1_0_CustomerResponseSchema = z.object({
|
||||
}),
|
||||
products: z.array(APICusProductSchema),
|
||||
add_ons: z.array(APICusProductSchema),
|
||||
entitlements: z.array(APICusFeatureSchema),
|
||||
entitlements: z.array(ApiCusFeatureSchema),
|
||||
invoices: z.array(z.any()),
|
||||
trials_used: z.array(z.any()).optional(),
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APIProductItemSchema } from "@api/products/apiProductItem.js";
|
||||
import { ApiProductItemSchema } from "@api/products/apiProductItem.js";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const APICusProductSchema = z.object({
|
||||
@@ -19,7 +19,7 @@ export const APICusProductSchema = z.object({
|
||||
|
||||
entity_id: z.string().nullish(),
|
||||
|
||||
items: z.array(APIProductItemSchema).nullish(),
|
||||
items: z.array(ApiProductItemSchema).nullish(),
|
||||
|
||||
quantity: z.number().optional(),
|
||||
});
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { ApiFeatureType } from "@api/features/apiFeature.js";
|
||||
import { EntInterval } from "@models/productModels/entModels/entEnums.js";
|
||||
import { ProductItemFeatureType } from "@models/productV2Models/productItemModels/productItemModels.js";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const CusRolloverSchema = z.object({
|
||||
export const ApiCusRolloverSchema = z.object({
|
||||
balance: z.number(),
|
||||
expires_at: z.number(),
|
||||
});
|
||||
|
||||
// OLD CUS FEATURE RESPONSE
|
||||
|
||||
// Version 2 of cus feature response
|
||||
export const CusEntResponseSchema = z.object({
|
||||
feature_id: z.string(),
|
||||
interval: z.enum(EntInterval).nullish(),
|
||||
@@ -19,10 +21,19 @@ export const CusEntResponseSchema = z.object({
|
||||
next_reset_at: z.number().nullish(),
|
||||
overage_allowed: z.boolean().nullish(),
|
||||
usage_limit: z.number().nullish(),
|
||||
rollovers: z.array(CusRolloverSchema).nullish(),
|
||||
rollovers: z.array(ApiCusRolloverSchema).nullish(),
|
||||
});
|
||||
|
||||
// Version 3 of cus feature response
|
||||
export const ApiCusFeatureBreakdownSchema = z.object({
|
||||
interval: z.enum(EntInterval),
|
||||
interval_count: z.number().nullish(),
|
||||
balance: z.number().nullish(),
|
||||
usage: z.number().nullish(),
|
||||
included_usage: z.number().nullish(),
|
||||
next_reset_at: z.number().nullish(),
|
||||
});
|
||||
|
||||
// NEW CUS FEATURE RESPONSE
|
||||
export const CoreCusFeatureSchema = z.object({
|
||||
interval: z.enum(EntInterval).or(z.literal("multiple")).nullish(),
|
||||
interval_count: z.number().nullish(),
|
||||
@@ -55,17 +66,20 @@ export const CoreCusFeatureSchema = z.object({
|
||||
.nullish(),
|
||||
|
||||
usage_limit: z.number().nullish(),
|
||||
rollovers: z.array(CusRolloverSchema).nullish(),
|
||||
rollovers: z.array(ApiCusRolloverSchema).nullish(),
|
||||
});
|
||||
|
||||
export const APICusFeatureSchema = z
|
||||
export const ApiCusFeatureSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
type: z.enum(ProductItemFeatureType),
|
||||
type: z.enum(ApiFeatureType),
|
||||
name: z.string().nullish(),
|
||||
})
|
||||
.extend(CoreCusFeatureSchema.shape);
|
||||
|
||||
export type CusEntResponse = z.infer<typeof CusEntResponseSchema>;
|
||||
export type CusEntResponseV2 = z.infer<typeof APICusFeatureSchema>;
|
||||
export type CusRollover = z.infer<typeof CusRolloverSchema>;
|
||||
export type ApiCusFeature = z.infer<typeof ApiCusFeatureSchema>;
|
||||
export type ApiCusRollover = z.infer<typeof ApiCusRolloverSchema>;
|
||||
export type ApiCusFeatureBreakdown = z.infer<
|
||||
typeof ApiCusFeatureBreakdownSchema
|
||||
>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APICusFeatureSchema } from "@api/customers/cusFeatures/apiCusFeature.js";
|
||||
import { ApiCusFeatureSchema } from "@api/customers/cusFeatures/apiCusFeature.js";
|
||||
import { ApiVersion } from "@api/versionUtils/ApiVersion.js";
|
||||
import {
|
||||
AffectedResource,
|
||||
@@ -14,10 +14,10 @@ import { z } from "zod/v4";
|
||||
*/
|
||||
|
||||
// V1_2+ features schema (object format)
|
||||
const V1_2_FeaturesSchema = z.record(z.string(), APICusFeatureSchema);
|
||||
const V1_2_FeaturesSchema = z.record(z.string(), ApiCusFeatureSchema);
|
||||
|
||||
// V1_1 features schema (array format)
|
||||
const V1_1_FeaturesSchema = z.array(APICusFeatureSchema);
|
||||
const V1_1_FeaturesSchema = z.array(ApiCusFeatureSchema);
|
||||
|
||||
export class V1_2_FeaturesArrayToObject extends VersionChange<
|
||||
typeof V1_2_FeaturesSchema,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { EntInterval } from "@models/productModels/entModels/entEnums.js";
|
||||
import { ProductItemFeatureType } from "@models/productV2Models/productItemModels/productItemModels.js";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
// const res: any = {
|
||||
@@ -18,71 +17,110 @@ import { z } from "zod/v4";
|
||||
*/
|
||||
export const ApiCusFeatureV0Schema = z.object({
|
||||
feature_id: z.string(),
|
||||
interval: z.enum(EntInterval).nullish(),
|
||||
interval_count: z.number().nullish(),
|
||||
unlimited: z.boolean().nullish(),
|
||||
interval: z.enum(EntInterval).nullish(),
|
||||
balance: z.number().nullish(),
|
||||
used: z.number().nullish(),
|
||||
});
|
||||
|
||||
// OLD CUS FEATURE RESPONSE
|
||||
export const CusEntResponseSchema = z.object({
|
||||
// Example
|
||||
/**
|
||||
|
||||
Boolean response:
|
||||
{
|
||||
"feature_id": "figma-integration",
|
||||
"interval": null
|
||||
},
|
||||
|
||||
Unlimited response:
|
||||
{
|
||||
"feature_id": "deepseek-messages",
|
||||
"unlimited": true,
|
||||
"interval": null,
|
||||
"balance": null,
|
||||
"used": null
|
||||
},
|
||||
|
||||
Regular response:
|
||||
{
|
||||
"feature_id": "chat-messages",
|
||||
"unlimited": false,
|
||||
"interval": "month",
|
||||
"balance": 1000,
|
||||
"used": 0
|
||||
},
|
||||
*/
|
||||
|
||||
/**
|
||||
* ApiCusFeatureV1Schema - Second version of customer feature API model, includes additional fields...
|
||||
*/
|
||||
export const ApiCusFeatureV1Schema = z.object({
|
||||
feature_id: z.string(),
|
||||
unlimited: z.boolean().nullish(),
|
||||
interval: z.enum(EntInterval).nullish(),
|
||||
interval_count: z.number().nullish(),
|
||||
unlimited: z.boolean().nullish(),
|
||||
balance: z.number().nullish(), //
|
||||
usage: z.number().nullish(),
|
||||
included_usage: z.number().nullish(),
|
||||
next_reset_at: z.number().nullish(),
|
||||
overage_allowed: z.boolean().nullish(),
|
||||
usage_limit: z.number().nullish(),
|
||||
// rollovers: z.array(CusRolloverSchema).nullish(),
|
||||
});
|
||||
|
||||
// NEW CUS FEATURE RESPONSE
|
||||
export const CoreCusFeatureSchema = z.object({
|
||||
interval: z.enum(EntInterval).or(z.literal("multiple")).nullish(),
|
||||
interval_count: z.number().nullish(),
|
||||
unlimited: z.boolean().nullish(),
|
||||
balance: z.number().nullish(),
|
||||
usage: z.number().nullish(),
|
||||
included_usage: z.number().nullish(),
|
||||
next_reset_at: z.number().nullish(),
|
||||
overage_allowed: z.boolean().nullish(),
|
||||
|
||||
breakdown: z
|
||||
.array(
|
||||
z.object({
|
||||
interval: z.enum(EntInterval),
|
||||
interval_count: z.number().nullish(),
|
||||
balance: z.number().nullish(),
|
||||
usage: z.number().nullish(),
|
||||
included_usage: z.number().nullish(),
|
||||
next_reset_at: z.number().nullish(),
|
||||
}),
|
||||
)
|
||||
.nullish(),
|
||||
credit_schema: z
|
||||
.array(
|
||||
z.object({
|
||||
feature_id: z.string(),
|
||||
credit_amount: z.number(),
|
||||
}),
|
||||
)
|
||||
.nullish(),
|
||||
|
||||
usage_limit: z.number().nullish(),
|
||||
rollovers: z.array(CusRolloverSchema).nullish(),
|
||||
used: z.number().nullish(),
|
||||
});
|
||||
|
||||
export const APICusFeatureSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
type: z.enum(ProductItemFeatureType),
|
||||
name: z.string().nullish(),
|
||||
})
|
||||
.extend(CoreCusFeatureSchema.shape);
|
||||
// // OLD CUS FEATURE RESPONSE
|
||||
// export const CusEntResponseSchema = z.object({
|
||||
// feature_id: z.string(),
|
||||
// interval: z.enum(EntInterval).nullish(),
|
||||
// interval_count: z.number().nullish(),
|
||||
// unlimited: z.boolean().nullish(),
|
||||
// balance: z.number().nullish(), //
|
||||
// usage: z.number().nullish(),
|
||||
// included_usage: z.number().nullish(),
|
||||
// next_reset_at: z.number().nullish(),
|
||||
// overage_allowed: z.boolean().nullish(),
|
||||
// usage_limit: z.number().nullish(),
|
||||
// // rollovers: z.array(ApiCusRolloverSchema).nullish(),
|
||||
// });
|
||||
|
||||
export type CusEntResponse = z.infer<typeof CusEntResponseSchema>;
|
||||
export type CusEntResponseV2 = z.infer<typeof APICusFeatureSchema>;
|
||||
export type CusRollover = z.infer<typeof CusRolloverSchema>;
|
||||
// // NEW CUS FEATURE RESPONSE
|
||||
// export const CoreCusFeatureSchema = z.object({
|
||||
// interval: z.enum(EntInterval).or(z.literal("multiple")).nullish(),
|
||||
// interval_count: z.number().nullish(),
|
||||
// unlimited: z.boolean().nullish(),
|
||||
// balance: z.number().nullish(),
|
||||
// usage: z.number().nullish(),
|
||||
// included_usage: z.number().nullish(),
|
||||
// next_reset_at: z.number().nullish(),
|
||||
// overage_allowed: z.boolean().nullish(),
|
||||
|
||||
// breakdown: z
|
||||
// .array(
|
||||
// z.object({
|
||||
// interval: z.enum(EntInterval),
|
||||
// interval_count: z.number().nullish(),
|
||||
// balance: z.number().nullish(),
|
||||
// usage: z.number().nullish(),
|
||||
// included_usage: z.number().nullish(),
|
||||
// next_reset_at: z.number().nullish(),
|
||||
// }),
|
||||
// )
|
||||
// .nullish(),
|
||||
// credit_schema: z
|
||||
// .array(
|
||||
// z.object({
|
||||
// feature_id: z.string(),
|
||||
// credit_amount: z.number(),
|
||||
// }),
|
||||
// )
|
||||
// .nullish(),
|
||||
|
||||
// usage_limit: z.number().nullish(),
|
||||
// rollovers: z.array(ApiCusRolloverSchema).nullish(),
|
||||
// });
|
||||
|
||||
// export const ApiCusFeatureSchema = z
|
||||
// .object({
|
||||
// id: z.string(),
|
||||
// type: z.enum(ProductItemFeatureType),
|
||||
// name: z.string().nullish(),
|
||||
// })
|
||||
// .extend(CoreCusFeatureSchema.shape);
|
||||
|
||||
// export type CusEntResponse = z.infer<typeof CusEntResponseSchema>;
|
||||
// export type CusEntResponseV2 = z.infer<typeof ApiCusFeatureSchema>;
|
||||
// export type ApiCusRollover = z.infer<typeof ApiCusRolloverSchema>;
|
||||
|
||||
@@ -2,170 +2,150 @@ import { z } from "zod/v4";
|
||||
import { EntityDataSchema } from "../common/entityData.js";
|
||||
|
||||
// Create Customer Params (based on handlePostCustomer logic)
|
||||
export const CreateCustomerParamsSchema = z
|
||||
.object({
|
||||
id: 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);
|
||||
},
|
||||
{
|
||||
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({
|
||||
description: "Customer's name",
|
||||
example: "John Doe",
|
||||
export const CreateCustomerParamsSchema = z.object({
|
||||
id: 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);
|
||||
},
|
||||
{
|
||||
message:
|
||||
"ID can only contain letters, numbers, underscores, and hyphens",
|
||||
},
|
||||
)
|
||||
.nullish()
|
||||
.meta({
|
||||
description: "Your unique identifier for the customer",
|
||||
example: "cus_123",
|
||||
}),
|
||||
email: z
|
||||
.email({ message: "not a valid email address" })
|
||||
.or(z.literal(""))
|
||||
.nullish()
|
||||
.meta({
|
||||
description: "Customer's email address",
|
||||
example: "john@example.com",
|
||||
}),
|
||||
fingerprint: z.string().nullish().meta({
|
||||
description:
|
||||
"Unique identifier (eg, serial number) to detect duplicate customers and prevent free trial abuse",
|
||||
example: "fp_123abc",
|
||||
name: z.string().nullish().meta({
|
||||
description: "Customer's name",
|
||||
example: "John Doe",
|
||||
}),
|
||||
email: z
|
||||
.email({ message: "not a valid email address" })
|
||||
.or(z.literal(""))
|
||||
.nullish()
|
||||
.meta({
|
||||
description: "Customer's email address",
|
||||
example: "john@example.com",
|
||||
}),
|
||||
metadata: z
|
||||
.record(z.any(), z.any())
|
||||
.default({})
|
||||
.nullish()
|
||||
.meta({
|
||||
description: "Additional metadata for the customer",
|
||||
example: { company: "Acme Inc" },
|
||||
}),
|
||||
stripe_id: z.string().nullish().meta({
|
||||
description: "Stripe customer ID if you already have one",
|
||||
example: "cus_stripe123",
|
||||
fingerprint: z.string().nullish().meta({
|
||||
description:
|
||||
"Unique identifier (eg, serial number) to detect duplicate customers and prevent free trial abuse",
|
||||
example: "fp_123abc",
|
||||
}),
|
||||
metadata: z
|
||||
.record(z.any(), z.any())
|
||||
.default({})
|
||||
.nullish()
|
||||
.meta({
|
||||
description: "Additional metadata for the customer",
|
||||
example: { company: "Acme Inc" },
|
||||
}),
|
||||
entity_id: z.string().nullish().meta({
|
||||
description: "Entity ID to associate with the customer",
|
||||
example: "entity_123",
|
||||
}),
|
||||
entity_data: EntityDataSchema.nullish().meta({
|
||||
description: "Data for creating an entity",
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
id: "CreateCustomerParams",
|
||||
description: "Parameters for creating a customer",
|
||||
});
|
||||
stripe_id: z.string().nullish().meta({
|
||||
description: "Stripe customer ID if you already have one",
|
||||
example: "cus_stripe123",
|
||||
}),
|
||||
entity_id: z.string().nullish().meta({
|
||||
description: "Entity ID to associate with the customer",
|
||||
example: "entity_123",
|
||||
}),
|
||||
entity_data: EntityDataSchema.nullish().meta({
|
||||
description: "Data for creating an entity",
|
||||
}),
|
||||
});
|
||||
|
||||
// Update Customer Params (based on handleUpdateCustomer logic)
|
||||
export const UpdateCustomerParamsSchema = z
|
||||
.object({
|
||||
id: 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);
|
||||
},
|
||||
{
|
||||
message:
|
||||
"ID can only contain letters, numbers, underscores, and hyphens",
|
||||
},
|
||||
)
|
||||
.nullish()
|
||||
.meta({
|
||||
description:
|
||||
"New unique identifier for the customer (cannot be changed to null)",
|
||||
example: "cus_123",
|
||||
}),
|
||||
name: z.string().nullish().meta({
|
||||
description: "Customer's name",
|
||||
example: "John Doe",
|
||||
}),
|
||||
email: z
|
||||
.string()
|
||||
.email({ message: "not a valid email address" })
|
||||
.or(z.literal(""))
|
||||
.nullish()
|
||||
.meta({
|
||||
description: "Customer's email address",
|
||||
example: "john@example.com",
|
||||
}),
|
||||
fingerprint: z.string().nullish().meta({
|
||||
export const UpdateCustomerParamsSchema = z.object({
|
||||
id: 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);
|
||||
},
|
||||
{
|
||||
message:
|
||||
"ID can only contain letters, numbers, underscores, and hyphens",
|
||||
},
|
||||
)
|
||||
.nullish()
|
||||
.meta({
|
||||
description:
|
||||
"Unique identifier (eg, serial number) to detect duplicate customers",
|
||||
example: "fp_123abc",
|
||||
"New unique identifier for the customer (cannot be changed to null)",
|
||||
example: "cus_123",
|
||||
}),
|
||||
metadata: z
|
||||
.record(z.any(), z.any())
|
||||
.nullish()
|
||||
.meta({
|
||||
description:
|
||||
"Additional metadata for the customer (set individual keys to null to delete them)",
|
||||
example: { company: "Acme Inc" },
|
||||
}),
|
||||
stripe_id: z.string().nullish().meta({
|
||||
description: "Stripe customer ID",
|
||||
example: "cus_stripe123",
|
||||
name: z.string().nullish().meta({
|
||||
description: "Customer's name",
|
||||
example: "John Doe",
|
||||
}),
|
||||
email: z
|
||||
.string()
|
||||
.email({ message: "not a valid email address" })
|
||||
.or(z.literal(""))
|
||||
.nullish()
|
||||
.meta({
|
||||
description: "Customer's email address",
|
||||
example: "john@example.com",
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
id: "UpdateCustomerParams",
|
||||
description: "Parameters for updating a customer",
|
||||
});
|
||||
fingerprint: z.string().nullish().meta({
|
||||
description:
|
||||
"Unique identifier (eg, serial number) to detect duplicate customers",
|
||||
example: "fp_123abc",
|
||||
}),
|
||||
metadata: z
|
||||
.record(z.any(), z.any())
|
||||
.nullish()
|
||||
.meta({
|
||||
description:
|
||||
"Additional metadata for the customer (set individual keys to null to delete them)",
|
||||
example: { company: "Acme Inc" },
|
||||
}),
|
||||
stripe_id: z.string().nullish().meta({
|
||||
description: "Stripe customer ID",
|
||||
example: "cus_stripe123",
|
||||
}),
|
||||
});
|
||||
|
||||
// List Customers Query (based on the docs)
|
||||
export const ListCustomersQuerySchema = z
|
||||
.object({
|
||||
limit: z.number().int().min(10).max(100).default(10).optional().meta({
|
||||
description: "Maximum number of customers to return",
|
||||
example: 10,
|
||||
}),
|
||||
offset: z.number().int().min(0).default(0).optional().meta({
|
||||
description: "Number of customers to skip before returning results",
|
||||
example: 0,
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
id: "ListCustomersQuery",
|
||||
description: "Query parameters for listing customers",
|
||||
});
|
||||
export const ListCustomersQuerySchema = z.object({
|
||||
limit: z.number().int().min(10).max(100).default(10).optional().meta({
|
||||
description: "Maximum number of customers to return",
|
||||
example: 10,
|
||||
}),
|
||||
offset: z.number().int().min(0).default(0).optional().meta({
|
||||
description: "Number of customers to skip before returning results",
|
||||
example: 0,
|
||||
}),
|
||||
});
|
||||
|
||||
// List Customers Response
|
||||
export const ListCustomersResponseSchema = z
|
||||
.object({
|
||||
list: z.array(z.any()).meta({
|
||||
description: "List of customers",
|
||||
}),
|
||||
total: z.number().int().meta({
|
||||
description: "Total number of customers available",
|
||||
example: 100,
|
||||
}),
|
||||
limit: z.number().int().meta({
|
||||
description: "Maximum number of customers returned",
|
||||
example: 10,
|
||||
}),
|
||||
offset: z.number().int().meta({
|
||||
description: "Number of customers skipped before returning results",
|
||||
example: 0,
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
id: "ListCustomersResponse",
|
||||
description: "Response for listing customers",
|
||||
});
|
||||
export const ListCustomersResponseSchema = z.object({
|
||||
list: z.array(z.any()).meta({
|
||||
description: "List of customers",
|
||||
}),
|
||||
total: z.number().int().meta({
|
||||
description: "Total number of customers available",
|
||||
example: 100,
|
||||
}),
|
||||
limit: z.number().int().meta({
|
||||
description: "Maximum number of customers returned",
|
||||
example: 10,
|
||||
}),
|
||||
offset: z.number().int().meta({
|
||||
description: "Number of customers skipped before returning results",
|
||||
example: 0,
|
||||
}),
|
||||
});
|
||||
|
||||
export type CreateCustomerParams = z.infer<typeof CreateCustomerParamsSchema>;
|
||||
export type UpdateCustomerParams = z.infer<typeof UpdateCustomerParamsSchema>;
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { CusExpand } from "@models/cusModels/cusExpand.js";
|
||||
import { z } from "zod/v4";
|
||||
import { SuccessResponseSchema } from "../common/commonResponses.js";
|
||||
import { APICustomerSchema } from "./apiCustomer.js";
|
||||
import { ApiCustomerSchema } from "./apiCustomer.js";
|
||||
import {
|
||||
CreateCustomerParamsSchema,
|
||||
ListCustomersResponseSchema,
|
||||
UpdateCustomerParamsSchema,
|
||||
} from "./customerOpModels.js";
|
||||
|
||||
// Register schema with .meta() for OpenAPI spec generation
|
||||
const ApiCustomerWithMeta = ApiCustomerSchema.meta({
|
||||
id: "Customer",
|
||||
description: "Customer object returned by the API",
|
||||
});
|
||||
|
||||
export const customerOps = {
|
||||
"/customers": {
|
||||
get: {
|
||||
@@ -44,7 +50,7 @@ export const customerOps = {
|
||||
responses: {
|
||||
"200": {
|
||||
description: "200 OK",
|
||||
content: { "application/json": { schema: APICustomerSchema } },
|
||||
content: { "application/json": { schema: ApiCustomerWithMeta } },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -64,7 +70,7 @@ export const customerOps = {
|
||||
responses: {
|
||||
"200": {
|
||||
description: "200 OK",
|
||||
content: { "application/json": { schema: APICustomerSchema } },
|
||||
content: { "application/json": { schema: ApiCustomerWithMeta } },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -87,7 +93,7 @@ export const customerOps = {
|
||||
responses: {
|
||||
"200": {
|
||||
description: "200 OK",
|
||||
content: { "application/json": { schema: APICustomerSchema } },
|
||||
content: { "application/json": { schema: ApiCustomerWithMeta } },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { EntityResponseSchema } from "@models/cusModels/entityModels/entityResModels.js";
|
||||
import type { z } from "zod/v4";
|
||||
|
||||
export const APIEntitySchema = EntityResponseSchema.meta({
|
||||
id: "Entity",
|
||||
description: "Entity object returned by the API",
|
||||
});
|
||||
// Base schema without .meta() to avoid side effects during imports
|
||||
export const APIEntitySchema = EntityResponseSchema;
|
||||
export type APIEntity = z.infer<typeof APIEntitySchema>;
|
||||
|
||||
@@ -4,13 +4,11 @@ import { SuccessResponseSchema } from "../common/commonResponses.js";
|
||||
import { APIEntitySchema } from "./apiEntity.js";
|
||||
import { CreateEntityParamsSchema } from "./entityOpModels.js";
|
||||
|
||||
const EntityListResponseSchema = z
|
||||
.object({
|
||||
data: z.array(APIEntitySchema),
|
||||
})
|
||||
.meta({
|
||||
id: "EntityListResponse",
|
||||
});
|
||||
// Register schema with .meta() for OpenAPI spec generation
|
||||
const APIEntityWithMeta = APIEntitySchema.meta({
|
||||
id: "Entity",
|
||||
description: "Entity object returned by the API",
|
||||
});
|
||||
|
||||
export const entityOps = {
|
||||
"/customers/{customer_id}/entities": {
|
||||
@@ -30,7 +28,7 @@ export const entityOps = {
|
||||
responses: {
|
||||
"200": {
|
||||
description: "200 OK",
|
||||
content: { "application/json": { schema: APIEntitySchema } },
|
||||
content: { "application/json": { schema: APIEntityWithMeta } },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -51,7 +49,7 @@ export const entityOps = {
|
||||
responses: {
|
||||
"200": {
|
||||
description: "200 OK",
|
||||
content: { "application/json": { schema: APIEntitySchema } },
|
||||
content: { "application/json": { schema: APIEntityWithMeta } },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,39 +1,28 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
// Create Entity Params (based on CreateEntitySchema from shared/models)
|
||||
export const CreateEntityParamsSchema = z
|
||||
.object({
|
||||
id: z.string().meta({
|
||||
description: "The ID of the entity",
|
||||
example: "entity_123",
|
||||
}),
|
||||
name: z.string().nullish().meta({
|
||||
description: "The name of the entity",
|
||||
example: "Team Alpha",
|
||||
}),
|
||||
feature_id: z.string().meta({
|
||||
description: "The ID of the feature this entity is associated with",
|
||||
example: "seats",
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
id: "CreateEntityParams",
|
||||
description: "Parameters for creating an entity",
|
||||
});
|
||||
export const CreateEntityParamsSchema = z.object({
|
||||
id: z.string().meta({
|
||||
description: "The ID of the entity",
|
||||
example: "entity_123",
|
||||
}),
|
||||
name: z.string().nullish().meta({
|
||||
description: "The name of the entity",
|
||||
example: "Team Alpha",
|
||||
}),
|
||||
feature_id: z.string().meta({
|
||||
description: "The ID of the feature this entity is associated with",
|
||||
example: "seats",
|
||||
}),
|
||||
});
|
||||
|
||||
// Get Entity Query Params
|
||||
export const GetEntityQuerySchema = z
|
||||
.object({
|
||||
expand: z.string().optional().meta({
|
||||
description:
|
||||
"Comma-separated list of fields to expand (e.g., 'invoices')",
|
||||
example: "invoices",
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
id: "GetEntityQuery",
|
||||
description: "Query parameters for getting an entity",
|
||||
});
|
||||
export const GetEntityQuerySchema = z.object({
|
||||
expand: z.string().optional().meta({
|
||||
description: "Comma-separated list of fields to expand (e.g., 'invoices')",
|
||||
example: "invoices",
|
||||
}),
|
||||
});
|
||||
|
||||
export type CreateEntityParams = z.infer<typeof CreateEntityParamsSchema>;
|
||||
export type GetEntityQuery = z.infer<typeof GetEntityQuerySchema>;
|
||||
|
||||
@@ -1,38 +1,35 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export enum APIFeatureType {
|
||||
export enum ApiFeatureType {
|
||||
Static = "static", // legacy (will deprecate)
|
||||
Boolean = "boolean",
|
||||
SingleUsage = "single_use",
|
||||
ContinuousUse = "continuous_use",
|
||||
CreditSystem = "credit_system",
|
||||
}
|
||||
|
||||
export const APIFeatureSchema = z
|
||||
.object({
|
||||
id: z.string(),
|
||||
name: z.string().nullish(),
|
||||
type: z.nativeEnum(APIFeatureType),
|
||||
display: z
|
||||
.object({
|
||||
singular: z.string(),
|
||||
plural: z.string(),
|
||||
})
|
||||
.nullish(),
|
||||
// Base schema without .meta() to avoid side effects during imports
|
||||
export const ApiFeatureSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().nullish(),
|
||||
type: z.enum(ApiFeatureType),
|
||||
display: z
|
||||
.object({
|
||||
singular: z.string(),
|
||||
plural: z.string(),
|
||||
})
|
||||
.nullish(),
|
||||
|
||||
credit_schema: z
|
||||
.array(
|
||||
z.object({
|
||||
metered_feature_id: z.string(),
|
||||
credit_cost: z.number(),
|
||||
}),
|
||||
)
|
||||
.nullish(),
|
||||
credit_schema: z
|
||||
.array(
|
||||
z.object({
|
||||
metered_feature_id: z.string(),
|
||||
credit_cost: z.number(),
|
||||
}),
|
||||
)
|
||||
.nullish(),
|
||||
|
||||
archived: z.boolean().nullish(),
|
||||
})
|
||||
.meta({
|
||||
id: "Feature",
|
||||
description: "Feature object returned by the API",
|
||||
});
|
||||
archived: z.boolean().nullish(),
|
||||
});
|
||||
|
||||
export type APIFeature = z.infer<typeof APIFeatureSchema>;
|
||||
export type ApiFeature = z.infer<typeof ApiFeatureSchema>;
|
||||
|
||||
@@ -1,97 +1,87 @@
|
||||
import { z } from "zod/v4";
|
||||
import { APIFeatureType } from "./apiFeature.js";
|
||||
import { ApiFeatureType } from "./apiFeature.js";
|
||||
|
||||
// Create Feature Params
|
||||
export const CreateFeatureParamsSchema = z
|
||||
.object({
|
||||
id: z.string().meta({
|
||||
description: "The ID of the feature",
|
||||
example: "feature_123",
|
||||
export const CreateFeatureParamsSchema = z.object({
|
||||
id: z.string().meta({
|
||||
description: "The ID of the feature",
|
||||
example: "feature_123",
|
||||
}),
|
||||
name: z.string().nullish().meta({
|
||||
description: "The name of the feature",
|
||||
example: "API Calls",
|
||||
}),
|
||||
type: z.enum(ApiFeatureType).meta({
|
||||
description: "The type of the feature",
|
||||
example: "single_use",
|
||||
}),
|
||||
display: z
|
||||
.object({
|
||||
singular: z.string(),
|
||||
plural: z.string(),
|
||||
})
|
||||
.nullish()
|
||||
.meta({
|
||||
description: "Display names for the feature",
|
||||
example: { singular: "API Call", plural: "API Calls" },
|
||||
}),
|
||||
name: z.string().nullish().meta({
|
||||
description: "The name of the feature",
|
||||
example: "API Calls",
|
||||
}),
|
||||
type: z.nativeEnum(APIFeatureType).meta({
|
||||
description: "The type of the feature",
|
||||
example: "single_use",
|
||||
}),
|
||||
display: z
|
||||
.object({
|
||||
singular: z.string(),
|
||||
plural: z.string(),
|
||||
})
|
||||
.nullish()
|
||||
.meta({
|
||||
description: "Display names for the feature",
|
||||
example: { singular: "API Call", plural: "API Calls" },
|
||||
credit_schema: z
|
||||
.array(
|
||||
z.object({
|
||||
metered_feature_id: z.string(),
|
||||
credit_cost: z.number(),
|
||||
}),
|
||||
credit_schema: z
|
||||
.array(
|
||||
z.object({
|
||||
metered_feature_id: z.string(),
|
||||
credit_cost: z.number(),
|
||||
}),
|
||||
)
|
||||
.nullish()
|
||||
.meta({
|
||||
description:
|
||||
"Credit schema for credit system features (only applicable when type is credit_system)",
|
||||
example: [{ metered_feature_id: "api_calls", credit_cost: 10 }],
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
id: "CreateFeatureParams",
|
||||
description: "Parameters for creating a feature",
|
||||
});
|
||||
)
|
||||
.nullish()
|
||||
.meta({
|
||||
description:
|
||||
"Credit schema for credit system features (only applicable when type is credit_system)",
|
||||
example: [{ metered_feature_id: "api_calls", credit_cost: 10 }],
|
||||
}),
|
||||
});
|
||||
|
||||
// Update Feature Params
|
||||
export const UpdateFeatureParamsSchema = z
|
||||
.object({
|
||||
id: z.string().optional().meta({
|
||||
description: "The ID of the feature",
|
||||
example: "feature_123",
|
||||
export const UpdateFeatureParamsSchema = z.object({
|
||||
id: z.string().optional().meta({
|
||||
description: "The ID of the feature",
|
||||
example: "feature_123",
|
||||
}),
|
||||
name: z.string().optional().meta({
|
||||
description: "The name of the feature",
|
||||
example: "API Calls",
|
||||
}),
|
||||
type: z.enum(ApiFeatureType).optional().meta({
|
||||
description: "The type of the feature",
|
||||
example: "single_use",
|
||||
}),
|
||||
display: z
|
||||
.object({
|
||||
singular: z.string(),
|
||||
plural: z.string(),
|
||||
})
|
||||
.optional()
|
||||
.meta({
|
||||
description: "Display names for the feature",
|
||||
example: { singular: "API Call", plural: "API Calls" },
|
||||
}),
|
||||
name: z.string().optional().meta({
|
||||
description: "The name of the feature",
|
||||
example: "API Calls",
|
||||
}),
|
||||
type: z.enum(APIFeatureType).optional().meta({
|
||||
description: "The type of the feature",
|
||||
example: "single_use",
|
||||
}),
|
||||
display: z
|
||||
.object({
|
||||
singular: z.string(),
|
||||
plural: z.string(),
|
||||
})
|
||||
.optional()
|
||||
.meta({
|
||||
description: "Display names for the feature",
|
||||
example: { singular: "API Call", plural: "API Calls" },
|
||||
credit_schema: z
|
||||
.array(
|
||||
z.object({
|
||||
metered_feature_id: z.string(),
|
||||
credit_cost: z.number(),
|
||||
}),
|
||||
credit_schema: z
|
||||
.array(
|
||||
z.object({
|
||||
metered_feature_id: z.string(),
|
||||
credit_cost: z.number(),
|
||||
}),
|
||||
)
|
||||
.optional()
|
||||
.meta({
|
||||
description:
|
||||
"Credit schema for credit system features (only applicable when type is credit_system)",
|
||||
example: [{ metered_feature_id: "api_calls", credit_cost: 10 }],
|
||||
}),
|
||||
archived: z.boolean().optional().meta({
|
||||
description: "Whether the feature is archived",
|
||||
example: false,
|
||||
)
|
||||
.optional()
|
||||
.meta({
|
||||
description:
|
||||
"Credit schema for credit system features (only applicable when type is credit_system)",
|
||||
example: [{ metered_feature_id: "api_calls", credit_cost: 10 }],
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
id: "UpdateFeatureParams",
|
||||
description: "Parameters for updating a feature",
|
||||
});
|
||||
archived: z.boolean().optional().meta({
|
||||
description: "Whether the feature is archived",
|
||||
example: false,
|
||||
}),
|
||||
});
|
||||
|
||||
export type CreateFeatureParams = z.infer<typeof CreateFeatureParamsSchema>;
|
||||
export type UpdateFeatureParams = z.infer<typeof UpdateFeatureParamsSchema>;
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import { APIFeatureSchema } from "./apiFeature.js";
|
||||
import { z } from "zod/v4";
|
||||
import {
|
||||
getListResponseSchema,
|
||||
SuccessResponseSchema,
|
||||
} from "../common/commonResponses.js";
|
||||
import { ApiFeatureSchema } from "./apiFeature.js";
|
||||
import {
|
||||
CreateFeatureParamsSchema,
|
||||
UpdateFeatureParamsSchema,
|
||||
} from "./featureOpModels.js";
|
||||
import { z } from "zod/v4";
|
||||
import { SuccessResponseSchema } from "../common/commonResponses.js";
|
||||
|
||||
const FeatureListResponseSchema = z
|
||||
.object({
|
||||
list: z.array(APIFeatureSchema),
|
||||
})
|
||||
.meta({
|
||||
id: "FeatureListResponse",
|
||||
});
|
||||
// Register the schema with .meta() for OpenAPI spec generation
|
||||
const ApiFeatureWithMeta = ApiFeatureSchema.meta({
|
||||
id: "Feature",
|
||||
description: "Feature object returned by the API",
|
||||
});
|
||||
|
||||
export const featureOps = {
|
||||
"/features": {
|
||||
@@ -27,7 +28,11 @@ export const featureOps = {
|
||||
responses: {
|
||||
"200": {
|
||||
description: "200 OK",
|
||||
content: { "application/json": { schema: FeatureListResponseSchema } },
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: getListResponseSchema({ schema: ApiFeatureWithMeta }),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -42,7 +47,7 @@ export const featureOps = {
|
||||
responses: {
|
||||
"200": {
|
||||
description: "200 OK",
|
||||
content: { "application/json": { schema: APIFeatureSchema } },
|
||||
content: { "application/json": { schema: ApiFeatureWithMeta } },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -59,7 +64,7 @@ export const featureOps = {
|
||||
responses: {
|
||||
"200": {
|
||||
description: "200 OK",
|
||||
content: { "application/json": { schema: APIFeatureSchema } },
|
||||
content: { "application/json": { schema: ApiFeatureWithMeta } },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -79,7 +84,7 @@ export const featureOps = {
|
||||
responses: {
|
||||
"200": {
|
||||
description: "200 OK",
|
||||
content: { "application/json": { schema: APIFeatureSchema } },
|
||||
content: { "application/json": { schema: ApiFeatureWithMeta } },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { APIFeatureSchema } from "@api/features/apiFeature.js";
|
||||
import { ApiFeatureSchema } from "@api/features/apiFeature.js";
|
||||
import type { z } from "zod/v4";
|
||||
|
||||
export const UpdateFeatureParamsSchema = APIFeatureSchema.partial();
|
||||
export const UpdateFeatureParamsSchema = ApiFeatureSchema.partial();
|
||||
|
||||
export type UpdateFeatureParams = z.infer<typeof UpdateFeatureParamsSchema>;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
export * from "./core/attachModels.js";
|
||||
export * from "./core/checkModels.js";
|
||||
export * from "./core/checkoutModels.js";
|
||||
export * from "./core/coreOpenApi.js";
|
||||
// NOTE: coreOpenApi.js is NOT exported here - it's only imported by openapi.ts for spec generation
|
||||
export * from "./core/coreOpModels.js";
|
||||
|
||||
// Customers
|
||||
@@ -12,17 +12,17 @@ export * from "./customers/components/apiCusProduct.js";
|
||||
export * from "./customers/components/apiCusReferral.js";
|
||||
export * from "./customers/cusFeatures/apiCusFeature.js";
|
||||
export * from "./customers/customerOpModels.js";
|
||||
export * from "./customers/customersOpenApi.js";
|
||||
// NOTE: customersOpenApi.js is NOT exported here - it's only imported by openapi.ts for spec generation
|
||||
|
||||
// Entities
|
||||
export * from "./entities/apiEntity.js";
|
||||
export * from "./entities/entitiesOpenApi.js";
|
||||
// NOTE: entitiesOpenApi.js is NOT exported here - it's only imported by openapi.ts for spec generation
|
||||
export * from "./entities/entityOpModels.js";
|
||||
|
||||
// Features
|
||||
export * from "./features/apiFeature.js";
|
||||
export * from "./features/featureOpModels.js";
|
||||
export * from "./features/featuresOpenApi.js";
|
||||
// NOTE: featuresOpenApi.js is NOT exported here - it's only imported by openapi.ts for spec generation
|
||||
|
||||
// Others
|
||||
export * from "./others/apiDiscount.js";
|
||||
@@ -33,7 +33,7 @@ export * from "./products/apiFreeTrial.js";
|
||||
export * from "./products/apiProduct.js";
|
||||
export * from "./products/apiProductItem.js";
|
||||
export * from "./products/productOpModels.js";
|
||||
export * from "./products/productsOpenApi.js";
|
||||
// NOTE: productsOpenApi.js is NOT exported here - it's only imported by openapi.ts for spec generation
|
||||
|
||||
// export * from "./products/apiFreeTrial.js";
|
||||
// export * from "./products/apiProduct.js";
|
||||
|
||||
@@ -45,8 +45,14 @@ const document = createDocument({
|
||||
id: "AutumnError",
|
||||
description: "An error that occurred in the API",
|
||||
}),
|
||||
customerData: CustomerDataSchema,
|
||||
entityData: EntityDataSchema,
|
||||
customerData: CustomerDataSchema.meta({
|
||||
id: "CustomerData",
|
||||
description: "Customer data for creating or updating a customer",
|
||||
}),
|
||||
entityData: EntityDataSchema.meta({
|
||||
id: "EntityData",
|
||||
description: "Entity data for creating an entity",
|
||||
}),
|
||||
},
|
||||
securitySchemes: {
|
||||
secretKey: {
|
||||
|
||||
@@ -2,9 +2,9 @@ import { AttachScenario } from "@models/checkModels/checkPreviewModels.js";
|
||||
import { AppEnv } from "@models/genModels/genEnums.js";
|
||||
import { z } from "zod/v4";
|
||||
import { APIFreeTrialSchema } from "./apiFreeTrial.js";
|
||||
import { APIProductItemSchema } from "./apiProductItem.js";
|
||||
import { ApiProductItemSchema } from "./apiProductItem.js";
|
||||
|
||||
export const APIProductPropertiesSchema = z.object({
|
||||
export const ApiProductPropertiesSchema = z.object({
|
||||
is_free: z.boolean().meta({
|
||||
description: "True if the product has no base price or usage prices",
|
||||
example: false,
|
||||
@@ -29,115 +29,109 @@ export const APIProductPropertiesSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
export const APIProductSchema = z
|
||||
.object({
|
||||
id: z.string().meta({
|
||||
description: "The ID of the product you set when creating the product",
|
||||
example: "pro_plan",
|
||||
}),
|
||||
export const ApiProductSchema = z.object({
|
||||
id: z.string().meta({
|
||||
description: "The ID of the product you set when creating the product",
|
||||
example: "pro_plan",
|
||||
}),
|
||||
|
||||
name: z.string().meta({
|
||||
description: "The name of the product",
|
||||
example: "Pro Plan",
|
||||
}),
|
||||
name: z.string().meta({
|
||||
description: "The name of the product",
|
||||
example: "Pro Plan",
|
||||
}),
|
||||
|
||||
group: z.string().nullable().meta({
|
||||
description: "The group of the product",
|
||||
example: "product_set_1",
|
||||
}),
|
||||
group: z.string().nullable().meta({
|
||||
description: "The group of the product",
|
||||
example: "product_set_1",
|
||||
}),
|
||||
|
||||
env: z.enum(AppEnv).meta({
|
||||
description: "The environment of the product",
|
||||
example: "production",
|
||||
}),
|
||||
env: z.enum(AppEnv).meta({
|
||||
description: "The environment of the product",
|
||||
example: "production",
|
||||
}),
|
||||
|
||||
is_add_on: z.boolean().meta({
|
||||
description:
|
||||
"Whether the product is an add-on and can be purchased alongside other products",
|
||||
example: true,
|
||||
}),
|
||||
is_add_on: z.boolean().meta({
|
||||
description:
|
||||
"Whether the product is an add-on and can be purchased alongside other products",
|
||||
example: true,
|
||||
}),
|
||||
|
||||
is_default: z.boolean().meta({
|
||||
description: "Whether the product is the default product",
|
||||
example: true,
|
||||
}),
|
||||
is_default: z.boolean().meta({
|
||||
description: "Whether the product is the default product",
|
||||
example: true,
|
||||
}),
|
||||
|
||||
archived: z.boolean({ message: "archived should be a boolean" }).meta({
|
||||
description:
|
||||
"Whether this product has been archived and is no longer available",
|
||||
example: false,
|
||||
}),
|
||||
archived: z.boolean({ message: "archived should be a boolean" }).meta({
|
||||
description:
|
||||
"Whether this product has been archived and is no longer available",
|
||||
example: false,
|
||||
}),
|
||||
|
||||
version: z.number().meta({
|
||||
description: "The version of the product",
|
||||
example: 1,
|
||||
}),
|
||||
version: z.number().meta({
|
||||
description: "The version of the product",
|
||||
example: 1,
|
||||
}),
|
||||
|
||||
created_at: z.number().meta({
|
||||
description:
|
||||
"The timestamp of when the product was created in milliseconds since epoch",
|
||||
example: 1759247877000,
|
||||
}),
|
||||
created_at: z.number().meta({
|
||||
description:
|
||||
"The timestamp of when the product was created in milliseconds since epoch",
|
||||
example: 1759247877000,
|
||||
}),
|
||||
|
||||
items: z.array(APIProductItemSchema).meta({
|
||||
description:
|
||||
"Array of product items that define the features and pricing",
|
||||
example: [
|
||||
{
|
||||
feature_id: "<string>",
|
||||
feature_type: "single_use",
|
||||
included_usage: 123,
|
||||
interval: "<string>",
|
||||
usage_model: "prepaid",
|
||||
price: 123,
|
||||
billing_units: 123,
|
||||
entity_feature_id: "<string>",
|
||||
reset_usage_when_enabled: true,
|
||||
tiers: [
|
||||
{
|
||||
to: 123,
|
||||
amount: 123,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
free_trial: APIFreeTrialSchema.nullable().meta({
|
||||
description: "Free trial configuration for this product, if available",
|
||||
example: {
|
||||
duration: "<string>",
|
||||
length: 123,
|
||||
unique_fingerprint: true,
|
||||
items: z.array(ApiProductItemSchema).meta({
|
||||
description: "Array of product items that define the features and pricing",
|
||||
example: [
|
||||
{
|
||||
feature_id: "<string>",
|
||||
feature_type: "single_use",
|
||||
included_usage: 123,
|
||||
interval: "<string>",
|
||||
usage_model: "prepaid",
|
||||
price: 123,
|
||||
billing_units: 123,
|
||||
entity_feature_id: "<string>",
|
||||
reset_usage_when_enabled: true,
|
||||
tiers: [
|
||||
{
|
||||
to: 123,
|
||||
amount: 123,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
||||
base_variant_id: z.string().nullable().meta({
|
||||
description: "ID of the base variant this product is derived from",
|
||||
example: "var_1234567890abcdef",
|
||||
}),
|
||||
free_trial: APIFreeTrialSchema.nullable().meta({
|
||||
description: "Free trial configuration for this product, if available",
|
||||
example: {
|
||||
duration: "<string>",
|
||||
length: 123,
|
||||
unique_fingerprint: true,
|
||||
},
|
||||
}),
|
||||
|
||||
scenario: z.enum(AttachScenario).optional().meta({
|
||||
description:
|
||||
"Scenario context for when this product is used in attach flows",
|
||||
example: "upgrade",
|
||||
}),
|
||||
base_variant_id: z.string().nullable().meta({
|
||||
description: "ID of the base variant this product is derived from",
|
||||
example: "var_1234567890abcdef",
|
||||
}),
|
||||
|
||||
properties: APIProductPropertiesSchema.optional().meta({
|
||||
description: "Additional properties and metadata for the product",
|
||||
example: {
|
||||
is_free: false,
|
||||
is_one_off: false,
|
||||
interval_group: "monthly",
|
||||
has_trial: true,
|
||||
updateable: true,
|
||||
},
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
description: "A product",
|
||||
id: "Product",
|
||||
});
|
||||
scenario: z.enum(AttachScenario).optional().meta({
|
||||
description:
|
||||
"Scenario context for when this product is used in attach flows",
|
||||
example: "upgrade",
|
||||
}),
|
||||
|
||||
export type APIProduct = z.infer<typeof APIProductSchema>;
|
||||
export type APIProductProperties = z.infer<typeof APIProductPropertiesSchema>;
|
||||
properties: ApiProductPropertiesSchema.optional().meta({
|
||||
description: "Additional properties and metadata for the product",
|
||||
example: {
|
||||
is_free: false,
|
||||
is_one_off: false,
|
||||
interval_group: "monthly",
|
||||
has_trial: true,
|
||||
updateable: true,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
export type ApiProduct = z.infer<typeof ApiProductSchema>;
|
||||
export type ApiProductProperties = z.infer<typeof ApiProductPropertiesSchema>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APIFeatureSchema } from "@api/features/apiFeature.js";
|
||||
import { ApiFeatureSchema } from "@api/features/apiFeature.js";
|
||||
import { Infinite } from "@models/productModels/productEnums.js";
|
||||
import {
|
||||
PriceTierSchema,
|
||||
@@ -9,47 +9,35 @@ import {
|
||||
} from "@models/productV2Models/productItemModels/productItemModels.js";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const APIProductItemSchema = z
|
||||
.object({
|
||||
// Feature stuff
|
||||
type: z.enum(ProductItemType).nullish(),
|
||||
feature_id: z.string().nullish(),
|
||||
feature_type: z.enum(ProductItemFeatureType).nullish(),
|
||||
export const ApiProductItemSchema = z.object({
|
||||
// Feature stuff
|
||||
type: z.enum(ProductItemType).nullish(),
|
||||
feature_id: z.string().nullish(),
|
||||
feature_type: z.enum(ProductItemFeatureType).nullish(),
|
||||
|
||||
// Feature response
|
||||
feature: APIFeatureSchema.nullish(),
|
||||
// Feature response
|
||||
feature: ApiFeatureSchema.nullish(),
|
||||
|
||||
included_usage: z.number().or(z.literal(Infinite)).nullish(),
|
||||
interval: z.enum(ProductItemInterval).nullish(),
|
||||
interval_count: z.number().nullish(),
|
||||
included_usage: z.number().or(z.literal(Infinite)).nullish(),
|
||||
interval: z.enum(ProductItemInterval).nullish(),
|
||||
interval_count: z.number().nullish(),
|
||||
|
||||
// Price config
|
||||
price: z.number().nullish(),
|
||||
tiers: z.array(PriceTierSchema).nullish(),
|
||||
usage_model: z.enum(UsageModel).nullish(),
|
||||
billing_units: z.number().nullish(), // amount per billing unit (eg. $9 / 250 units)
|
||||
reset_usage_when_enabled: z.boolean().nullish(),
|
||||
quantity: z.number().nullish(),
|
||||
next_cycle_quantity: z.number().nullish(),
|
||||
entity_feature_id: z.string().nullish(),
|
||||
// Price config
|
||||
price: z.number().nullish(),
|
||||
tiers: z.array(PriceTierSchema).nullish(),
|
||||
usage_model: z.enum(UsageModel).nullish(),
|
||||
billing_units: z.number().nullish(), // amount per billing unit (eg. $9 / 250 units)
|
||||
reset_usage_when_enabled: z.boolean().nullish(),
|
||||
quantity: z.number().nullish(),
|
||||
next_cycle_quantity: z.number().nullish(),
|
||||
entity_feature_id: z.string().nullish(),
|
||||
|
||||
display: z
|
||||
.object({
|
||||
primary_text: z.string(),
|
||||
secondary_text: z.string().nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
})
|
||||
.meta({
|
||||
id: "ProductItem",
|
||||
description: "A product item that defines a feature",
|
||||
example: {
|
||||
feature_id: "feature_1",
|
||||
feature_type: "single_use",
|
||||
included_usage: 123,
|
||||
interval: "monthly",
|
||||
usage_model: "prepaid",
|
||||
},
|
||||
});
|
||||
display: z
|
||||
.object({
|
||||
primary_text: z.string(),
|
||||
secondary_text: z.string().nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
});
|
||||
|
||||
export type APIProductItem = z.infer<typeof APIProductItemSchema>;
|
||||
export type ApiProductItem = z.infer<typeof ApiProductItemSchema>;
|
||||
|
||||
@@ -7,50 +7,40 @@ export const CreateProductItemParamsSchema = ProductItemSchema;
|
||||
|
||||
// Base product params
|
||||
|
||||
export const CreateProductV2ParamsSchema = z
|
||||
.object({
|
||||
id: z.string().nonempty().regex(idRegex),
|
||||
export const CreateProductV2ParamsSchema = z.object({
|
||||
id: z.string().nonempty().regex(idRegex),
|
||||
|
||||
name: z.string().refine((val) => val.length > 0, {
|
||||
name: z.string().refine((val) => val.length > 0, {
|
||||
message: "name must be a non-empty string",
|
||||
}),
|
||||
|
||||
is_add_on: z.boolean().default(false),
|
||||
is_default: z.boolean().default(false),
|
||||
version: z.number().optional(),
|
||||
group: z.string().default(""),
|
||||
|
||||
items: z.array(CreateProductItemParamsSchema).optional(),
|
||||
free_trial: CreateFreeTrialSchema.nullish().default(null),
|
||||
});
|
||||
|
||||
export const UpdateProductV2ParamsSchema = z.object({
|
||||
id: z.string().nonempty().regex(idRegex).optional(),
|
||||
name: z
|
||||
.string()
|
||||
.refine((val) => val.length > 0, {
|
||||
message: "name must be a non-empty string",
|
||||
}),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
is_add_on: z.boolean().default(false),
|
||||
is_default: z.boolean().default(false),
|
||||
version: z.number().optional(),
|
||||
group: z.string().default(""),
|
||||
is_add_on: z.boolean().optional(),
|
||||
is_default: z.boolean().optional(),
|
||||
version: z.number().optional(),
|
||||
group: z.string().optional(),
|
||||
archived: z.boolean().optional(),
|
||||
|
||||
items: z.array(CreateProductItemParamsSchema).optional(),
|
||||
free_trial: CreateFreeTrialSchema.nullish().default(null),
|
||||
})
|
||||
.meta({
|
||||
id: "CreateProductParams",
|
||||
description: "Create Product",
|
||||
});
|
||||
|
||||
export const UpdateProductV2ParamsSchema = z
|
||||
.object({
|
||||
id: z.string().nonempty().regex(idRegex).optional(),
|
||||
name: z
|
||||
.string()
|
||||
.refine((val) => val.length > 0, {
|
||||
message: "name must be a non-empty string",
|
||||
})
|
||||
.optional(),
|
||||
|
||||
is_add_on: z.boolean().optional(),
|
||||
is_default: z.boolean().optional(),
|
||||
version: z.number().optional(),
|
||||
group: z.string().optional(),
|
||||
archived: z.boolean().optional(),
|
||||
|
||||
items: z.array(CreateProductItemParamsSchema).optional(),
|
||||
free_trial: CreateFreeTrialSchema.nullish(),
|
||||
})
|
||||
.meta({
|
||||
id: "UpdateProductParams",
|
||||
description: "Update Product",
|
||||
});
|
||||
items: z.array(CreateProductItemParamsSchema).optional(),
|
||||
free_trial: CreateFreeTrialSchema.nullish(),
|
||||
});
|
||||
|
||||
export const UpdateProductQuerySchema = z.object({
|
||||
version: z.string().optional(),
|
||||
|
||||
@@ -2,7 +2,13 @@ import {
|
||||
CreateProductV2ParamsSchema,
|
||||
UpdateProductV2ParamsSchema,
|
||||
} from "@api/models.js";
|
||||
import { APIProductSchema } from "./apiProduct.js";
|
||||
import { ApiProductSchema } from "./apiProduct.js";
|
||||
|
||||
// Register schema with .meta() for OpenAPI spec generation
|
||||
const ApiProductWithMeta = ApiProductSchema.meta({
|
||||
id: "Product",
|
||||
description: "A product",
|
||||
});
|
||||
|
||||
export const productOps = {
|
||||
"/products": {
|
||||
@@ -17,7 +23,7 @@ export const productOps = {
|
||||
responses: {
|
||||
"200": {
|
||||
description: "200 OK",
|
||||
content: { "application/json": { schema: APIProductSchema } },
|
||||
content: { "application/json": { schema: ApiProductWithMeta } },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -32,7 +38,7 @@ export const productOps = {
|
||||
responses: {
|
||||
"200": {
|
||||
description: "200 OK",
|
||||
content: { "application/json": { schema: APIProductSchema } },
|
||||
content: { "application/json": { schema: ApiProductWithMeta } },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -21,8 +21,8 @@ export enum AffectedResource {
|
||||
*
|
||||
* @example
|
||||
* // Features changed from array to object in V1_2
|
||||
* const V1_2_FeaturesSchema = z.record(z.string(), APICusFeatureSchema);
|
||||
* const V1_1_FeaturesSchema = z.array(APICusFeatureSchema);
|
||||
* const V1_2_FeaturesSchema = z.record(z.string(), ApiCusFeatureSchema);
|
||||
* const V1_1_FeaturesSchema = z.array(ApiCusFeatureSchema);
|
||||
*
|
||||
* class V1_2_FeaturesArrayToObject extends VersionChange {
|
||||
* version = ApiVersion.V1_2;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ApiVersion } from "../ApiVersion.js";
|
||||
import type { ApiVersionClass } from "../ApiVersionClass.js";
|
||||
import { type ApiVersion, LATEST_VERSION } from "../ApiVersion.js";
|
||||
import { ApiVersionClass } from "../ApiVersionClass.js";
|
||||
import { getVersionsBetween } from "../versionRegistryUtils.js";
|
||||
import type { AffectedResource } from "./VersionChange.js";
|
||||
import { VersionChangeRegistryClass } from "./VersionChangeRegistryClass.js";
|
||||
@@ -12,12 +12,20 @@ import { VersionChangeRegistryClass } from "./VersionChangeRegistryClass.js";
|
||||
*
|
||||
* @param input - Data in the newest version format
|
||||
* @param data - Additional context data for transformations (optional)
|
||||
* @param currentVersion - Version of the input data
|
||||
* @param currentVersion - Version of the input data (defaults to LATEST_VERSION)
|
||||
* @param targetVersion - Version to transform to (older)
|
||||
* @param resource - Resource being transformed
|
||||
*
|
||||
* @example
|
||||
* // Data is in V1_2 format, transform to V1_1
|
||||
* // Data is in latest format, transform to V1_1 (currentVersion defaults to latest)
|
||||
* const v1_1_data = applyResponseVersionChanges({
|
||||
* input: latestCustomer,
|
||||
* targetVersion: new ApiVersionClass(ApiVersion.V1_1),
|
||||
* resource: AffectedResource.Customer
|
||||
* });
|
||||
*
|
||||
* @example
|
||||
* // Explicitly specify currentVersion
|
||||
* const v1_1_data = applyResponseVersionChanges({
|
||||
* input: v1_2_customer,
|
||||
* currentVersion: new ApiVersionClass(ApiVersion.V1_2),
|
||||
@@ -35,19 +43,21 @@ export function applyResponseVersionChanges<T = any, TData = any>({
|
||||
}: {
|
||||
input: T;
|
||||
data?: TData;
|
||||
currentVersion: ApiVersionClass;
|
||||
currentVersion?: ApiVersionClass;
|
||||
targetVersion: ApiVersionClass;
|
||||
resource: AffectedResource;
|
||||
}): T {
|
||||
// Default currentVersion to latest if not provided
|
||||
const _currentVersion = currentVersion || new ApiVersionClass(LATEST_VERSION);
|
||||
// If versions are equal, no transformation needed
|
||||
if (currentVersion.eq(targetVersion)) {
|
||||
if (_currentVersion.eq(targetVersion)) {
|
||||
return input;
|
||||
}
|
||||
|
||||
// If target is newer than current, throw error (can't transform forward)
|
||||
if (targetVersion.gt(currentVersion)) {
|
||||
if (targetVersion.gt(_currentVersion)) {
|
||||
throw new Error(
|
||||
`Cannot transform forward from ${currentVersion} to ${targetVersion}. ` +
|
||||
`Cannot transform forward from ${_currentVersion} to ${targetVersion}. ` +
|
||||
"Transforms only work backwards to older versions.",
|
||||
);
|
||||
}
|
||||
@@ -55,7 +65,7 @@ export function applyResponseVersionChanges<T = any, TData = any>({
|
||||
// Get all versions between current and target (exclusive of target, inclusive of current)
|
||||
const versionsToApply = getVersionsBetween({
|
||||
from: targetVersion.value,
|
||||
to: currentVersion.value,
|
||||
to: _currentVersion.value,
|
||||
}).filter((v) => v !== targetVersion.value); // Exclude target itself
|
||||
|
||||
// Sort versions from newest to oldest (we apply backwards)
|
||||
@@ -204,12 +214,14 @@ export function applyRequestVersionChanges<T = any, TData = any>({
|
||||
* expandArray.push(CusExpand.Invoices);
|
||||
* }
|
||||
*/
|
||||
export function isChangeActive(
|
||||
targetVersion: ApiVersionClass,
|
||||
// biome-ignore lint/suspicious/noExplicitAny: Generic version change class constructor
|
||||
changeClass: new () => any,
|
||||
): boolean {
|
||||
const change = new changeClass();
|
||||
export function isBeforeChange({
|
||||
targetVersion,
|
||||
versionChange,
|
||||
}: {
|
||||
targetVersion: ApiVersionClass;
|
||||
versionChange: new () => any;
|
||||
}): boolean {
|
||||
const change = new versionChange();
|
||||
return targetVersion.lt(change.version);
|
||||
}
|
||||
|
||||
@@ -226,7 +238,7 @@ export function applyResponseVersionChangesToArray<T = any, TData = any>({
|
||||
}: {
|
||||
inputArray: T[];
|
||||
data?: TData;
|
||||
currentVersion: ApiVersionClass;
|
||||
currentVersion?: ApiVersionClass;
|
||||
targetVersion: ApiVersionClass;
|
||||
resource: AffectedResource;
|
||||
}): T[] {
|
||||
|
||||
@@ -6,11 +6,45 @@
|
||||
export type { ApiVersionString } from "./ApiVersion.js";
|
||||
export { API_VERSIONS, ApiVersion, LATEST_VERSION } from "./ApiVersion.js";
|
||||
export { ApiVersionClass } from "./ApiVersionClass.js";
|
||||
|
||||
// Conversion utilities
|
||||
export {
|
||||
calVerToSemVer,
|
||||
legacyToSemVer,
|
||||
parseVersion,
|
||||
semVerToCalVer,
|
||||
semVerToLegacy,
|
||||
} from "./convertVersionUtils.js";
|
||||
// Org-specific utilities (deprecated)
|
||||
export { getOrgApiVersion, toLegacyVersion } from "./orgVersionUtils.js";
|
||||
// Branching utilities
|
||||
export {
|
||||
ifVersion,
|
||||
requireVersion,
|
||||
versionRange,
|
||||
versionSwitch,
|
||||
versionTernary,
|
||||
} from "./versionBranchUtils.js";
|
||||
export {
|
||||
applyRequestVersionChanges,
|
||||
applyRequestVersionChangesToArray,
|
||||
applyResponseVersionChanges,
|
||||
// Deprecated aliases (for backward compatibility)
|
||||
applyResponseVersionChanges as applyVersionChanges,
|
||||
applyResponseVersionChangesToArray,
|
||||
applyResponseVersionChangesToArray as applyVersionChangesToArray,
|
||||
getChangesForResource,
|
||||
isBeforeChange,
|
||||
} from "./versionChangeUtils/applyVersionChanges.js";
|
||||
// Version changes
|
||||
export {
|
||||
AffectedResource,
|
||||
VersionChange,
|
||||
type VersionChangeConstructor,
|
||||
} from "./versionChangeUtils/VersionChange.js";
|
||||
export { VersionChangeRegistryClass } from "./versionChangeUtils/VersionChangeRegistryClass.js";
|
||||
// Version registry
|
||||
export type { VersionMetadata } from "./versionRegistry.js";
|
||||
export { VERSION_REGISTRY } from "./versionRegistry.js";
|
||||
|
||||
// Version registry utilities
|
||||
export {
|
||||
CALVER_TO_SEMVER_MAP,
|
||||
@@ -20,45 +54,5 @@ export {
|
||||
isValidVersion,
|
||||
} from "./versionRegistryUtils.js";
|
||||
|
||||
// Conversion utilities
|
||||
export {
|
||||
calVerToSemVer,
|
||||
legacyToSemVer,
|
||||
parseVersion,
|
||||
semVerToCalVer,
|
||||
semVerToLegacy,
|
||||
} from "./convertVersionUtils.js";
|
||||
|
||||
// Org-specific utilities (deprecated)
|
||||
export { getOrgApiVersion, toLegacyVersion } from "./orgVersionUtils.js";
|
||||
|
||||
// Branching utilities
|
||||
export {
|
||||
ifVersion,
|
||||
requireVersion,
|
||||
versionRange,
|
||||
versionSwitch,
|
||||
versionTernary,
|
||||
} from "./versionBranchUtils.js";
|
||||
|
||||
// Version changes
|
||||
export {
|
||||
AffectedResource,
|
||||
VersionChange,
|
||||
type VersionChangeConstructor,
|
||||
} from "./versionChangeUtils/VersionChange.js";
|
||||
export { VersionChangeRegistryClass } from "./versionChangeUtils/VersionChangeRegistryClass.js";
|
||||
export {
|
||||
applyResponseVersionChanges,
|
||||
applyResponseVersionChangesToArray,
|
||||
applyRequestVersionChanges,
|
||||
applyRequestVersionChangesToArray,
|
||||
getChangesForResource,
|
||||
isChangeActive,
|
||||
// Deprecated aliases (for backward compatibility)
|
||||
applyResponseVersionChanges as applyVersionChanges,
|
||||
applyResponseVersionChangesToArray as applyVersionChangesToArray,
|
||||
} from "./versionChangeUtils/applyVersionChanges.js";
|
||||
|
||||
// Auto-register all version changes
|
||||
import "./versionChangeUtils/versionChangeRegistry.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { APIProduct } from "@api/products/apiProduct.js";
|
||||
import type { ApiProduct } from "@api/products/apiProduct.js";
|
||||
import type { Infinite } from "../productModels/productEnums.js";
|
||||
import type { UsageModel } from "../productV2Models/productItemModels/productItemModels.js";
|
||||
|
||||
@@ -59,7 +59,7 @@ export interface CheckProductPreview {
|
||||
currency: string;
|
||||
};
|
||||
|
||||
product?: APIProduct;
|
||||
product?: ApiProduct;
|
||||
payment_method?: any;
|
||||
}
|
||||
|
||||
@@ -76,5 +76,5 @@ export interface CheckFeaturePreview {
|
||||
feature_id: string;
|
||||
feature_name: string;
|
||||
|
||||
products: APIProduct[];
|
||||
products: ApiProduct[];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { APICusProductSchema } from "@api/customers/components/apiCusProduct.js";
|
||||
import { APICusFeatureSchema } from "@api/customers/cusFeatures/apiCusFeature.js";
|
||||
import { ApiCusFeatureSchema } from "@api/customers/cusFeatures/apiCusFeature.js";
|
||||
import { APIInvoiceSchema } from "@api/others/apiInvoice.js";
|
||||
import { z } from "zod/v4";
|
||||
import { AppEnv } from "../../genModels/genEnums.js";
|
||||
@@ -13,7 +13,7 @@ export const EntityResponseSchema = z.object({
|
||||
created_at: z.number(),
|
||||
env: z.enum(AppEnv),
|
||||
products: z.array(APICusProductSchema).optional(),
|
||||
features: z.record(z.string(), APICusFeatureSchema).optional(),
|
||||
features: z.record(z.string(), ApiCusFeatureSchema).optional(),
|
||||
invoices: z.array(APIInvoiceSchema).optional(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FullCustomerEntitlement } from "../../models/cusProductModels/cusEntModels/cusEntModels.js";
|
||||
import type { FullCustomerEntitlement } from "../../models/cusProductModels/cusEntModels/cusEntModels.js";
|
||||
import { notNullish, nullish } from "../utils.js";
|
||||
|
||||
export const getSummedEntityBalances = ({
|
||||
@@ -16,16 +16,16 @@ export const getSummedEntityBalances = ({
|
||||
}
|
||||
|
||||
return {
|
||||
balance: Object.values(cusEnt.entities!).reduce(
|
||||
balance: Object.values(cusEnt.entities).reduce(
|
||||
(acc, curr) => acc + curr.balance,
|
||||
0,
|
||||
),
|
||||
adjustment: Object.values(cusEnt.entities!).reduce(
|
||||
adjustment: Object.values(cusEnt.entities).reduce(
|
||||
(acc, curr) => acc + curr.adjustment,
|
||||
0,
|
||||
),
|
||||
unused: 0,
|
||||
count: Object.values(cusEnt.entities!).length,
|
||||
count: Object.values(cusEnt.entities).length,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -36,9 +36,7 @@ export const getCusEntBalance = ({
|
||||
cusEnt: FullCustomerEntitlement;
|
||||
entityId?: string | null;
|
||||
}) => {
|
||||
let entitlement = cusEnt.entitlement;
|
||||
let ent = cusEnt.entitlement;
|
||||
let feature = ent.feature;
|
||||
const entitlement = cusEnt.entitlement;
|
||||
|
||||
if (notNullish(entitlement.entity_feature_id)) {
|
||||
if (nullish(entityId)) {
|
||||
@@ -46,8 +44,8 @@ export const getCusEntBalance = ({
|
||||
cusEnt,
|
||||
});
|
||||
} else {
|
||||
let entityBalance = cusEnt.entities?.[entityId!]?.balance;
|
||||
let adjustment = cusEnt.entities?.[entityId!]?.adjustment || 0;
|
||||
const entityBalance = cusEnt.entities?.[entityId]?.balance;
|
||||
const adjustment = cusEnt.entities?.[entityId]?.adjustment || 0;
|
||||
|
||||
if (nullish(entityBalance)) {
|
||||
return { balance: 0, adjustment: 0, unused: 0, count: 1 };
|
||||
@@ -63,8 +61,8 @@ export const getCusEntBalance = ({
|
||||
}
|
||||
|
||||
return {
|
||||
balance: cusEnt.balance,
|
||||
adjustment: cusEnt.adjustment,
|
||||
balance: cusEnt.balance || 0,
|
||||
adjustment: cusEnt.adjustment || 0,
|
||||
unused: cusEnt.replaceables?.length || 0,
|
||||
count: 1,
|
||||
};
|
||||
|
||||
107
shared/utils/cusEntUtils/convertCusEntUtils.ts
Normal file
107
shared/utils/cusEntUtils/convertCusEntUtils.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { Decimal } from "decimal.js";
|
||||
import {
|
||||
entToOptions,
|
||||
type FullCusEntWithFullCusProduct,
|
||||
type FullCustomerEntitlement,
|
||||
getCusEntBalance,
|
||||
getStartingBalance,
|
||||
} from "../../index.js";
|
||||
import { cusEntToCusPrice } from "../productUtils/convertUtils.js";
|
||||
import { getRolloverFields } from "./getRolloverFields.js";
|
||||
|
||||
export const cusEntToKey = ({
|
||||
cusEnt,
|
||||
}: {
|
||||
cusEnt: FullCustomerEntitlement;
|
||||
}) => {
|
||||
const ent = cusEnt.entitlement;
|
||||
return `${ent.interval || "null"}-${ent.interval_count || 1}-${ent.feature.id}`;
|
||||
};
|
||||
|
||||
export const cusEntToBalance = ({
|
||||
cusEnt,
|
||||
entityId,
|
||||
withRollovers = false,
|
||||
}: {
|
||||
cusEnt: FullCustomerEntitlement;
|
||||
entityId?: string;
|
||||
withRollovers?: boolean;
|
||||
}) => {
|
||||
const { balance } = getCusEntBalance({
|
||||
cusEnt,
|
||||
entityId,
|
||||
});
|
||||
|
||||
const rollover = getRolloverFields({
|
||||
cusEnt,
|
||||
entityId,
|
||||
});
|
||||
|
||||
if (withRollovers && rollover) {
|
||||
return balance + rollover.balance;
|
||||
}
|
||||
|
||||
return balance;
|
||||
};
|
||||
|
||||
export const cusEntToIncludedUsage = ({
|
||||
cusEnt,
|
||||
entityId,
|
||||
withRollovers = false,
|
||||
}: {
|
||||
cusEnt: FullCusEntWithFullCusProduct;
|
||||
entityId?: string;
|
||||
withRollovers?: boolean;
|
||||
}) => {
|
||||
const rollover = getRolloverFields({
|
||||
cusEnt,
|
||||
entityId,
|
||||
});
|
||||
|
||||
const { count: entityCount } = getCusEntBalance({
|
||||
cusEnt,
|
||||
entityId,
|
||||
});
|
||||
|
||||
const cusProduct = cusEnt.customer_product;
|
||||
const options = entToOptions({
|
||||
ent: cusEnt.entitlement,
|
||||
options: cusProduct.options,
|
||||
});
|
||||
|
||||
const cusPrice = cusEntToCusPrice({ cusEnt });
|
||||
const startingBalance = getStartingBalance({
|
||||
entitlement: cusEnt.entitlement,
|
||||
options: options || undefined,
|
||||
relatedPrice: cusPrice?.price,
|
||||
productQuantity: cusProduct.quantity || 1,
|
||||
});
|
||||
|
||||
const total = new Decimal(startingBalance).mul(entityCount).toNumber();
|
||||
|
||||
if (withRollovers && rollover) {
|
||||
return total + rollover.balance + rollover.usage;
|
||||
}
|
||||
|
||||
return total;
|
||||
|
||||
// if (rollover) {
|
||||
// total = new Decimal(total)
|
||||
// .add(rollover.balance)
|
||||
// .add(rollover.usage)
|
||||
// .toNumber();
|
||||
// }
|
||||
};
|
||||
|
||||
export const cusEntToUsageLimit = ({
|
||||
cusEnt,
|
||||
}: {
|
||||
cusEnt: FullCusEntWithFullCusProduct;
|
||||
}) => {
|
||||
const startingBalance = cusEntToIncludedUsage({
|
||||
cusEnt,
|
||||
});
|
||||
|
||||
if (cusEnt.entitlement.usage_limit) return cusEnt.entitlement.usage_limit;
|
||||
return startingBalance;
|
||||
};
|
||||
9
shared/utils/cusEntUtils/cusEntUtils.ts
Normal file
9
shared/utils/cusEntUtils/cusEntUtils.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { FullCustomerEntitlement } from "@models/cusProductModels/cusEntModels/cusEntModels.js";
|
||||
|
||||
export const formatCusEnt = ({
|
||||
cusEnt,
|
||||
}: {
|
||||
cusEnt: FullCustomerEntitlement;
|
||||
}) => {
|
||||
return `${cusEnt.entitlement.feature_id} (${cusEnt.entitlement.interval}) (${cusEnt.balance})`;
|
||||
};
|
||||
106
shared/utils/cusEntUtils/getRolloverFields.ts
Normal file
106
shared/utils/cusEntUtils/getRolloverFields.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import type { FullCustomerEntitlement } from "@models/cusProductModels/cusEntModels/cusEntModels.js";
|
||||
import { notNullish } from "@utils/utils.js";
|
||||
|
||||
type CusRolloverInfo = {
|
||||
balance: number;
|
||||
usage: number;
|
||||
expires_at: number | null;
|
||||
};
|
||||
interface RolloverFields {
|
||||
balance: number;
|
||||
usage: number;
|
||||
rollovers: CusRolloverInfo[];
|
||||
}
|
||||
export const getRolloverFields = ({
|
||||
cusEnt,
|
||||
entityId,
|
||||
}: {
|
||||
cusEnt: FullCustomerEntitlement;
|
||||
entityId?: string;
|
||||
}): RolloverFields | undefined => {
|
||||
const hasRollover = notNullish(cusEnt.entitlement.rollover);
|
||||
if (!hasRollover) return;
|
||||
|
||||
const rollovers = cusEnt.rollovers || [];
|
||||
|
||||
if (cusEnt.entitlement.entity_feature_id) {
|
||||
if (entityId) {
|
||||
return rollovers.reduce(
|
||||
(acc, rollover) => {
|
||||
if (rollover.entities[entityId]) {
|
||||
return {
|
||||
balance: acc.balance + rollover.entities[entityId].balance,
|
||||
usage: acc.usage + rollover.entities[entityId].usage,
|
||||
rollovers: [
|
||||
...acc.rollovers,
|
||||
{
|
||||
balance: rollover.entities[entityId].balance,
|
||||
usage: rollover.entities[entityId].usage,
|
||||
expires_at: rollover.expires_at,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
balance: 0,
|
||||
usage: 0,
|
||||
rollovers: [] as CusRolloverInfo[],
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return rollovers.reduce(
|
||||
(acc, rollover) => {
|
||||
let newBalance = 0;
|
||||
let newUsage = 0;
|
||||
|
||||
for (const entityId in rollover.entities) {
|
||||
newBalance += rollover.entities[entityId].balance;
|
||||
newUsage += rollover.entities[entityId].usage;
|
||||
}
|
||||
|
||||
return {
|
||||
balance: acc.balance + newBalance,
|
||||
usage: acc.usage + newUsage,
|
||||
rollovers: [
|
||||
...acc.rollovers,
|
||||
{
|
||||
balance: newBalance,
|
||||
usage: newUsage,
|
||||
expires_at: rollover.expires_at,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
{
|
||||
balance: 0,
|
||||
usage: 0,
|
||||
rollovers: [] as CusRolloverInfo[],
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return rollovers.reduce(
|
||||
(acc, rollover) => {
|
||||
return {
|
||||
balance: acc.balance + rollover.balance,
|
||||
usage: acc.usage + rollover.usage,
|
||||
rollovers: [
|
||||
...acc.rollovers,
|
||||
{
|
||||
balance: rollover.balance,
|
||||
usage: rollover.usage,
|
||||
expires_at: rollover.expires_at,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
{
|
||||
balance: 0,
|
||||
usage: 0,
|
||||
rollovers: [] as CusRolloverInfo[],
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
45
shared/utils/cusEntUtils/getStartingBalance.ts
Normal file
45
shared/utils/cusEntUtils/getStartingBalance.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { FeatureOptions } from "@models/cusProductModels/cusProductModels.js";
|
||||
import type { Entitlement } from "@models/productModels/entModels/entModels.js";
|
||||
import { BillingType } from "@models/productModels/priceModels/priceEnums.js";
|
||||
import type { Price } from "@models/productModels/priceModels/priceModels.js";
|
||||
import { getBillingType } from "@utils/productUtils/priceUtils.js";
|
||||
import { nullish } from "@utils/utils.js";
|
||||
|
||||
export const getStartingBalance = ({
|
||||
entitlement,
|
||||
options,
|
||||
relatedPrice,
|
||||
productQuantity,
|
||||
}: {
|
||||
entitlement: Entitlement;
|
||||
options?: FeatureOptions;
|
||||
relatedPrice?: Price;
|
||||
productQuantity?: number;
|
||||
}) => {
|
||||
// 1. No related price
|
||||
if (!relatedPrice) {
|
||||
return (entitlement.allowance || 0) * (productQuantity || 1);
|
||||
}
|
||||
|
||||
const config = relatedPrice.config;
|
||||
|
||||
const billingType = getBillingType(config);
|
||||
if (billingType !== BillingType.UsageInAdvance) {
|
||||
return entitlement.allowance || 0;
|
||||
}
|
||||
|
||||
const quantity = options?.quantity;
|
||||
const billingUnits = relatedPrice.config.billing_units;
|
||||
if (nullish(quantity) || nullish(billingUnits)) {
|
||||
return entitlement.allowance || 0;
|
||||
}
|
||||
|
||||
try {
|
||||
return (entitlement.allowance || 0) + quantity * billingUnits;
|
||||
} catch (_error) {
|
||||
console.log(
|
||||
"WARNING: Failed to return quantity * billing units, returning allowance...",
|
||||
);
|
||||
return entitlement.allowance || 0;
|
||||
}
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
import { APIFeatureSchema } from "@api/features/apiFeature.js";
|
||||
import { ApiFeatureSchema } from "@api/features/apiFeature.js";
|
||||
import type { CreditSchemaItem } from "../models/featureModels/featureConfig/creditConfig.js";
|
||||
import { FeatureType } from "../models/featureModels/featureEnums.js";
|
||||
import type { Feature } from "../models/featureModels/featureModels.js";
|
||||
|
||||
export const toAPIFeature = ({ feature }: { feature: Feature }) => {
|
||||
export const toApiFeature = ({ feature }: { feature: Feature }) => {
|
||||
// return FeatureResponseSchema.parse(feature);
|
||||
// 1. Get feature type
|
||||
let featureType = feature.type;
|
||||
@@ -19,7 +19,7 @@ export const toAPIFeature = ({ feature }: { feature: Feature }) => {
|
||||
}));
|
||||
}
|
||||
|
||||
return APIFeatureSchema.parse({
|
||||
return ApiFeatureSchema.parse({
|
||||
id: feature.id,
|
||||
name: feature.name,
|
||||
type: featureType,
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
// Cus ent utils
|
||||
export * from "./cusEntUtils/balanceUtils.js";
|
||||
export * from "./cusEntUtils/convertCusEntUtils.js";
|
||||
export * from "./cusEntUtils/cusEntUtils.js";
|
||||
export * from "./cusEntUtils/getStartingBalance.js";
|
||||
export * from "./cusEntUtils/sortCusEntsForDeduction.js";
|
||||
// Cus product utils
|
||||
export * from "./cusProductUtils/classifyCusProduct.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APIProductItemSchema } from "@api/products/apiProductItem.js";
|
||||
import { ApiProductItemSchema } from "@api/products/apiProductItem.js";
|
||||
import { Decimal } from "decimal.js";
|
||||
import type { FeatureOptions } from "../../models/cusProductModels/cusProductModels.js";
|
||||
import type { Feature } from "../../models/featureModels/featureModels.js";
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
type ProductItem,
|
||||
UsageModel,
|
||||
} from "../../models/productV2Models/productItemModels/productItemModels.js";
|
||||
import { toAPIFeature } from "../featureUtils.js";
|
||||
import { toApiFeature } from "../featureUtils.js";
|
||||
import { getProductItemDisplay } from "../productDisplayUtils.js";
|
||||
import { notNullish } from "../utils.js";
|
||||
import { getItemType } from "./getItemType.js";
|
||||
@@ -135,10 +135,10 @@ export const getProductItemResponse = ({
|
||||
}
|
||||
|
||||
const feature = features.find((f) => f.id === item.feature_id);
|
||||
return APIProductItemSchema.parse({
|
||||
return ApiProductItemSchema.parse({
|
||||
type,
|
||||
...item,
|
||||
feature: feature ? toAPIFeature({ feature }) : null,
|
||||
feature: feature ? toApiFeature({ feature }) : null,
|
||||
display: withDisplay ? display : undefined,
|
||||
...priceData,
|
||||
quantity,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import type { FullCusEntWithFullCusProduct } from "@models/cusProductModels/cusEntModels/cusEntWithProduct.js";
|
||||
import type { FullCustomerPrice } from "@models/cusProductModels/cusPriceModels/cusPriceModels.js";
|
||||
import type { FeatureOptions } from "@models/cusProductModels/cusProductModels.js";
|
||||
import type {
|
||||
Entitlement,
|
||||
EntitlementWithFeature,
|
||||
@@ -54,3 +57,32 @@ export const priceToEnt = ({
|
||||
ent.internal_product_id === price.internal_product_id,
|
||||
);
|
||||
};
|
||||
|
||||
export const entToOptions = ({
|
||||
ent,
|
||||
options,
|
||||
}: {
|
||||
ent: Entitlement;
|
||||
options: FeatureOptions[];
|
||||
}) => {
|
||||
return options.find(
|
||||
(option) => option.internal_feature_id === ent.internal_feature_id,
|
||||
);
|
||||
};
|
||||
|
||||
export const cusEntToCusPrice = ({
|
||||
cusEnt,
|
||||
}: {
|
||||
cusEnt: FullCusEntWithFullCusProduct;
|
||||
}) => {
|
||||
const cusProduct = cusEnt.customer_product;
|
||||
const cusPrices = cusProduct.customer_prices;
|
||||
return cusPrices.find((cusPrice: FullCustomerPrice) => {
|
||||
const productMatch =
|
||||
cusPrice.customer_product_id === cusEnt.customer_product_id;
|
||||
|
||||
const entMatch = cusPrice.price.entitlement_id === cusEnt.entitlement.id;
|
||||
|
||||
return productMatch && entMatch;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -8,3 +8,7 @@ export const notNullish = <T>(value: T | null | undefined): value is T =>
|
||||
value !== null && value !== undefined;
|
||||
|
||||
export const idRegex = /^[a-zA-Z0-9_-]+$/;
|
||||
|
||||
export const sumValues = (vals: number[]) => {
|
||||
return vals.reduce((acc, curr) => acc + curr, 0);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
APIFeatureType,
|
||||
ApiFeatureType,
|
||||
type CreateFeature,
|
||||
FeatureType,
|
||||
} from "@autumn/shared";
|
||||
@@ -18,12 +18,12 @@ export const SelectFeatureType = ({
|
||||
const featureType = feature.type;
|
||||
const usageType = feature.config?.usage_type;
|
||||
|
||||
const setFeatureType = (type: APIFeatureType) => {
|
||||
const setFeatureType = (type: ApiFeatureType) => {
|
||||
// 1. If type is boolean
|
||||
if (type === APIFeatureType.Boolean) {
|
||||
if (type === ApiFeatureType.Boolean) {
|
||||
setFeature({
|
||||
...feature,
|
||||
type: APIFeatureType.Boolean,
|
||||
type: ApiFeatureType.Boolean,
|
||||
config: undefined,
|
||||
});
|
||||
} else {
|
||||
@@ -73,7 +73,7 @@ export const SelectFeatureType = ({
|
||||
description="Features that are either enabled or disabled, like 'premium models'"
|
||||
icon={<ToggleLeft className="h-3 w-3 text-t3" />}
|
||||
isSelected={featureType === FeatureType.Boolean}
|
||||
onClick={() => setFeatureType(APIFeatureType.Boolean)}
|
||||
onClick={() => setFeatureType(ApiFeatureType.Boolean)}
|
||||
/> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {
|
||||
APIFeatureType,
|
||||
ApiFeatureType,
|
||||
type CreateFeature,
|
||||
FeatureType,
|
||||
FeatureUsageType,
|
||||
@@ -19,12 +19,12 @@ export const SelectFeatureUsageType = ({
|
||||
const featureType = feature.type;
|
||||
const usageType = feature.config?.usage_type;
|
||||
|
||||
const setFeatureType = (type: APIFeatureType) => {
|
||||
const setFeatureType = (type: ApiFeatureType) => {
|
||||
// 1. If type is boolean
|
||||
if (type === APIFeatureType.Boolean) {
|
||||
if (type === ApiFeatureType.Boolean) {
|
||||
setFeature({
|
||||
...feature,
|
||||
type: APIFeatureType.Boolean,
|
||||
type: ApiFeatureType.Boolean,
|
||||
config: undefined,
|
||||
});
|
||||
} else {
|
||||
@@ -47,7 +47,7 @@ export const SelectFeatureUsageType = ({
|
||||
featureType === FeatureType.Metered &&
|
||||
usageType === FeatureUsageType.Single
|
||||
}
|
||||
onClick={() => setFeatureType(APIFeatureType.SingleUsage)}
|
||||
onClick={() => setFeatureType(ApiFeatureType.SingleUsage)}
|
||||
/>
|
||||
<SelectType
|
||||
title="Continuous"
|
||||
@@ -57,14 +57,14 @@ export const SelectFeatureUsageType = ({
|
||||
featureType === FeatureType.Metered &&
|
||||
usageType === FeatureUsageType.Continuous
|
||||
}
|
||||
onClick={() => setFeatureType(APIFeatureType.ContinuousUse)}
|
||||
onClick={() => setFeatureType(ApiFeatureType.ContinuousUse)}
|
||||
/>
|
||||
{/* <SelectType
|
||||
title="Boolean"
|
||||
description="Features that are either enabled or disabled, like 'premium models'"
|
||||
icon={<ToggleLeft className="h-3 w-3 text-t3" />}
|
||||
isSelected={featureType === FeatureType.Boolean}
|
||||
onClick={() => setFeatureType(APIFeatureType.Boolean)}
|
||||
onClick={() => setFeatureType(ApiFeatureType.Boolean)}
|
||||
/> */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user