fix: api customer schema

This commit is contained in:
John Yeo
2025-10-02 13:48:01 +01:00
parent 7aa5715f78
commit fd7361b0bd
49 changed files with 972 additions and 1185 deletions

View File

@@ -1,6 +1,6 @@
import { ErrCode } from "@autumn/shared";
import type { Context, Next } from "hono";
import type { HonoEnv } from "@/initHono.js";
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
import RecaseError from "@/utils/errorUtils.js";
import { floatToVersion } from "@/utils/versionUtils.js";

View File

@@ -1,6 +1,6 @@
import { type AppEnv, AuthType, ErrCode } from "@autumn/shared";
import type { Context, Next } from "hono";
import type { HonoEnv } from "@/initHono.js";
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { auth } from "@/utils/auth.js";
import RecaseError from "@/utils/errorUtils.js";

View File

@@ -3,7 +3,7 @@ import type { Context } from "hono";
import type { ContentfulStatusCode } from "hono/utils/http-status";
import Stripe from "stripe";
import { ZodError } from "zod/v4";
import type { HonoEnv } from "@/initHono.js";
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
import RecaseError, { formatZodError } from "@/utils/errorUtils.js";
/**

View File

@@ -1,5 +1,5 @@
import type { Context, Next } from "hono";
import type { HonoEnv } from "@/initHono.js";
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
/**
* Middleware to handle org-config header and merge with org config

View File

@@ -1,6 +1,6 @@
import { AuthType, ErrCode } from "@autumn/shared";
import type { Context, Next } from "hono";
import type { HonoEnv } from "@/initHono.js";
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
import { verifyKey } from "@/internal/dev/api-keys/apiKeyUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { betterAuthMiddleware } from "./betterAuthMiddleware.js";

View File

@@ -1,7 +1,7 @@
import { context, trace } from "@opentelemetry/api";
import type { Context, Next } from "hono";
import { logger } from "@/external/logtail/logtailUtils.js";
import type { HonoEnv } from "@/initHono.js";
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
const tracer = trace.getTracer("hono");

View File

@@ -1,25 +1,24 @@
import {
type APICusProduct,
APIVersion,
type AppEnv,
type Entity,
type EntityExpand,
type EntityResponse,
ErrCode,
type Feature,
type FullCusProduct,
type FullCustomer,
notNullish,
type Organization,
type Subscription,
} from "@autumn/shared";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { getCusWithCache } from "@/internal/customers/cusCache/getCusWithCache.js";
import { getCusFeaturesResponse } from "@/internal/customers/cusUtils/cusFeatureResponseUtils/getCusFeaturesResponse.js";
import { processFullCusProducts } from "@/internal/customers/cusUtils/cusProductResponseUtils/processFullCusProducts.js";
import RecaseError from "@/utils/errorUtils.js";
import { nullish } from "@/utils/genUtils.js";
import {
type AppEnv,
Feature,
type Entity,
EntityExpand,
type EntityResponse,
ErrCode,
type FullCusProduct,
type Organization,
type Subscription,
CusProductResponse,
notNullish,
FullCustomer,
APIVersion,
} from "@autumn/shared";
export const getSingleEntityResponse = async ({
entityId,
@@ -53,24 +52,24 @@ export const getSingleEntityResponse = async ({
if (org.config.entity_product) {
return (
notNullish(p.internal_entity_id) &&
p.internal_entity_id == entity.internal_id
p.internal_entity_id === entity.internal_id
);
}
return (
p.internal_entity_id == entity.internal_id ||
p.internal_entity_id === entity.internal_id ||
nullish(p.internal_entity_id)
);
},
);
let entitySubs = (fullCus.subscriptions || []).filter((s: Subscription) =>
const entitySubs = (fullCus.subscriptions || []).filter((s: Subscription) =>
entityCusProducts.some((p: FullCusProduct) =>
p.subscription_ids?.includes(s.stripe_id || ""),
),
);
let { main, addOns } = await processFullCusProducts({
const { main, addOns } = await processFullCusProducts({
fullCusProducts: entityCusProducts,
entity,
subs: entitySubs,
@@ -79,9 +78,9 @@ export const getSingleEntityResponse = async ({
features,
});
let products: CusProductResponse[] = [...main, ...addOns];
const products: APICusProduct[] = [...main, ...addOns];
let cusFeatures = await getCusFeaturesResponse({
const cusFeatures = await getCusFeaturesResponse({
cusProducts: entityCusProducts,
org,
entity,
@@ -128,7 +127,7 @@ export const getEntityResponse = async ({
logger: any;
skipCache?: boolean;
}) => {
let fullCus = await getCusWithCache({
const fullCus = await getCusWithCache({
db,
idOrInternalId: customerId,
org,
@@ -151,7 +150,7 @@ export const getEntityResponse = async ({
for (const entityId of entityIds) {
const entity = fullCus.entities.find(
(e: Entity) => e.id == entityId || e.internal_id == entityId,
(e: Entity) => e.id === entityId || e.internal_id === entityId,
);
if (!entity) {
@@ -162,7 +161,7 @@ export const getEntityResponse = async ({
});
}
let entityResponse = await getSingleEntityResponse({
const entityResponse = await getSingleEntityResponse({
entityId,
org,
env,

View File

@@ -1,19 +1,19 @@
import { getUnlimitedAndUsageAllowed } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import { cusEntMatchesFeature } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.js";
import { getCusBalances } from "@/internal/customers/cusUtils/cusFeatureResponseUtils/getCusBalances.js";
import { balancesToFeatureResponse } from "@/internal/customers/cusUtils/cusFeatureResponseUtils/balancesToFeatureResponse.js";
import {
CheckResponseSchema,
Feature,
CheckResultSchema,
type Feature,
FeatureType,
FullCusEntWithFullCusProduct,
FullCusProduct,
FullCustomer,
Organization,
type FullCusEntWithFullCusProduct,
type FullCusProduct,
type FullCustomer,
type Organization,
SuccessCode,
} from "@autumn/shared";
import { notNullish } from "@/utils/genUtils.js";
import { cusEntMatchesFeature } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.js";
import { getUnlimitedAndUsageAllowed } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import { balancesToFeatureResponse } from "@/internal/customers/cusUtils/cusFeatureResponseUtils/balancesToFeatureResponse.js";
import { getCusBalances } from "@/internal/customers/cusUtils/cusFeatureResponseUtils/getCusBalances.js";
import { featureToCreditSystem } from "@/internal/features/creditSystemUtils.js";
import { notNullish } from "@/utils/genUtils.js";
export const getFeatureToUse = ({
creditSystems,
@@ -25,12 +25,12 @@ export const getFeatureToUse = ({
cusEnts: FullCusEntWithFullCusProduct[];
}) => {
// 1. If there's a credit system
let featureCusEnts = cusEnts.filter((cusEnt) =>
const featureCusEnts = cusEnts.filter((cusEnt) =>
cusEntMatchesFeature({ cusEnt, feature }),
);
if (creditSystems.length > 0) {
let creditCusEnts = cusEnts.filter((cusEnt) =>
const creditCusEnts = cusEnts.filter((cusEnt) =>
cusEntMatchesFeature({ cusEnt, feature: creditSystems[0] }),
);
@@ -95,7 +95,7 @@ export const getV2CheckResponse = async ({
apiVersion,
});
let cusFeatures = balancesToFeatureResponse({
const cusFeatures = balancesToFeatureResponse({
cusEnts: featureCusEnts,
balances,
});
@@ -104,8 +104,8 @@ export const getV2CheckResponse = async ({
let allowed = false;
let totalPaidUsageAllowance = featureCusEnts.reduce((acc, ce) => {
let ent = ce.entitlement;
const totalPaidUsageAllowance = featureCusEnts.reduce((acc, ce) => {
const ent = ce.entitlement;
if (notNullish(ent.usage_limit)) {
return acc + ent.usage_limit! - (ent.allowance || 0);
}
@@ -122,7 +122,7 @@ export const getV2CheckResponse = async ({
}
let finalRequired = notNullish(requiredBalance) ? requiredBalance : 1;
if (featureToUse.type == FeatureType.CreditSystem) {
if (featureToUse.type === FeatureType.CreditSystem) {
finalRequired = featureToCreditSystem({
featureId: feature.id,
creditSystem: featureToUse,
@@ -130,7 +130,7 @@ export const getV2CheckResponse = async ({
});
}
return CheckResponseSchema.parse({
return CheckResultSchema.parse({
customer_id: fullCus.id,
feature_id: featureToUse.id,
entity_id: fullCus.entity?.id,

View File

@@ -1,34 +1,32 @@
import { Router } from "express";
import {
APIVersion,
AppEnv,
type AppEnv,
CreateEventSchema,
CusProductStatus,
EntityData,
type EntityData,
ErrCode,
EventInsert,
Feature,
type EventInsert,
type Feature,
FeatureType,
FullCustomer,
Organization,
type FullCustomer,
type Organization,
} from "@autumn/shared";
import { Router } from "express";
import { StatusCodes } from "http-status-codes";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { getOrCreateCustomer } from "@/internal/customers/cusUtils/getOrCreateCustomer.js";
import { creditSystemContainsFeature } from "@/internal/features/creditSystemUtils.js";
import { FeatureService } from "@/internal/features/FeatureService.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { JobName } from "@/queue/JobName.js";
import { addTaskToQueue } from "@/queue/queueUtils.js";
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
import { generateId, notNullish } from "@/utils/genUtils.js";
import { EventService } from "./EventService.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { handleUsageEvent } from "./usageRouter.js";
import { StatusCodes } from "http-status-codes";
import { getOrCreateCustomer } from "@/internal/customers/cusUtils/getOrCreateCustomer.js";
import { FeatureService } from "@/internal/features/FeatureService.js";
import { creditSystemContainsFeature } from "@/internal/features/creditSystemUtils.js";
import { addTaskToQueue } from "@/queue/queueUtils.js";
import { JobName } from "@/queue/JobName.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import { orgToVersion } from "@/utils/versionUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { EventService } from "./EventService.js";
import { getEventTimestamp } from "./eventUtils.js";
import { handleUsageEvent } from "./usageRouter.js";
export const eventsRouter: Router = Router();
@@ -99,7 +97,7 @@ const getEventAndCustomer = async ({
internal_entity_id: internalEntityId,
};
let event = await EventService.insert({ db, event: newEvent });
const event = await EventService.insert({ db, event: newEvent });
return { customer, event };
};
@@ -111,20 +109,20 @@ const getAffectedFeatures = async ({
req: any;
eventName: string;
}) => {
let features = await FeatureService.getFromReq(req);
const features = await FeatureService.getFromReq(req);
let featuresWithEvent = features.filter((feature) => {
const featuresWithEvent = features.filter((feature) => {
return (
feature.type == FeatureType.Metered &&
feature.type === FeatureType.Metered &&
feature.config.filters.some((filter: any) => {
return filter.value.includes(eventName);
})
);
});
let creditSystems = features.filter((cs: Feature) => {
const creditSystems = features.filter((cs: Feature) => {
return (
cs.type == FeatureType.CreditSystem &&
cs.type === FeatureType.CreditSystem &&
featuresWithEvent.some((f) =>
creditSystemContainsFeature({
creditSystem: cs,
@@ -156,7 +154,7 @@ export const handleEventSent = async ({
const { env, db } = req;
let eventName = event_data.event_name;
const eventName = event_data.event_name;
const org = await OrgService.getFromReq(req);
const features = await FeatureService.getFromReq(req);
@@ -166,7 +164,7 @@ export const handleEventSent = async ({
eventName,
});
if (affectedFeatures.length == 0) {
if (affectedFeatures.length === 0) {
throw new RecaseError({
message: `No features found for event_name ${event_data.event_name}`,
code: ErrCode.InvalidEventName,
@@ -236,19 +234,19 @@ eventsRouter.post("", async (req: any, res: any) => {
});
}
let { event, org }: any = await handleEventSent({
const { event, org }: any = await handleEventSent({
req,
customer_id: body.customer_id,
customer_data: body.customer_data,
event_data: body,
});
let apiVersion = orgToVersion({
const apiVersion = orgToVersion({
org,
reqApiVersion: req.apiVersion,
});
let response: any = {
const response: any = {
id: event?.id,
code: "event_received",
customer_id: body.customer_id,

View File

@@ -1,9 +1,9 @@
import {
ACTIVE_STATUSES,
APICusProductSchema,
APIVersion,
type AppEnv,
AttachScenario,
CusProductResponseSchema,
CusProductStatus,
cusProductToPrices,
type Entity,
@@ -508,28 +508,25 @@ export const processFullCusProduct = ({
}
if (apiVersion >= APIVersion.v1_1) {
if ((!subIds || subIds.length == 0) && trialing) {
if ((!subIds || subIds.length === 0) && trialing) {
stripeSubData = {
current_period_start: cusProduct.starts_at,
current_period_end: cusProduct.trial_ends_at,
};
}
return CusProductResponseSchema.parse({
return APICusProductSchema.parse({
id: cusProduct.product.id,
name: cusProduct.product.name,
group: cusProduct.product.group || null,
status: trialing ? CusProductStatus.Trialing : cusProduct.status,
// created_at: cusProduct.created_at,
canceled_at: cusProduct.canceled_at,
is_default: cusProduct.product.is_default || false,
is_add_on: cusProduct.product.is_add_on || false,
stripe_subscription_ids: cusProduct.subscription_ids || [],
started_at: cusProduct.starts_at,
// entity_id: cusProduct.entity_id,
entity_id: cusProduct.internal_entity_id
? entities?.find((e) => e.internal_id == cusProduct.internal_entity_id)
? entities?.find((e) => e.internal_id === cusProduct.internal_entity_id)
?.id
: cusProduct.entity_id || undefined,

View File

@@ -1,19 +1,19 @@
import {
CreditSchemaItem,
CusEntResponse,
type CreditSchemaItem,
type CusEntResponse,
CusEntResponseSchema,
CusEntResponseV2,
CusRollover,
Feature,
type CusEntResponseV2,
type CusRollover,
type Feature,
FeatureType,
FullCustomerEntitlement,
type FullCustomerEntitlement,
} from "@autumn/shared";
import { CusFeatureBalance } from "./getCusBalances.js";
import {
getCusFeatureType,
isCreditSystem,
} from "@/internal/features/featureUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import type { CusFeatureBalance } from "./getCusBalances.js";
export const sumValues = (
entList: CusEntResponse[],
@@ -29,7 +29,7 @@ export const sumValues = (
};
export const getEarliestNextResetAt = (entList: CusEntResponse[]) => {
let earliest = entList.reduce((acc, curr) => {
const earliest = entList.reduce((acc, curr) => {
if (curr.next_reset_at && curr.next_reset_at < acc) {
return curr.next_reset_at;
}
@@ -47,21 +47,21 @@ export const featuresToObject = ({
features: Feature[];
entList: CusEntResponse[];
}) => {
let featureObject: Record<string, CusEntResponseV2> = {};
const featureObject: Record<string, CusEntResponseV2> = {};
for (let entRes of entList) {
let feature = features.find((f) => f.id == entRes.feature_id)!;
let featureType = getCusFeatureType({ feature });
for (const entRes of entList) {
const feature = features.find((f) => f.id == entRes.feature_id)!;
const featureType = getCusFeatureType({ feature });
let featureId = feature.id;
let unlimited = entRes.unlimited;
let relatedEnts = entList.filter((e) => e.feature_id == featureId);
const featureId = feature.id;
const unlimited = entRes.unlimited;
const relatedEnts = entList.filter((e) => e.feature_id == featureId);
if (featureObject[featureId]) {
continue;
}
let includedUsage = sumValues(relatedEnts, "included_usage");
const includedUsage = sumValues(relatedEnts, "included_usage");
let usageLimit: number | undefined = sumValues(relatedEnts, "usage_limit");
if (notNullish(usageLimit) && usageLimit === includedUsage) {
usageLimit = undefined;
@@ -69,13 +69,13 @@ export const featuresToObject = ({
// console.log(`Feature ${featureId} list:`, relatedEnts);
let hasRollovers = relatedEnts.some((e) => notNullish(e.rollovers));
let rollovers = hasRollovers
const hasRollovers = relatedEnts.some((e) => notNullish(e.rollovers));
const rollovers = hasRollovers
? (relatedEnts
.flatMap((e) => e.rollovers)
.filter(notNullish) as CusRollover[])
: undefined;
let cusFeature: CusEntResponseV2 = {
const cusFeature: CusEntResponseV2 = {
id: featureId,
name: feature.name,
type: featureType,
@@ -124,11 +124,11 @@ export const balancesToFeatureResponse = ({
cusEnts: FullCustomerEntitlement[];
balances: CusFeatureBalance[];
}) => {
let features = cusEnts.map((cusEnt) => cusEnt.entitlement.feature);
const features = cusEnts.map((cusEnt) => cusEnt.entitlement.feature);
let entList: any = balances.map((b) => {
let isBoolean =
features.find((f: Feature) => f.id == b.feature_id)?.type ==
const isBoolean =
features.find((f: Feature) => f.id === b.feature_id)?.type ===
FeatureType.Boolean;
if (b.unlimited || isBoolean) {

View File

@@ -1,42 +1,27 @@
import {
APICusProductSchema,
APIVersion,
CusProductStatus,
type Entity,
type Feature,
type FixedPriceConfig,
type FullCusProduct,
type Organization,
PriceType,
type Subscription,
TierInfinite,
type UsagePriceConfig,
} from "@autumn/shared";
import {
getPriceOptions,
getUsageTier,
} from "@/internal/products/prices/priceUtils.js";
import { getProductResponse } from "@/internal/products/productUtils/productResponseUtils/getProductResponse.js";
import { BREAK_API_VERSION } from "@/utils/constants.js";
import { notNullish } from "@/utils/genUtils.js";
import {
FullCusProduct,
Organization,
Subscription,
PriceType,
FixedPriceConfig,
UsagePriceConfig,
TierInfinite,
APIVersion,
CusProductResponseSchema,
CusProductStatus,
Entity,
Feature,
} from "@autumn/shared";
import Stripe from "stripe";
import { getRelatedCusEnt } from "../../cusProducts/cusPrices/cusPriceUtils.js";
import { fullCusProductToProduct } from "../../cusProducts/cusProductUtils.js";
import {
getProductItemResponse,
getProductResponse,
} from "@/internal/products/productUtils/productResponseUtils/getProductResponse.js";
const getQuantityData = ({ cusProduct }: { cusProduct: FullCusProduct }) => {
return {
prepaid_quantities: cusProduct.options.map((o) => {
return {
quantity: o.quantity,
feature_id: o.feature_id,
};
}),
};
};
export const getCusProductResponse = async ({
cusProduct,
subs,
@@ -55,26 +40,27 @@ export const getCusProductResponse = async ({
// Process prices
const prices = cusProduct.customer_prices.map((cp) => {
let price = cp.price;
const price = cp.price;
if (price.config?.type == PriceType.Fixed) {
let config = price.config as FixedPriceConfig;
if (price.config?.type === PriceType.Fixed) {
const config = price.config as FixedPriceConfig;
return {
amount: config.amount,
interval: config.interval,
};
} else {
let config = price.config as UsagePriceConfig;
let priceOptions = getPriceOptions(price, cusProduct.options);
let usageTier = getUsageTier(price, priceOptions?.quantity!);
let cusEnt = getRelatedCusEnt({
const config = price.config as UsagePriceConfig;
const priceOptions = getPriceOptions(price, cusProduct.options);
const usageTier = getUsageTier(price, priceOptions?.quantity!);
const cusEnt = getRelatedCusEnt({
cusPrice: cp,
cusEnts: cusProduct.customer_entitlements,
});
let ent = cusEnt?.entitlement;
const ent = cusEnt?.entitlement;
let singleTier = ent?.allowance == 0 && config.usage_tiers.length == 1;
const singleTier =
ent?.allowance === 0 && config.usage_tiers.length === 1;
if (singleTier) {
return {
@@ -84,7 +70,7 @@ export const getCusProductResponse = async ({
};
} else {
// Add allowance to tiers
let allowance = ent?.allowance;
const allowance = ent?.allowance;
let tiers;
if (notNullish(allowance) && allowance! > 0) {
@@ -94,7 +80,7 @@ export const getCusProductResponse = async ({
amount: 0,
},
...config.usage_tiers.map((tier) => {
let isLastTier = tier.to == -1 || tier.to == TierInfinite;
const isLastTier = tier.to === -1 || tier.to === TierInfinite;
return {
to: isLastTier ? tier.to : Number(tier.to) + allowance!,
amount: tier.amount,
@@ -103,7 +89,7 @@ export const getCusProductResponse = async ({
];
} else {
tiers = config.usage_tiers.map((tier) => {
let isLastTier = tier.to == -1 || tier.to == TierInfinite;
const isLastTier = tier.to === -1 || tier.to === TierInfinite;
return {
to: isLastTier ? tier.to : Number(tier.to) + allowance!,
amount: tier.amount,
@@ -131,8 +117,8 @@ export const getCusProductResponse = async ({
subIds.length > 0 &&
org.config.api_version >= BREAK_API_VERSION
) {
let baseSub = subs?.find(
(s) => s.id == subIds[0] || (s as Subscription).stripe_id == subIds[0],
const baseSub = subs?.find(
(s) => s.id === subIds[0] || (s as Subscription).stripe_id === subIds[0],
);
stripeSubData = {
current_period_end: baseSub?.current_period_end
@@ -152,7 +138,7 @@ export const getCusProductResponse = async ({
}
if (apiVersion >= APIVersion.v1_1) {
if ((!subIds || subIds.length == 0) && trialing) {
if ((!subIds || subIds.length === 0) && trialing) {
stripeSubData = {
current_period_start: cusProduct.starts_at,
current_period_end: cusProduct.trial_ends_at,
@@ -167,35 +153,26 @@ export const getCusProductResponse = async ({
options: cusProduct.options,
});
return CusProductResponseSchema.parse({
return APICusProductSchema.parse({
id: fullProduct.id,
name: fullProduct.name,
group: fullProduct.group || null,
status: trialing ? CusProductStatus.Trialing : cusProduct.status,
canceled_at: cusProduct.canceled_at || null,
// canceled: cusProduct.canceled || false,
// trialing: trialing ? true : false,
is_default: fullProduct.is_default || false,
is_add_on: fullProduct.is_add_on || false,
version: fullProduct.version,
quantity: cusProduct.quantity,
// stripe_subscription_ids: cusProduct.subscription_ids || [],
started_at: cusProduct.starts_at,
entity_id: entity?.id || cusProduct.entity_id || undefined,
// entity_id: cusProduct.internal_entity_id
// ? entities?.find(
// (e: Entity) => e.internal_id == cusProduct.internal_entity_id
// )?.id
// : cusProduct.entity_id || undefined,
...stripeSubData,
items: v2Product.items,
});
} else {
let cusProductResponse = {
const cusProductResponse = {
id: cusProduct.product.id,
name: cusProduct.product.name,
group: cusProduct.product.group,

View File

@@ -1,20 +1,21 @@
import {
ACTIVE_STATUSES,
type APICusProduct,
APIVersion,
CusProductResponse,
Entity,
Feature,
Organization,
type CusProductStatus,
type Entity,
type Feature,
type Organization,
} from "@autumn/shared";
import { getCusProductResponse } from "./getCusProductResponse.js";
const mergeCusProductResponses = ({
cusProductResponses,
}: {
cusProductResponses: CusProductResponse[];
cusProductResponses: APICusProduct[];
}) => {
const getProductKey = (product: CusProductResponse) => {
let status = ACTIVE_STATUSES.includes(product.status)
const getProductKey = (product: APICusProduct) => {
const status = ACTIVE_STATUSES.includes(product.status as CusProductStatus)
? "active"
: product.status;
return `${product.id}:${status}`;
@@ -63,7 +64,7 @@ export const processFullCusProducts = async ({
let main = [];
let addOns = [];
for (const cusProduct of fullCusProducts) {
let processed = await getCusProductResponse({
const processed = await getCusProductResponse({
cusProduct,
subs,
org,
@@ -72,7 +73,7 @@ export const processFullCusProducts = async ({
features,
});
let isAddOn = cusProduct.product.is_add_on;
const isAddOn = cusProduct.product.is_add_on;
if (isAddOn) {
addOns.push(processed);
} else {
@@ -82,10 +83,10 @@ export const processFullCusProducts = async ({
if (apiVersion >= APIVersion.v1_1) {
main = mergeCusProductResponses({
cusProductResponses: main as CusProductResponse[],
cusProductResponses: main as APICusProduct[],
});
addOns = mergeCusProductResponses({
cusProductResponses: addOns as CusProductResponse[],
cusProductResponses: addOns as APICusProduct[],
});
}

View File

@@ -1,7 +1,10 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
APICusReferralSchema,
CusExpand,
type FullCustomer,
} from "@autumn/shared";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js";
import { CusExpand, FullCustomer } from "@autumn/shared";
import { CusReferralResponseSchema } from "@autumn/shared";
export const getCusReferrals = async ({
db,
@@ -16,7 +19,7 @@ export const getCusReferrals = async ({
return undefined;
}
let referred = await RewardRedemptionService.getByReferrer({
const referred = await RewardRedemptionService.getByReferrer({
db,
internalCustomerId: fullCus.internal_id,
withCustomer: true,
@@ -25,7 +28,7 @@ export const getCusReferrals = async ({
});
return referred.map((r) =>
CusReferralResponseSchema.parse({
APICusReferralSchema.parse({
program_id: r.reward_program?.id,
customer: {
id: r.customer.id,

View File

@@ -1,25 +1,25 @@
import { z } from "zod";
import {
type APIInvoice,
CusExpand,
Customer,
CustomerData,
type Customer,
type CustomerData,
ErrCode,
Feature,
FullCustomer,
Invoice,
InvoiceResponse,
Organization,
type Feature,
type FullCustomer,
type Invoice,
type Organization,
sortCusEntsForDeduction,
} from "@autumn/shared";
import { CusService } from "@/internal/customers/CusService.js";
import { InvoiceService } from "@/internal/invoices/InvoiceService.js";
import { StatusCodes } from "http-status-codes";
import { notNullish, nullish } from "@/utils/genUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { z } from "zod";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { CusService } from "@/internal/customers/CusService.js";
import {
InvoiceService,
processInvoice,
} from "@/internal/invoices/InvoiceService.js";
import RecaseError from "@/utils/errorUtils.js";
import { processInvoice } from "@/internal/invoices/InvoiceService.js";
import { notNullish, nullish } from "@/utils/genUtils.js";
import { refreshCusCache } from "../cusCache/updateCachedCus.js";
export const updateCustomerDetails = async ({
@@ -35,7 +35,7 @@ export const updateCustomerDetails = async ({
org: Organization;
logger: any;
}) => {
let updates: any = {};
const updates: any = {};
if (!customer.name && customerData?.name) {
updates.name = customerData.name;
}
@@ -84,7 +84,7 @@ export const getCusInvoices = async ({
limit?: number;
withItems?: boolean;
features?: Feature[];
}): Promise<InvoiceResponse[]> => {
}): Promise<APIInvoice[]> => {
const finalInvoices = notNullish(invoices)
? invoices
: await InvoiceService.list({
@@ -116,7 +116,7 @@ export const getCusEntsInFeatures = async ({
logger: any;
reverseOrder?: boolean;
}) => {
let cusProducts = customer.customer_products;
const cusProducts = customer.customer_products;
// This is important, attaching customer_product to cus ent is used elsewhere, don't delete.
let cusEnts = cusProducts.flatMap((cusProduct) => {
@@ -126,7 +126,7 @@ export const getCusEntsInFeatures = async ({
}));
});
let cusPrices = cusProducts.flatMap((cusProduct) => {
const cusPrices = cusProducts.flatMap((cusProduct) => {
return cusProduct.customer_prices || [];
});
@@ -137,7 +137,7 @@ export const getCusEntsInFeatures = async ({
}
if (customer.entity) {
let entity = customer.entity;
const entity = customer.entity;
cusEnts = cusEnts.filter(
(cusEnt) =>
nullish(cusEnt.customer_product.internal_entity_id) ||
@@ -152,8 +152,8 @@ export const getCusEntsInFeatures = async ({
export const parseCusExpand = (expand?: string): CusExpand[] => {
if (expand) {
let options = expand.split(",");
let result: CusExpand[] = [];
const options = expand.split(",");
const result: CusExpand[] = [];
for (const option of options) {
if (!Object.values(CusExpand).includes(option as CusExpand)) {
throw new RecaseError({
@@ -171,7 +171,7 @@ export const parseCusExpand = (expand?: string): CusExpand[] => {
};
export const newCusToFullCus = ({ newCus }: { newCus: Customer }) => {
let fullCus: FullCustomer = {
const fullCus: FullCustomer = {
...newCus,
customer_products: [],
entities: [],

View File

@@ -1,10 +1,10 @@
import {
APICustomerSchema,
APIVersion,
type AppEnv,
CusEntResponseSchema,
CusExpand,
CusProductStatus,
CusResponseSchema,
CustomerResponseSchema,
cusProductsToCusEnts,
cusProductsToCusPrices,
@@ -142,7 +142,7 @@ export const getCustomerDetails = async ({
});
const cusResponse = {
...CusResponseSchema.parse({
...APICustomerSchema.parse({
...customer,
stripe_id: customer.processor?.id,
features: entList,

View File

@@ -1,11 +1,8 @@
import { handleRequestError } from "@/utils/errorUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { CusProductStatus, ErrCode } from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
import { getCustomerDetails } from "../cusUtils/getCustomerDetails.js";
import { getOrCreateCustomer } from "../cusUtils/getOrCreateCustomer.js";
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
import { parseCusExpand } from "../cusUtils/cusUtils.js";
import { getCustomerDetails } from "../cusUtils/getCustomerDetails.js";
import { getOrCreateCustomer } from "../cusUtils/getOrCreateCustomer.js";
export const handlePostCustomerRequest = async (req: any, res: any) => {
const logger = req.logtail;
@@ -37,7 +34,7 @@ export const handlePostCustomerRequest = async (req: any, res: any) => {
});
}
let customer = await getOrCreateCustomer({
const customer = await getOrCreateCustomer({
req,
customerId: data.id,
customerData: data,
@@ -53,7 +50,7 @@ export const handlePostCustomerRequest = async (req: any, res: any) => {
withCache: true,
});
let cusDetails = await getCustomerDetails({
const cusDetails = await getCustomerDetails({
db,
customer,
org,

View File

@@ -1,15 +1,17 @@
import RecaseError from "@/utils/errorUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { CusService } from "@/internal/customers/CusService.js";
import { notNullish } from "@/utils/genUtils.js";
import { routeHandler } from "@/utils/routerUtils.js";
import { CreateCustomerSchema, ErrCode, ProcessorType } from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
import { getCustomerDetails } from "../cusUtils/getCustomerDetails.js";
import { parseCusExpand } from "../cusUtils/cusUtils.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { CusService } from "@/internal/customers/CusService.js";
import { FeatureService } from "@/internal/features/FeatureService.js";
import { ExtendedResponse } from "@/utils/models/Request.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import RecaseError from "@/utils/errorUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import type {
ExtendedRequest,
ExtendedResponse,
} from "@/utils/models/Request.js";
import { routeHandler } from "@/utils/routerUtils.js";
import { parseCusExpand } from "../cusUtils/cusUtils.js";
import { getCustomerDetails } from "../cusUtils/getCustomerDetails.js";
export const handleUpdateCustomer = async (req: any, res: any) =>
routeHandler({
@@ -38,7 +40,7 @@ export const handleUpdateCustomer = async (req: any, res: any) =>
});
}
let newCusData: any = CreateCustomerSchema.parse(req.body);
const newCusData: any = CreateCustomerSchema.parse(req.body);
if (req.body.id === null) {
throw new RecaseError({
@@ -70,7 +72,7 @@ export const handleUpdateCustomer = async (req: any, res: any) =>
// Try to update stripe ID
let stripeId = originalCustomer.processor?.id;
let newStripeId = newCusData.stripe_id;
const newStripeId = newCusData.stripe_id;
if (notNullish(newStripeId) && stripeId !== newStripeId) {
const stripeCli = createStripeCli({ org, env: req.env });
@@ -83,16 +85,16 @@ export const handleUpdateCustomer = async (req: any, res: any) =>
}
// 2. Check if customer email is being changed
let oldMetadata = originalCustomer.metadata || {};
let newMetadata = newCusData.metadata || {};
for (let key in newMetadata) {
const oldMetadata = originalCustomer.metadata || {};
const newMetadata = newCusData.metadata || {};
for (const key in newMetadata) {
if (newMetadata[key] === null) {
delete newMetadata[key];
delete oldMetadata[key];
}
}
let stripeUpdate = {
const stripeUpdate = {
email:
originalCustomer.email !== newCusData.email
? newCusData.email
@@ -123,7 +125,7 @@ export const handleUpdateCustomer = async (req: any, res: any) =>
},
});
let finalCustomer = await CusService.getFull({
const finalCustomer = await CusService.getFull({
db,
idOrInternalId: originalCustomer.internal_id,
orgId: req.orgId,
@@ -132,7 +134,7 @@ export const handleUpdateCustomer = async (req: any, res: any) =>
});
// res.status(200).json({ customer: updatedCustomer });
let customerDetails = await getCustomerDetails({
const customerDetails = await getCustomerDetails({
db,
customer: finalCustomer,
org,

View File

@@ -1,10 +1,10 @@
import {
type APIInvoice,
APIInvoiceItemSchema,
type Customer,
type Feature,
type Invoice,
type InvoiceItem,
InvoiceItemResponseSchema,
type InvoiceResponse,
type InvoiceStatus,
invoices,
type Organization,
@@ -41,7 +41,7 @@ export const processInvoice = ({
(f) => f.internal_id === i.internal_feature_id,
);
return InvoiceItemResponseSchema.parse({
return APIInvoiceItemSchema.parse({
description: i.description,
period_start: i.period_start,
period_end: i.period_end,
@@ -50,7 +50,7 @@ export const processInvoice = ({
});
})
: undefined,
} as InvoiceResponse;
} as APIInvoice;
};
export class InvoiceService {

View File

@@ -1,6 +1,6 @@
import {
AppEnv,
CreateProductSchema,
CreateProductV2ParamsSchema,
EntInsertSchema,
type Entitlement,
type Feature,
@@ -32,7 +32,7 @@ export const parseChatProducts = async ({
for (const product of chatProducts) {
const backendProduct: Product = constructProduct({
productData: CreateProductSchema.parse({
productData: CreateProductV2ParamsSchema.parse({
...product,
}),
orgId,

View File

@@ -1,255 +0,0 @@
import {
APIProductSchema,
CreateProductSchema,
type Entitlement,
ErrCode,
type FreeTrial,
type FullProduct,
type Price,
type Product,
ProductAlreadyExistsError,
type ProductItem,
} from "@autumn/shared";
import {
handleNewFreeTrial,
validateAndInitFreeTrial,
} from "@/internal/products/free-trials/freeTrialUtils.js";
import { ProductService } from "@/internal/products/ProductService.js";
import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems.js";
import { validateProductItems } from "@/internal/products/product-items/validateProductItems.js";
import {
constructProduct,
getGroupToDefaults,
initProductInStripe,
} from "@/internal/products/productUtils.js";
import { JobName } from "@/queue/JobName.js";
import { addTaskToQueue } from "@/queue/queueUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import {
keyToTitle,
notNullish,
nullish,
validateId,
} from "@/utils/genUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import { routeHandler } from "@/utils/routerUtils.js";
import { validateOneOffTrial } from "../free-trials/freeTrialUtils.js";
import { isDefaultTrial } from "../productUtils/classifyProduct.js";
const validateCreateProduct = async ({ req }: { req: ExtendedRequest }) => {
const { free_trial, items } = req.body;
const { orgId, env, db, features } = req;
const productData = CreateProductSchema.parse(req.body);
validateId("Product", productData.id);
if (nullish(req.body.name)) {
productData.name = keyToTitle(productData.id);
}
const existing = await ProductService.get({
db,
orgId,
env,
id: productData.id,
});
// 1. If existing product, throw error
if (existing) {
throw new ProductAlreadyExistsError({
productId: productData.id,
});
}
// 2. Validate items if exist
if (items && !Array.isArray(items)) {
throw new RecaseError({
message: "Items must be an array",
code: ErrCode.InvalidRequest,
});
} else if (items) {
validateProductItems({
newItems: items,
features,
orgId: req.orgId,
env: req.env,
});
}
// 3. Validate free trial if exist
let freeTrial: FreeTrial | null = null;
if (notNullish(free_trial)) {
// console.log("Free trial before:", free_trial);
freeTrial = validateAndInitFreeTrial({
freeTrial: free_trial,
internalProductId: productData.id,
isCustom: false,
});
// console.log("Free trial after:", freeTrial);
}
return {
features,
freeTrial,
productData,
};
};
export const disableCurrentDefault = async ({
req,
newProduct,
items,
freeTrial,
}: {
req: ExtendedRequest;
newProduct: Product;
items: ProductItem[];
freeTrial: FreeTrial | null;
}) => {
const { db, org, env, logger } = req;
let defaultProds = await ProductService.listDefault({
db,
orgId: org.id,
env,
});
defaultProds = defaultProds.filter((prod) => prod.id !== newProduct.id);
if (defaultProds.length === 0) return;
const defaults = getGroupToDefaults({
defaultProds,
})?.[newProduct.group];
const willBeDefaultTrial = isDefaultTrial({
product: {
...newProduct,
free_trial: freeTrial,
items: items || [],
},
});
if (willBeDefaultTrial) {
// Disable current default trial
const curDefault = defaults?.defaultTrial;
if (curDefault) {
logger.info(
`Disabling trial on cur default trial product: ${curDefault.id}`,
);
await ProductService.updateByInternalId({
db,
internalId: curDefault.internal_id,
update: { is_default: false },
});
}
} else if (newProduct.is_default) {
const curDefault = defaults?.free;
if (curDefault) {
logger.info(`Disabling trial on cur default product: ${curDefault.id}`);
await ProductService.updateByInternalId({
db,
internalId: curDefault.internal_id,
update: { is_default: false },
});
}
}
};
export const handleCreateProduct = async (req: Request, res: any) =>
routeHandler({
req,
res,
action: "POST /products",
handler: async (req, res) => {
const { items } = req.body;
const { logtail: logger, org, features, env, db } = req;
const { freeTrial, productData } = await validateCreateProduct({
req,
});
const newProduct = constructProduct({
productData,
orgId: org.id,
env,
});
await disableCurrentDefault({
req,
newProduct,
items,
freeTrial: freeTrial || null,
});
const product = await ProductService.insert({ db, product: newProduct });
let prices: Price[] = [];
let entitlements: Entitlement[] = [];
if (notNullish(items)) {
const res = await handleNewProductItems({
db,
product,
features,
curPrices: [],
curEnts: [],
newItems: items,
logger,
isCustom: false,
newVersion: false,
});
prices = res.prices;
entitlements = res.entitlements;
}
await validateOneOffTrial({
prices,
freeTrial: freeTrial || null,
});
await initProductInStripe({
db,
product: {
...product,
prices,
entitlements,
} as FullProduct,
org,
env,
logger,
});
if (notNullish(freeTrial)) {
await handleNewFreeTrial({
db,
newFreeTrial: freeTrial,
curFreeTrial: null,
internalProductId: product.internal_id,
isCustom: false,
});
}
await addTaskToQueue({
jobName: JobName.DetectBaseVariant,
payload: {
curProduct: {
...product,
prices,
entitlements: [],
},
},
});
res.status(200).json(
APIProductSchema.parse({
...product,
autumn_id: product.internal_id,
items: items || [],
free_trial: freeTrial,
}),
);
},
});

View File

@@ -1,214 +1,213 @@
import {
ErrCode,
ProductNotFoundError,
UpdateProductSchema,
} from "@autumn/shared";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import { FeatureService } from "@/internal/features/FeatureService.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { handleNewFreeTrial } from "@/internal/products/free-trials/freeTrialUtils.js";
import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems.js";
import { RewardProgramService } from "@/internal/rewards/RewardProgramService.js";
import { JobName } from "@/queue/JobName.js";
import { addTaskToQueue } from "@/queue/queueUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { notNullish } from "@/utils/genUtils.js";
import { routeHandler } from "@/utils/routerUtils.js";
import { validateOneOffTrial } from "../../free-trials/freeTrialUtils.js";
import { ProductService } from "../../ProductService.js";
import { productsAreSame } from "../../productUtils/compareProductUtils.js";
import { initProductInStripe } from "../../productUtils.js";
import { mapToProductItems } from "../../productV2Utils.js";
import {
disableCurrentDefault,
handleCreateProduct,
} from "../handleCreateProduct_old.js";
import { handleVersionProductV2 } from "../handleVersionProduct.js";
import { handleUpdateProductDetails } from "./updateProductDetails.js";
// import {
// ErrCode,
// ProductNotFoundError,
// UpdateProductSchema,
// } from "@autumn/shared";
// import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
// import { FeatureService } from "@/internal/features/FeatureService.js";
// import { OrgService } from "@/internal/orgs/OrgService.js";
// import { handleNewFreeTrial } from "@/internal/products/free-trials/freeTrialUtils.js";
// import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems.js";
// import { RewardProgramService } from "@/internal/rewards/RewardProgramService.js";
// import { JobName } from "@/queue/JobName.js";
// import { addTaskToQueue } from "@/queue/queueUtils.js";
// import RecaseError from "@/utils/errorUtils.js";
// import { notNullish } from "@/utils/genUtils.js";
// import { routeHandler } from "@/utils/routerUtils.js";
// import { validateOneOffTrial } from "../../free-trials/freeTrialUtils.js";
// import { ProductService } from "../../ProductService.js";
// import { productsAreSame } from "../../productUtils/compareProductUtils.js";
// import { initProductInStripe } from "../../productUtils.js";
// import { mapToProductItems } from "../../productV2Utils.js";
// import {
// disableCurrentDefault,
// handleCreateProduct,
// } from "../handleCreateProduct_old.js";
// import { handleUpdateProductDetails } from "./updateProductDetails.js";
export const handleUpdateProductV2 = async (req: any, res: any) =>
routeHandler({
req,
res,
action: "Update product",
handler: async () => {
const { productId } = req.params;
const { version, upsert, disable_version } = req.query;
const { orgId, env, logger, db } = req;
// export const handleUpdateProductV2 = async (req: any, res: any) =>
// routeHandler({
// req,
// res,
// action: "Update product",
// handler: async () => {
// const { productId } = req.params;
// const { version, upsert, disable_version } = req.query;
// const { orgId, env, logger, db } = req;
const [features, org, fullProduct, rewardPrograms, _defaultProds] =
await Promise.all([
FeatureService.getFromReq(req),
OrgService.getFromReq(req),
ProductService.getFull({
db,
idOrInternalId: productId,
orgId,
env,
version: version ? parseInt(version) : undefined,
allowNotFound: upsert === "true",
}),
RewardProgramService.getByProductId({
db,
productIds: [productId],
orgId,
env,
}),
ProductService.listDefault({
db,
orgId,
env,
}),
]);
// const [features, org, fullProduct, rewardPrograms, _defaultProds] =
// await Promise.all([
// FeatureService.getFromReq(req),
// OrgService.getFromReq(req),
// ProductService.getFull({
// db,
// idOrInternalId: productId,
// orgId,
// env,
// version: version ? parseInt(version) : undefined,
// allowNotFound: upsert === "true",
// }),
// RewardProgramService.getByProductId({
// db,
// productIds: [productId],
// orgId,
// env,
// }),
// ProductService.listDefault({
// db,
// orgId,
// env,
// }),
// ]);
if (!fullProduct) {
if (upsert === "true") {
await handleCreateProduct(req, res);
return;
}
// if (!fullProduct) {
// if (upsert === "true") {
// await handleCreateProduct(req, res);
// return;
// }
throw new ProductNotFoundError({ productId: productId });
}
// throw new ProductNotFoundError({ productId: productId });
// }
const cusProductsCurVersion =
await CusProductService.getByInternalProductId({
db,
internalProductId: fullProduct.internal_id,
});
// const cusProductsCurVersion =
// await CusProductService.getByInternalProductId({
// db,
// internalProductId: fullProduct.internal_id,
// });
const cusProductExists = cusProductsCurVersion.length > 0;
// const cusProductExists = cusProductsCurVersion.length > 0;
await disableCurrentDefault({
req,
newProduct: {
...fullProduct,
...req.body,
},
items:
req.body.items ||
mapToProductItems({
prices: fullProduct.prices,
entitlements: fullProduct.entitlements,
features,
}),
freeTrial: req.body.free_trial || fullProduct.free_trial || null,
});
// await disableCurrentDefault({
// req,
// newProduct: {
// ...fullProduct,
// ...req.body,
// },
// items:
// req.body.items ||
// mapToProductItems({
// prices: fullProduct.prices,
// entitlements: fullProduct.entitlements,
// features,
// }),
// freeTrial: req.body.free_trial || fullProduct.free_trial || null,
// });
await handleUpdateProductDetails({
db,
curProduct: fullProduct,
newProduct: UpdateProductSchema.parse(req.body),
newFreeTrial: req.body.free_trial,
items: req.body.items,
org,
rewardPrograms,
logger,
});
// await handleUpdateProductDetails({
// db,
// curProduct: fullProduct,
// newProduct: UpdateProductSchema.parse(req.body),
// newFreeTrial: req.body.free_trial,
// items: req.body.items,
// org,
// rewardPrograms,
// logger,
// });
const itemsExist = notNullish(req.body.items);
if (cusProductExists && itemsExist) {
if (disable_version === "true") {
throw new RecaseError({
message: "Cannot auto save product as there are existing customers",
code: ErrCode.InvalidRequest,
statusCode: 400,
});
}
// const itemsExist = notNullish(req.body.items);
// if (cusProductExists && itemsExist) {
// if (disable_version === "true") {
// throw new RecaseError({
// message: "Cannot auto save product as there are existing customers",
// code: ErrCode.InvalidRequest,
// statusCode: 400,
// });
// }
const { itemsSame, freeTrialsSame } = productsAreSame({
newProductV2: req.body,
curProductV1: fullProduct,
features,
});
const productSame = itemsSame && freeTrialsSame;
// const { itemsSame, freeTrialsSame } = productsAreSame({
// newProductV2: req.body,
// curProductV1: fullProduct,
// features,
// });
// const productSame = itemsSame && freeTrialsSame;
if (!productSame) {
await handleVersionProductV2({
req,
res,
latestProduct: fullProduct,
org,
env,
items: req.body.items,
freeTrial: req.body.free_trial,
});
return;
}
res.status(200).send(fullProduct);
return;
}
// if (!productSame) {
// // await handleVersionProductV2({
// // req,
// // res,
// // latestProduct: fullProduct,
// // org,
// // env,
// // items: req.body.items,
// // freeTrial: req.body.free_trial,
// // });
// return;
// }
// res.status(200).send(fullProduct);
// return;
// }
const { items, free_trial } = req.body;
// const { items, free_trial } = req.body;
if (free_trial !== undefined) {
await validateOneOffTrial({
prices: fullProduct.prices,
freeTrial: free_trial,
});
}
// if (free_trial !== undefined) {
// await validateOneOffTrial({
// prices: fullProduct.prices,
// freeTrial: free_trial,
// });
// }
await handleNewProductItems({
db,
curPrices: fullProduct.prices,
curEnts: fullProduct.entitlements,
newItems: items,
features,
product: fullProduct,
logger,
isCustom: false,
});
// await handleNewProductItems({
// db,
// curPrices: fullProduct.prices,
// curEnts: fullProduct.entitlements,
// newItems: items,
// features,
// product: fullProduct,
// logger,
// isCustom: false,
// });
// New full product
const newFullProduct = await ProductService.getFull({
db,
idOrInternalId: fullProduct.id,
orgId,
env,
});
// // New full product
// const newFullProduct = await ProductService.getFull({
// db,
// idOrInternalId: fullProduct.id,
// orgId,
// env,
// });
if (free_trial !== undefined) {
await validateOneOffTrial({
prices: newFullProduct.prices,
freeTrial: free_trial,
});
// if (free_trial !== undefined) {
// await validateOneOffTrial({
// prices: newFullProduct.prices,
// freeTrial: free_trial,
// });
await handleNewFreeTrial({
db,
curFreeTrial: fullProduct.free_trial,
newFreeTrial: free_trial,
internalProductId: fullProduct.internal_id,
isCustom: false,
product: fullProduct,
});
}
// await handleNewFreeTrial({
// db,
// curFreeTrial: fullProduct.free_trial,
// newFreeTrial: free_trial,
// internalProductId: fullProduct.internal_id,
// isCustom: false,
// product: fullProduct,
// });
// }
// New full product
await initProductInStripe({
db,
product: newFullProduct,
org,
env,
logger,
});
// // New full product
// await initProductInStripe({
// db,
// product: newFullProduct,
// org,
// env,
// logger,
// });
logger.info("Adding task to queue to detect base variant");
await addTaskToQueue({
jobName: JobName.DetectBaseVariant,
payload: {
curProduct: newFullProduct,
},
});
// logger.info("Adding task to queue to detect base variant");
// await addTaskToQueue({
// jobName: JobName.DetectBaseVariant,
// payload: {
// curProduct: newFullProduct,
// },
// });
await addTaskToQueue({
jobName: JobName.RewardMigration,
payload: {
oldPrices: fullProduct.prices,
productId: fullProduct.id,
orgId: org.id,
env,
},
});
res.status(200).send({ message: "Product updated" });
return;
},
});
// await addTaskToQueue({
// jobName: JobName.RewardMigration,
// payload: {
// oldPrices: fullProduct.prices,
// productId: fullProduct.id,
// orgId: org.id,
// env,
// },
// });
// res.status(200).send({ message: "Product updated" });
// return;
// },
// });

View File

@@ -1,7 +1,8 @@
import type {
CreateProductParams,
CreateProductV2Params,
FullProduct,
Price,
ProductItem,
ProductV2,
} from "@autumn/shared";
import { pricesOnlyOneOff } from "../prices/priceUtils.js";
@@ -39,16 +40,18 @@ export const isMainProduct = ({
export const isFreeProductV2 = ({
product,
}: {
product: ProductV2 | CreateProductParams;
product: ProductV2 | CreateProductV2Params;
}) => {
return (product.items || []).every((item) => isFeatureItem(item));
return (product.items || []).every((item: ProductItem) =>
isFeatureItem(item),
);
};
export const isDefaultTrial = ({
product,
skipDefault = false,
}: {
product: ProductV2 | CreateProductParams;
product: ProductV2 | CreateProductV2Params;
skipDefault?: boolean;
}) => {
return (

View File

@@ -0,0 +1,143 @@
import { FeatureOptionsSchema } from "@models/cusProductModels/cusProductModels.js";
import { CreateFreeTrialSchema } from "@models/productModels/freeTrialModels/freeTrialModels.js";
import { ProductItemSchema } from "@models/productV2Models/productItemModels/productItemModels.js";
import { z } from "zod/v4";
import { notNullish } from "../../utils/utils.js";
export const ProductOptions = z.object({
product_id: z.string(),
quantity: z.number().nullish(),
entity_id: z.string().nullish(),
options: z.array(FeatureOptionsSchema).nullish(),
});
export const ExtAttachBodySchema = z.object({
// Customer / Entity Info
customer_id: z
.string()
.describe("ID of the customer to attach the product to"),
customer_data: z
.any()
.nullish()
.describe("Customer data if using attach to auto create customer"),
entity_id: z.string().nullish(),
entity_data: z.any().nullish(),
// Product Info
product_id: z.string().nullish(),
product_ids: z.array(z.string()).min(1).nullish(),
options: z.array(FeatureOptionsSchema).nullish(),
free_trial: z.boolean(),
// Others
success_url: z.string().optional(),
force_checkout: z.boolean().optional(),
checkout_session_params: z.any().optional(),
reward: z.string().or(z.array(z.string())).optional(),
invoice: z.boolean().optional(),
// Checkout params
skip_checkout: z.boolean().optional(),
setup_payment: z.boolean().optional(),
});
export const AttachBodySchema = ExtAttachBodySchema.extend({
products: z.array(ProductOptions).nullish(),
// Custom Product
is_custom: z.boolean().optional(),
items: z.array(ProductItemSchema).optional(),
// Override free_trial to support CreateFreeTrialSchema
free_trial: CreateFreeTrialSchema.or(z.boolean()).optional(),
// New Version
version: z.number().optional(),
// Others
invoice_only: z.boolean().optional(),
metadata: z.any().optional(),
billing_cycle_anchor: z.number().optional(),
enable_product_immediately: z.boolean().optional(),
finalize_invoice: z.boolean().optional(),
})
.refine(
(data) => {
if (!data.product_id && !data.product_ids && !data.products) {
return false;
}
return true;
},
{
message: "`product_id` is not provided",
},
)
.refine(
(data) => {
if (data.product_id && data.product_ids) {
return false;
}
return true;
},
{
message: "provide either one `product_id` or `product_ids`",
},
)
.refine(
(data) => {
if (
notNullish(data.product_ids) &&
new Set(data.product_ids).size !== data.product_ids!.length
) {
return false;
}
return true;
},
{
message: "Can't pass in duplicate product_ids",
},
)
.refine(
(data) => {
if (data.product_ids && data.is_custom) {
return false;
}
return true;
},
{
message: "Can't pass in product_ids if is_custom is true",
},
)
.refine(
(data) => {
if (data.items && !data.is_custom) {
return false;
}
return true;
},
{
message: "Can't pass in items if is_custom is false",
},
);
export const AttachResultSchema = z.object({
customer_id: z.string(),
product_ids: z.array(z.string()),
code: z.string(),
message: z.string(),
checkout_url: z.string().nullish(),
invoice: z.any().nullish(),
});
export type AttachBody = z.infer<typeof AttachBodySchema>;
export type ProductOptions = z.infer<typeof ProductOptions>;
export type AttachResult = z.infer<typeof AttachResultSchema>;

View File

@@ -0,0 +1,42 @@
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";
export const ExtCheckoutParamsSchema = ExtAttachBodySchema.extend({
setup_payment: z.boolean().optional(),
});
export const CheckoutParamsSchema = AttachBodySchema.extend({
skip_checkout: z.boolean().optional(),
setup_payment: z.boolean().optional(),
});
// RESULT
export const CheckoutLineSchema = z.object({
description: z.string(),
amount: z.number(),
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(),
options: z.array(FeatureOptionsSchema).nullish(),
total: z.number().nullish(),
currency: z.string().nullish(),
has_prorations: z.boolean().nullish(),
next_cycle: z
.object({
starts_at: z.number().nullish(),
total: z.number().nullish(),
})
.nullish(),
});
export type CheckoutLine = z.infer<typeof CheckoutLineSchema>;
export type CheckoutParams = z.infer<typeof CheckoutParamsSchema>;

View File

@@ -0,0 +1,43 @@
import {
AttachResultSchema,
CheckoutResponseSchema,
ExtAttachBodySchema,
ExtCheckoutParamsSchema,
} from "@api/models.js";
export const coreOps = {
"/core": {
"/attach": {
post: {
summary: "Attach Product",
tags: ["core"],
requestBody: {
content: {
"application/json": { schema: ExtAttachBodySchema },
},
},
responses: {
"200": {
description: "200 OK",
content: { "application/json": { schema: AttachResultSchema } },
},
},
},
},
"/checkout": {
post: {
summary: "Checkout",
tags: ["core"],
requestBody: {
content: { "application/json": { schema: ExtCheckoutParamsSchema } },
},
responses: {
"200": {
description: "200 OK",
content: { "application/json": { schema: CheckoutResponseSchema } },
},
},
},
},
},
};

View File

@@ -1,80 +0,0 @@
import { AppEnv } from "@models/genModels/genEnums.js";
import { z } from "zod/v4";
// import { AppEnv } from "../genModels/genEnums.js";
// import { RewardResponseSchema } from "../rewardModels/rewardModels/rewardResponseModels.js";
// import { CusProductResponseSchema } from "./cusResModels/cusProductResponse.js";
// import { CusReferralResponseSchema } from "./cusResModels/cusReferralsResponse.js";
// import { UpcomingInvoiceResponseSchema } from "./cusResModels/upcomingInvoiceResponse.js";
// import { EntityResponseSchema } from "./entityModels/entityResModels.js";
// import { InvoiceResponseSchema } from "./invoiceModels/invoiceResponseModels.js";
// export const TrialUsedResponseSchema = z.object({
// product_id: z.string().meta({
// description: "The ID of the product for which the trial was used",
// example: "prod_123",
// }),
// customer_id: z.string().meta({
// description: "The ID of the customer who used the trial",
// example: "cus_123",
// }),
// fingerprint: z.string().nullish().meta({
// description: "Device/browser fingerprint when the trial was used",
// example: "fp_1234567890abcdef",
// }),
// });
export const APICustomerSchema = z.object({
id: z.string().nullable().meta({
description: "Your internal ID for the customer",
example: "cus_123",
}),
name: z.string().nullable().meta({
description: "The customer's full name",
example: "John Doe",
}),
email: z.string().nullable().meta({
description: "The customer's email address",
example: "john.doe@example.com",
}),
fingerprint: z.string().nullable().meta({
description: "Unique device/browser fingerprint for the customer",
example: "fp_1234567890abcdef",
}),
stripe_id: z.string().nullable().meta({
description: "The customer's Stripe customer ID",
example: "cus_1234567890abcdef",
}),
created_at: z.number().meta({
description:
"The date and time the customer was created in milliseconds since epoch",
example: 1717000000,
}),
env: z.enum(AppEnv),
// products: z.array(CusProductResponseSchema),
// features: z.any(),
// invoices: z.array(InvoiceResponseSchema).optional(),
// trials_used: z.array(TrialUsedResponseSchema).optional(),
// rewards: RewardResponseSchema.nullish(),
// metadata: z.record(z.any(), z.any()).default({}),
// entities: z.array(EntityResponseSchema).optional(),
// referrals: z.array(CusReferralResponseSchema).optional(),
// payment_method: z.any().nullish(),
// upcoming_invoice: UpcomingInvoiceResponseSchema.nullish(),
});
// export const IntAPICustomer = CusResponseV2Schema.extend({
// autumn_id: z.string().nullish().meta({
// description: "Internal Autumn system ID for the customer",
// example: "autumn_cus_1234567890",
// }),
// });
export type APICustomer = z.infer<typeof APICustomerSchema>;
// export type CusProductResponse = z.infer<typeof CusProductResponseSchema>;

View File

@@ -0,0 +1,51 @@
import { APICusFeatureSchema } from "@api/customers/components/apiCusFeature.js";
import { APICusProductSchema } from "@api/customers/components/apiCusProduct.js";
import { APICusReferralSchema } from "@api/customers/components/apiCusReferral.js";
import { APICusRewardsSchema } from "@api/models.js";
import { APIInvoiceSchema } from "@api/others/apiInvoice.js";
import { EntityResponseSchema } from "@models/cusModels/entityModels/entityResModels.js";
import { AppEnv } from "@models/genModels/genEnums.js";
import { z } from "zod/v4";
import { APICusUpcomingInvoiceSchema } from "./components/apiCusUpcomingInvoice.js";
export const APITrialsUsedSchema = z.object({
product_id: z.string(),
customer_id: z.string(),
fingerprint: 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",
}),
created_at: z.number().meta({
description:
"The date and time the customer was created in milliseconds since epoch",
example: 1717000000,
}),
name: z.string().nullable(),
email: z.string().nullable(),
fingerprint: z.string().nullable(),
stripe_id: z.string().nullable().default(null),
env: z.enum(AppEnv),
products: z.array(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(),
});
export type APICustomer = z.infer<typeof APICustomerSchema>;

View File

@@ -1,15 +1,16 @@
import { EntInterval } from "@models/productModels/entModels/entEnums.js";
import { ProductItemFeatureType } from "@models/productV2Models/productItemModels/productItemModels.js";
import { z } from "zod/v4";
import { EntInterval } from "../../productModels/entModels/entEnums.js";
import { ProductItemFeatureType } from "../../productV2Models/productItemModels/productItemModels.js";
export const CusRolloverSchema = z.object({
balance: z.number(),
expires_at: z.number(),
});
// OLD CUS FEATURE RESPONSE
export const CusEntResponseSchema = z.object({
feature_id: z.string(),
interval: z.nativeEnum(EntInterval).nullish(),
interval: z.enum(EntInterval).nullish(),
interval_count: z.number().nullish(),
unlimited: z.boolean().nullish(),
balance: z.number().nullish(), //
@@ -21,8 +22,9 @@ export const CusEntResponseSchema = z.object({
rollovers: z.array(CusRolloverSchema).nullish(),
});
export const CoreCusFeatureResponseSchema = z.object({
interval: z.nativeEnum(EntInterval).or(z.literal("multiple")).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(),
@@ -34,7 +36,7 @@ export const CoreCusFeatureResponseSchema = z.object({
breakdown: z
.array(
z.object({
interval: z.nativeEnum(EntInterval),
interval: z.enum(EntInterval),
interval_count: z.number().nullish(),
balance: z.number().nullish(),
usage: z.number().nullish(),
@@ -56,15 +58,15 @@ export const CoreCusFeatureResponseSchema = z.object({
rollovers: z.array(CusRolloverSchema).nullish(),
});
export const CusEntResponseV2Schema = z
export const APICusFeatureSchema = z
.object({
id: z.string(),
type: z.nativeEnum(ProductItemFeatureType),
type: z.enum(ProductItemFeatureType),
name: z.string().nullish(),
})
.extend(CoreCusFeatureResponseSchema.shape);
.extend(CoreCusFeatureSchema.shape);
export const CheckResponseSchema = z
export const CheckResultSchema = z
.object({
allowed: z.boolean(),
customer_id: z.string(),
@@ -73,9 +75,9 @@ export const CheckResponseSchema = z
required_balance: z.number(),
code: z.string(),
})
.extend(CoreCusFeatureResponseSchema.shape);
.extend(CoreCusFeatureSchema.shape);
export type CusEntResponse = z.infer<typeof CusEntResponseSchema>;
export type CusEntResponseV2 = z.infer<typeof CusEntResponseV2Schema>;
export type CheckResponse = z.infer<typeof CheckResponseSchema>;
export type CusEntResponseV2 = z.infer<typeof APICusFeatureSchema>;
export type CheckResponse = z.infer<typeof CheckResultSchema>;
export type CusRollover = z.infer<typeof CusRolloverSchema>;

View File

@@ -1,15 +1,11 @@
import { APIProductItemSchema } from "@api/products/apiProductItem.js";
import { z } from "zod/v4";
import { CusProductStatus } from "../../cusProductModels/cusProductEnums.js";
export const CusProductResponseSchema = z.object({
export const APICusProductSchema = z.object({
id: z.string(),
name: z.string().nullable(),
group: z.string().nullable(),
status: z.nativeEnum(CusProductStatus),
// created_at: z.number(),
canceled: z.boolean().nullish(),
trialing: z.boolean().nullish(),
status: z.enum(["active", "expired", "scheduled"]),
canceled_at: z.number().nullish(),
started_at: z.number(),
@@ -18,18 +14,14 @@ export const CusProductResponseSchema = z.object({
version: z.number().nullish(),
stripe_subscription_ids: z.array(z.string()).nullish(),
current_period_start: z.number().nullish(),
current_period_end: z.number().nullish(),
entity_id: z.string().nullish(),
items: z.array(APIProductItemSchema).nullish(),
quantity: z.number().optional(),
prepaid_quantities: z
.array(
z.object({
quantity: z.number(),
feature_id: z.string(),
}),
)
.nullish(),
});
export type APICusProduct = z.infer<typeof APICusProductSchema>;

View File

@@ -1,6 +1,6 @@
import { z } from "zod/v4";
export const CusReferralResponseSchema = z.object({
export const APICusReferralSchema = z.object({
program_id: z.string(),
customer: z.object({
id: z.string(),
@@ -11,4 +11,4 @@ export const CusReferralResponseSchema = z.object({
created_at: z.number(),
});
export type CusReferralResponse = z.infer<typeof CusReferralResponseSchema>;
export type APICusReferralResponse = z.infer<typeof APICusReferralSchema>;

View File

@@ -0,0 +1,18 @@
import { APIDiscountSchema } from "@api/others/apiDiscount.js";
import { z } from "zod/v4";
export const APICusUpcomingInvoiceSchema = z.object({
lines: z.array(
z.object({
product_id: z.string().nullish(),
description: z.string(),
amount: z.number(),
}),
),
discounts: z.array(APIDiscountSchema),
subtotal: z.number(),
total: z.number(),
currency: z.string(),
});
export type APICusUpcomingInvoice = z.infer<typeof APICusUpcomingInvoiceSchema>;

View File

@@ -1,81 +0,0 @@
// import { z } from "zod/v4";
// import { EntInterval } from "../../productModels/entModels/entEnums.js";
// import { ProductItemFeatureType } from "../../productV2Models/productItemModels/productItemModels.js";
// export const CusRolloverSchema = z.object({
// balance: z.number(),
// expires_at: z.number(),
// });
// export const CusEntResponseSchema = z.object({
// feature_id: z.string(),
// interval: z.nativeEnum(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(),
// });
// export const CoreCusFeatureResponseSchema = z.object({
// interval: z.nativeEnum(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.nativeEnum(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(),
// });
// export const CusEntResponseV2Schema = z
// .object({
// id: z.string(),
// type: z.nativeEnum(ProductItemFeatureType),
// name: z.string().nullish(),
// })
// .extend(CoreCusFeatureResponseSchema.shape);
// export const CheckResponseSchema = z
// .object({
// allowed: z.boolean(),
// customer_id: z.string(),
// feature_id: z.string(),
// entity_id: z.string().nullish(),
// required_balance: z.number(),
// code: z.string(),
// })
// .extend(CoreCusFeatureResponseSchema.shape);
// export type CusEntResponse = z.infer<typeof CusEntResponseSchema>;
// export type CusEntResponseV2 = z.infer<typeof CusEntResponseV2Schema>;
// export type CheckResponse = z.infer<typeof CheckResponseSchema>;
// export type CusRollover = z.infer<typeof CusRolloverSchema>;

View File

@@ -1,20 +0,0 @@
// import { z } from "zod/v4";
// import { DiscountResponseSchema } from "../../rewardModels/rewardModels/rewardResponseModels.js";
// export const UpcomingInvoiceResponseSchema = z.object({
// lines: z.array(
// z.object({
// product_id: z.string().nullish(),
// description: z.string(),
// amount: z.number(),
// }),
// ),
// discounts: z.array(DiscountResponseSchema),
// subtotal: z.number(),
// total: z.number(),
// currency: z.string(),
// });
// export type UpcomingInvoiceResponse = z.infer<
// typeof UpcomingInvoiceResponseSchema
// >;

View File

@@ -1,10 +1,24 @@
export * from "./features/apiFeature.js";
// Core
export * from "./core/attachModels.js";
export * from "./core/checkoutModels.js";
// Customers
export * from "./customers/apiCustomer.js";
export * from "./customers/components/apiCusFeature.js";
export * from "./customers/components/apiCusProduct.js";
export * from "./customers/components/apiCusReferral.js";
// Features
export * from "./features/apiFeature.js";
// Others
export * from "./others/apiDiscount.js";
export * from "./others/apiInvoice.js";
// Product
export * from "./products/apiFreeTrial.js";
export * from "./products/apiProduct.js";
export * from "./products/apiProductItem.js";
export * from "./products/operations/createProductParams.js";
export * from "./products/productOpModels.js";
// export * from "./products/apiFreeTrial.js";
// export * from "./products/apiProduct.js";

View File

@@ -3,8 +3,8 @@ import { AppEnv } from "@models/genModels/genEnums.js";
import yaml from "yaml";
import { z } from "zod/v4";
import { createDocument } from "zod-openapi";
import { APIProductSchema } from "./products/apiProduct.js";
import { CreateProductParamsSchema } from "./products/operations/createProductParams.js";
import { coreOps } from "./core/coreOpenApi.js";
import { productOps } from "./products/productsOpenApi.js";
const API_VERSION = "1.2.0";
@@ -50,33 +50,8 @@ const document = createDocument({
},
paths: {
"/products": {
post: {
summary: "Create Product",
tags: ["products"],
requestBody: {
content: {
"application/json": { schema: CreateProductParamsSchema },
},
},
responses: {
"200": {
description: "200 OK",
content: {
"application/json": { schema: APIProductSchema },
},
},
},
// responses: withErrorResponses({
// "200": {
// description: "200 OK",
// content: {
// "application/json": { schema: APIProductSchema },
// },
// },
// }),
},
},
...productOps,
...coreOps,
},
});

View File

@@ -0,0 +1,29 @@
import {
CouponDurationType,
RewardType,
} from "@models/rewardModels/rewardModels/rewardEnums.js";
import { z } from "zod/v4";
export const APIDiscountSchema = z.object({
id: z.string(), // either from Autumn or Stripe
name: z.string(),
type: z.enum(RewardType),
discount_value: z.number(),
duration_type: z.enum(CouponDurationType),
duration_value: z.number().nullish(),
currency: z.string().nullish(),
start: z.number().nullish(),
end: z.number().nullish(),
subscription_id: z.string().nullish(),
total_discount_amount: z.number().nullish(),
});
export const APICusRewardsSchema = z.object({
discounts: z.array(APIDiscountSchema),
});
export type APIDiscount = z.infer<typeof APIDiscountSchema>;
export type APIDiscountList = z.infer<typeof APICusRewardsSchema>;

View File

@@ -1,6 +1,6 @@
import { z } from "zod/v4";
export const InvoiceItemResponseSchema = z.object({
export const APIInvoiceItemSchema = z.object({
description: z.string(),
period_start: z.number(),
period_end: z.number(),
@@ -9,7 +9,7 @@ export const InvoiceItemResponseSchema = z.object({
feature_name: z.string().optional(),
});
export const InvoiceResponseSchema = z.object({
export const APIInvoiceSchema = z.object({
product_ids: z.array(z.string()),
stripe_id: z.string(),
status: z.string(),
@@ -21,5 +21,5 @@ export const InvoiceResponseSchema = z.object({
// period_end: z.number().nullish(),
});
export const InvoiceResponseListSchema = z.array(InvoiceResponseSchema);
export type InvoiceResponse = z.infer<typeof InvoiceResponseSchema>;
export const APIInvoiceListSchema = z.array(APIInvoiceSchema);
export type APIInvoice = z.infer<typeof APIInvoiceSchema>;

View File

@@ -7,21 +7,26 @@ 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, {
message: "name must be a non-empty string",
}),
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(""),
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),
});
items: z.array(CreateProductItemParamsSchema).optional(),
free_trial: CreateFreeTrialSchema.nullish().default(null),
})
.meta({
id: "CreateProductParams",
description: "Create Product",
});
export const UpdateProductV2ParamsSchema = CreateProductV2ParamsSchema.extend({
id: z.string().nonempty().regex(idRegex).optional(),
@@ -39,6 +44,9 @@ export const UpdateProductV2ParamsSchema = CreateProductV2ParamsSchema.extend({
// items: z.array(CreateProductItemParamsSchema).optional(),
free_trial: CreateFreeTrialSchema.nullish(),
}).meta({
id: "UpdateProductParams",
description: "Update Product",
});
export const UpdateProductQuerySchema = z.object({

View File

@@ -1,5 +1,8 @@
import {
CreateProductV2ParamsSchema,
UpdateProductV2ParamsSchema,
} from "@api/models.js";
import { APIProductSchema } from "./apiProduct.js";
import { CreateProductParamsSchema } from "./operations/createProductParams.js";
export const productOps = {
"/products": {
@@ -8,7 +11,22 @@ export const productOps = {
tags: ["products"],
requestBody: {
content: {
"application/json": { schema: CreateProductParamsSchema },
"application/json": { schema: CreateProductV2ParamsSchema },
},
},
responses: {
"200": {
description: "200 OK",
content: { "application/json": { schema: APIProductSchema } },
},
},
},
patch: {
summary: "Update Product",
tags: ["products"],
requestBody: {
content: {
"application/json": { schema: UpdateProductV2ParamsSchema },
},
},
responses: {

View File

@@ -39,11 +39,9 @@ export * from "./models/cusModels/cusExpand.js";
// 8. Customer Models
export * from "./models/cusModels/cusModels.js";
export * from "./models/cusModels/cusResModels/cusFeatureResponse.js";
export * from "./models/cusModels/cusResModels/cusProductResponse.js";
export * from "./models/cusModels/cusResModels/cusReferralsResponse.js";
// Cus response
export * from "./models/cusModels/cusResponseModels.js";
export * from "./models/cusModels/cusTable.js";
export * from "./models/cusModels/entityModels/entityExpand.js";
export * from "./models/cusModels/entityModels/entityModels.js";
@@ -51,7 +49,6 @@ export * from "./models/cusModels/entityModels/entityResModels.js";
export * from "./models/cusModels/entityModels/entityTable.js";
export * from "./models/cusModels/fullCusModel.js";
export * from "./models/cusModels/invoiceModels/invoiceModels.js";
export * from "./models/cusModels/invoiceModels/invoiceResponseModels.js";
export * from "./models/cusModels/invoiceModels/invoiceTable.js";
export * from "./models/cusProductModels/cusEntModels/cusEntModels.js";
export * from "./models/cusProductModels/cusEntModels/cusEntTable.js";

View File

@@ -1,126 +1,126 @@
import { z } from "zod/v4";
import { notNullish } from "../../utils/utils.js";
import { FeatureOptionsSchema } from "../cusProductModels/cusProductModels.js";
import { CreateFreeTrialSchema } from "../productModels/freeTrialModels/freeTrialModels.js";
import { ProductItemSchema } from "../productV2Models/productItemModels/productItemModels.js";
// import { z } from "zod/v4";
// import { notNullish } from "../../utils/utils.js";
// import { FeatureOptionsSchema } from "../cusProductModels/cusProductModels.js";
// import { CreateFreeTrialSchema } from "../productModels/freeTrialModels/freeTrialModels.js";
// import { ProductItemSchema } from "../productV2Models/productItemModels/productItemModels.js";
export const ProductOptions = z.object({
product_id: z.string(),
quantity: z.number().nullish(),
entity_id: z.string().nullish(),
options: z.array(FeatureOptionsSchema).nullish(),
});
// export const ProductOptions = z.object({
// product_id: z.string(),
// quantity: z.number().nullish(),
// entity_id: z.string().nullish(),
// options: z.array(FeatureOptionsSchema).nullish(),
// });
export const AttachBodySchema = z
.object({
// Customer Info
customer_id: z
.string()
.describe("ID of the customer to attach the product to"),
// export const AttachBodySchema = z
// .object({
// // Customer Info
// customer_id: z
// .string()
// .describe("ID of the customer to attach the product to"),
customer_data: z
.any()
.nullish()
.describe("Customer data if using attach to auto create customer"),
// customer_data: z
// .any()
// .nullish()
// .describe("Customer data if using attach to auto create customer"),
// Entity Info
entity_id: z.string().nullish(),
entity_data: z.any().nullish(),
// // Entity Info
// entity_id: z.string().nullish(),
// entity_data: z.any().nullish(),
// Product Info
product_id: z.string().nullish(),
product_ids: z.array(z.string()).min(1).nullish(),
options: z.array(FeatureOptionsSchema).nullish(),
// // Product Info
// product_id: z.string().nullish(),
// product_ids: z.array(z.string()).min(1).nullish(),
// options: z.array(FeatureOptionsSchema).nullish(),
products: z.array(ProductOptions).nullish(),
// products: z.array(ProductOptions).nullish(),
// Custom Product
is_custom: z.boolean().optional(),
items: z.array(ProductItemSchema).optional(),
free_trial: CreateFreeTrialSchema.or(z.boolean()).optional(),
// // Custom Product
// is_custom: z.boolean().optional(),
// items: z.array(ProductItemSchema).optional(),
// free_trial: CreateFreeTrialSchema.or(z.boolean()).optional(),
// New Version
version: z.number().optional(),
// // New Version
// version: z.number().optional(),
// Others
success_url: z.string().optional(),
force_checkout: z.boolean().optional(),
invoice_only: z.boolean().optional(),
metadata: z.any().optional(),
billing_cycle_anchor: z.number().optional(),
checkout_session_params: z.any().optional(),
reward: z.string().or(z.array(z.string())).optional(),
invoice: z.boolean().optional(),
enable_product_immediately: z.boolean().optional(),
finalize_invoice: z.boolean().optional(),
// // Others
// success_url: z.string().optional(),
// force_checkout: z.boolean().optional(),
// invoice_only: z.boolean().optional(),
// metadata: z.any().optional(),
// billing_cycle_anchor: z.number().optional(),
// checkout_session_params: z.any().optional(),
// reward: z.string().or(z.array(z.string())).optional(),
// invoice: z.boolean().optional(),
// enable_product_immediately: z.boolean().optional(),
// finalize_invoice: z.boolean().optional(),
// Checkout params
skip_checkout: z.boolean().optional(),
setup_payment: z.boolean().optional(),
})
.refine(
(data) => {
if (!data.product_id && !data.product_ids && !data.products) {
return false;
}
// // Checkout params
// skip_checkout: z.boolean().optional(),
// setup_payment: z.boolean().optional(),
// })
// .refine(
// (data) => {
// if (!data.product_id && !data.product_ids && !data.products) {
// return false;
// }
return true;
},
{
message: "`product_id` is not provided",
},
)
.refine(
(data) => {
if (data.product_id && data.product_ids) {
return false;
}
// return true;
// },
// {
// message: "`product_id` is not provided",
// },
// )
// .refine(
// (data) => {
// if (data.product_id && data.product_ids) {
// return false;
// }
return true;
},
{
message: "provide either one `product_id` or `product_ids`",
},
)
.refine(
(data) => {
if (
notNullish(data.product_ids) &&
new Set(data.product_ids).size !== data.product_ids!.length
) {
return false;
}
// return true;
// },
// {
// message: "provide either one `product_id` or `product_ids`",
// },
// )
// .refine(
// (data) => {
// if (
// notNullish(data.product_ids) &&
// new Set(data.product_ids).size !== data.product_ids!.length
// ) {
// return false;
// }
return true;
},
{
message: "Can't pass in duplicate product_ids",
},
)
.refine(
(data) => {
if (data.product_ids && data.is_custom) {
return false;
}
// return true;
// },
// {
// message: "Can't pass in duplicate product_ids",
// },
// )
// .refine(
// (data) => {
// if (data.product_ids && data.is_custom) {
// return false;
// }
return true;
},
{
message: "Can't pass in product_ids if is_custom is true",
},
)
.refine(
(data) => {
if (data.items && !data.is_custom) {
return false;
}
// return true;
// },
// {
// message: "Can't pass in product_ids if is_custom is true",
// },
// )
// .refine(
// (data) => {
// if (data.items && !data.is_custom) {
// return false;
// }
return true;
},
{
message: "Can't pass in items if is_custom is false",
},
);
// return true;
// },
// {
// message: "Can't pass in items if is_custom is false",
// },
// );
export type AttachBody = z.infer<typeof AttachBodySchema>;
export type ProductOptions = z.infer<typeof ProductOptions>;
// export type AttachBody = z.infer<typeof AttachBodySchema>;
// export type ProductOptions = z.infer<typeof ProductOptions>;

View File

@@ -1,32 +1,32 @@
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 { z } from "zod/v4";
import { FeatureOptionsSchema } from "../cusProductModels/cusProductModels.js";
// import { z } from "zod/v4";
// import { FeatureOptionsSchema } from "../cusProductModels/cusProductModels.js";
export const CheckoutLineSchema = z.object({
description: z.string(),
amount: z.number(),
item: APIProductItemSchema.nullish(),
});
// export const CheckoutLineSchema = z.object({
// description: z.string(),
// amount: z.number(),
// 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(),
options: z.array(FeatureOptionsSchema).nullish(),
total: z.number().nullish(),
currency: z.string().nullish(),
has_prorations: z.boolean().nullish(),
// next_cycle_at: z.number().nullish(),
next_cycle: z
.object({
starts_at: z.number().nullish(),
total: z.number().nullish(),
})
.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(),
// options: z.array(FeatureOptionsSchema).nullish(),
// total: z.number().nullish(),
// currency: z.string().nullish(),
// has_prorations: z.boolean().nullish(),
// // next_cycle_at: z.number().nullish(),
// next_cycle: z
// .object({
// starts_at: z.number().nullish(),
// total: z.number().nullish(),
// })
// .nullish(),
// });
export type CheckoutLine = z.infer<typeof CheckoutLineSchema>;
// export type CheckoutLine = z.infer<typeof CheckoutLineSchema>;

View File

@@ -1,14 +0,0 @@
import { z } from "zod/v4";
export const CusReferralResponseSchema = z.object({
program_id: z.string(),
customer: z.object({
id: z.string(),
name: z.string().nullish(),
email: z.string().nullish(),
}),
reward_applied: z.boolean(),
created_at: z.number(),
});
export type CusReferralResponse = z.infer<typeof CusReferralResponseSchema>;

View File

@@ -1,20 +0,0 @@
import { z } from "zod/v4";
import { DiscountResponseSchema } from "../../rewardModels/rewardModels/rewardResponseModels.js";
export const UpcomingInvoiceResponseSchema = z.object({
lines: z.array(
z.object({
product_id: z.string().nullish(),
description: z.string(),
amount: z.number(),
}),
),
discounts: z.array(DiscountResponseSchema),
subtotal: z.number(),
total: z.number(),
currency: z.string(),
});
export type UpcomingInvoiceResponse = z.infer<
typeof UpcomingInvoiceResponseSchema
>;

View File

@@ -1,51 +0,0 @@
import { z } from "zod/v4";
import { AppEnv } from "../genModels/genEnums.js";
import { RewardResponseSchema } from "../rewardModels/rewardModels/rewardResponseModels.js";
import { CusProductResponseSchema } from "./cusResModels/cusProductResponse.js";
import { CusReferralResponseSchema } from "./cusResModels/cusReferralsResponse.js";
import { UpcomingInvoiceResponseSchema } from "./cusResModels/upcomingInvoiceResponse.js";
import { EntityResponseSchema } from "./entityModels/entityResModels.js";
import { InvoiceResponseSchema } from "./invoiceModels/invoiceResponseModels.js";
export const TrialUsedResponseSchema = z.object({
product_id: z.string(),
customer_id: z.string(),
fingerprint: z.string().nullish(),
});
export const CusResponseSchema = z.object({
// Internal fields
autumn_id: z.string().nullish(),
id: z.string().nullable().meta({
description: "Your internal ID for the customer",
example: "cus_123",
}),
created_at: z.number().meta({
description:
"The date and time the customer was created in milliseconds since epoch",
example: 1717000000,
}),
name: z.string().nullable(),
email: z.string().nullable(),
fingerprint: z.string().nullable(),
stripe_id: z.string().nullable().default(null),
env: z.nativeEnum(AppEnv),
products: z.array(CusProductResponseSchema),
features: z.any(),
invoices: z.array(InvoiceResponseSchema).optional(),
trials_used: z.array(TrialUsedResponseSchema).optional(),
rewards: RewardResponseSchema.nullish(),
metadata: z.record(z.any(), z.any()).default({}),
entities: z.array(EntityResponseSchema).optional(),
referrals: z.array(CusReferralResponseSchema).optional(),
payment_method: z.any().nullish(),
upcoming_invoice: UpcomingInvoiceResponseSchema.nullish(),
});
export type CusResponse = z.infer<typeof CusResponseSchema>;
export type CusProductResponse = z.infer<typeof CusProductResponseSchema>;

View File

@@ -1,8 +1,8 @@
import { APICusFeatureSchema } from "@api/customers/components/apiCusFeature.js";
import { APICusProductSchema } from "@api/customers/components/apiCusProduct.js";
import { APIInvoiceSchema } from "@api/others/apiInvoice.js";
import { z } from "zod/v4";
import { AppEnv } from "../../genModels/genEnums.js";
import { CusEntResponseV2Schema } from "../cusResModels/cusFeatureResponse.js";
import { CusProductResponseSchema } from "../cusResModels/cusProductResponse.js";
import { InvoiceResponseSchema } from "../invoiceModels/invoiceResponseModels.js";
export const EntityResponseSchema = z.object({
id: z.string().nullable(),
@@ -11,10 +11,10 @@ export const EntityResponseSchema = z.object({
feature_id: z.string().nullish(),
created_at: z.number(),
env: z.nativeEnum(AppEnv),
products: z.array(CusProductResponseSchema).optional(),
features: z.record(z.string(), CusEntResponseV2Schema).optional(),
invoices: z.array(InvoiceResponseSchema).optional(),
env: z.enum(AppEnv),
products: z.array(APICusProductSchema).optional(),
features: z.record(z.string(), APICusFeatureSchema).optional(),
invoices: z.array(APIInvoiceSchema).optional(),
});
export type EntityResponse = z.infer<typeof EntityResponseSchema>;