writing tests for migrations
This commit is contained in:
14
server/src/external/autumn/autumnCli.ts
vendored
14
server/src/external/autumn/autumnCli.ts
vendored
@@ -7,7 +7,7 @@ import {
|
||||
CusExpand,
|
||||
ErrCode,
|
||||
} from "@autumn/shared";
|
||||
import { CheckParams, TrackParams, UsageParams } from "autumn-js";
|
||||
import { CheckParams, CheckResult, TrackParams, UsageParams } from "autumn-js";
|
||||
import { AttachBody } from "@/internal/customers/attach/models/AttachBody.js";
|
||||
|
||||
export default class AutumnError extends Error {
|
||||
@@ -398,7 +398,7 @@ export class AutumnInt {
|
||||
return data;
|
||||
};
|
||||
|
||||
check = async (params: CheckParams) => {
|
||||
check = async (params: CheckParams): Promise<CheckResult> => {
|
||||
const data = await this.post(`/check`, params);
|
||||
return data;
|
||||
};
|
||||
@@ -408,6 +408,16 @@ export class AutumnInt {
|
||||
return data;
|
||||
};
|
||||
|
||||
migrate = async (params: {
|
||||
from_product_id: string;
|
||||
to_product_id: string;
|
||||
from_version: number;
|
||||
to_version: number;
|
||||
}) => {
|
||||
const data = await this.post(`/migrations`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
initStripe = async () => {
|
||||
await this.post(`/products/all/init_stripe`, {});
|
||||
};
|
||||
|
||||
@@ -1,29 +1,21 @@
|
||||
import {
|
||||
AppEnv,
|
||||
AttachScenario,
|
||||
CollectionMethod,
|
||||
CusProductStatus,
|
||||
ErrCode,
|
||||
FullCusProduct,
|
||||
Organization,
|
||||
} from "@autumn/shared";
|
||||
|
||||
import { createStripeCli } from "../utils.js";
|
||||
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
|
||||
import { formatUnixToDateTime, notNullish, nullish } from "@/utils/genUtils.js";
|
||||
import { ProductService } from "@/internal/products/ProductService.js";
|
||||
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
|
||||
import { cancelFutureProductSchedule } from "@/internal/customers/change-product/scheduleUtils.js";
|
||||
import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js";
|
||||
|
||||
import {
|
||||
getWebhookLock,
|
||||
releaseWebhookLock,
|
||||
} from "@/external/redis/stripeWebhookLocks.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { SubService } from "@/internal/subscriptions/SubService.js";
|
||||
import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { CusService } from "@/internal/customers/CusService.js";
|
||||
import { FeatureService } from "@/internal/features/FeatureService.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { handleSubCanceled } from "./handleSubUpdated/handleSubCanceled.js";
|
||||
@@ -54,11 +46,6 @@ export const handleSubscriptionUpdated = async ({
|
||||
env,
|
||||
});
|
||||
let fullSub = await stripeCli.subscriptions.retrieve(subscription.id);
|
||||
let features = await FeatureService.list({
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
});
|
||||
|
||||
let subStatusMap: {
|
||||
[key: string]: CusProductStatus;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { pricingMiddleware } from "@/middleware/pricingMiddleware.js";
|
||||
import { usageRouter } from "./events/usageRouter.js";
|
||||
import { invoiceRouter } from "./invoiceRouter.js";
|
||||
import { entityRouter } from "./entities/entityRouter.js";
|
||||
import { migrationRouter } from "./migrations/migrationRouter.js";
|
||||
import { migrationRouter } from "../migrations/migrationRouter.js";
|
||||
|
||||
import { redemptionRouter, referralRouter } from "./rewards/referralRouter.js";
|
||||
import { rewardProgramRouter } from "./rewards/rewardProgramRouter.js";
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { isFreeProduct } from "@/internal/products/productUtils.js";
|
||||
import {
|
||||
AttachBranch,
|
||||
AttachPreview,
|
||||
AttachScenario,
|
||||
FullProduct,
|
||||
} from "@autumn/shared";
|
||||
|
||||
// New = "new", done
|
||||
// Downgrade = "downgrade",
|
||||
// Upgrade = "upgrade",
|
||||
// Cancel = "cancel",
|
||||
// Renew = "renew",
|
||||
|
||||
// Scheduled = "scheduled",
|
||||
// Active = "active",
|
||||
// Expired = "expired",
|
||||
|
||||
export const getAttachScenario = async ({
|
||||
preview,
|
||||
product,
|
||||
}: {
|
||||
preview: AttachPreview;
|
||||
product: FullProduct;
|
||||
}) => {
|
||||
let branch = preview.branch;
|
||||
let attachFunc = preview.func;
|
||||
|
||||
if (
|
||||
branch == AttachBranch.New ||
|
||||
branch == AttachBranch.OneOff ||
|
||||
branch == AttachBranch.AddOn
|
||||
) {
|
||||
return AttachScenario.New;
|
||||
}
|
||||
|
||||
if (
|
||||
branch == AttachBranch.MainIsFree ||
|
||||
branch == AttachBranch.MainIsTrial ||
|
||||
branch == AttachBranch.Upgrade
|
||||
) {
|
||||
return AttachScenario.Upgrade;
|
||||
}
|
||||
|
||||
if (branch == AttachBranch.Downgrade) {
|
||||
// return AttachScenario.Downgrade;
|
||||
if (isFreeProduct(product.prices)) {
|
||||
return AttachScenario.Cancel;
|
||||
} else {
|
||||
return AttachScenario.Downgrade;
|
||||
}
|
||||
}
|
||||
|
||||
if (branch == AttachBranch.Renew) {
|
||||
return AttachScenario.Renew;
|
||||
}
|
||||
|
||||
return AttachScenario.New;
|
||||
};
|
||||
@@ -0,0 +1,153 @@
|
||||
import { checkToAttachParams } from "@/internal/customers/attach/attachUtils/attachParams/checkToAttachParams.js";
|
||||
import { FullCusProduct, FullCustomer, FullProduct } from "@autumn/shared";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { AttachBody } from "@/internal/customers/attach/models/AttachBody.js";
|
||||
import { attachParamsToPreview } from "@/internal/customers/attach/handleAttachPreview/attachParamsToPreview.js";
|
||||
|
||||
import {
|
||||
AttachFunction,
|
||||
AttachPreview,
|
||||
CheckProductPreview,
|
||||
Feature,
|
||||
Organization,
|
||||
} from "@autumn/shared";
|
||||
|
||||
import { getAttachScenario } from "./attachToCheckPreview/getAttachScenario.js";
|
||||
import { getProductResponse } from "@/internal/products/productV2Utils.js";
|
||||
import { isOneOff } from "@/internal/products/productUtils.js";
|
||||
import { formatAmount } from "@/utils/formatUtils.js";
|
||||
import { Decimal } from "decimal.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
|
||||
export const attachToCheckPreview = async ({
|
||||
preview,
|
||||
product,
|
||||
org,
|
||||
features,
|
||||
}: {
|
||||
preview: AttachPreview;
|
||||
product: FullProduct;
|
||||
org: Organization;
|
||||
features: Feature[];
|
||||
}) => {
|
||||
// 1. If check
|
||||
let branch = preview.branch;
|
||||
let attachFunc = preview.func;
|
||||
|
||||
const noOptions = !preview.options || preview.options.length === 0;
|
||||
if (attachFunc == AttachFunction.CreateCheckout && noOptions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let scenario = await getAttachScenario({
|
||||
preview,
|
||||
product,
|
||||
});
|
||||
|
||||
let items = preview.due_today?.line_items?.map((item) => {
|
||||
return {
|
||||
price: notNullish(item.amount)
|
||||
? formatAmount({
|
||||
amount: item.amount!,
|
||||
org,
|
||||
minFractionDigits: 2,
|
||||
maxFractionDigits: 2,
|
||||
})
|
||||
: item.price,
|
||||
description: item.description,
|
||||
usage_model: item.usage_model,
|
||||
};
|
||||
});
|
||||
|
||||
let options = preview.options?.map((option: any) => {
|
||||
return {
|
||||
...option,
|
||||
price: new Decimal(option.price).toDecimalPlaces(2).toNumber(),
|
||||
};
|
||||
});
|
||||
|
||||
let due_today = preview.due_today
|
||||
? {
|
||||
price: preview.due_today.total,
|
||||
currency: org.default_currency || "usd",
|
||||
}
|
||||
: undefined;
|
||||
|
||||
let due_next_cycle = {
|
||||
price:
|
||||
preview.due_next_cycle?.line_items.reduce((acc, item) => {
|
||||
if (item.amount) {
|
||||
return acc + item.amount;
|
||||
}
|
||||
return acc;
|
||||
}, 0) || 0,
|
||||
currency: org.default_currency || "usd",
|
||||
};
|
||||
|
||||
let checkPreview: CheckProductPreview = {
|
||||
title: "Check",
|
||||
message: "Check",
|
||||
scenario,
|
||||
|
||||
// Meta
|
||||
product_id: product.id,
|
||||
product_name: product.name,
|
||||
recurring: !isOneOff(product.prices),
|
||||
error_on_attach: false,
|
||||
next_cycle_at: preview.due_next_cycle?.due_at,
|
||||
current_product_name: preview.current_product?.name,
|
||||
|
||||
// Otehrs
|
||||
|
||||
options,
|
||||
items,
|
||||
due_today,
|
||||
due_next_cycle,
|
||||
product: getProductResponse({ product, features }),
|
||||
};
|
||||
return checkPreview;
|
||||
};
|
||||
|
||||
export const getProductCheckPreview = async ({
|
||||
req,
|
||||
customer,
|
||||
product,
|
||||
logger,
|
||||
}: {
|
||||
req: ExtendedRequest;
|
||||
customer: FullCustomer;
|
||||
product: FullProduct;
|
||||
logger: any;
|
||||
}) => {
|
||||
const { org, features } = req;
|
||||
|
||||
// Build attach params
|
||||
const attachParams = await checkToAttachParams({
|
||||
req,
|
||||
customer,
|
||||
product,
|
||||
logger,
|
||||
});
|
||||
|
||||
const attachBody: AttachBody = {
|
||||
customer_id: customer.id!,
|
||||
product_id: product.id,
|
||||
entity_id: customer.entity?.id,
|
||||
};
|
||||
|
||||
const preview = await attachParamsToPreview({
|
||||
req,
|
||||
attachParams,
|
||||
attachBody,
|
||||
logger,
|
||||
});
|
||||
|
||||
const checkPreview = await attachToCheckPreview({
|
||||
preview,
|
||||
product,
|
||||
org,
|
||||
features,
|
||||
});
|
||||
|
||||
return checkPreview;
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import { getOrCreateCustomer } from "@/internal/customers/cusUtils/getOrCreateCu
|
||||
import { getOrgAndFeatures } from "@/internal/orgs/orgUtils.js";
|
||||
import { getProductResponse } from "@/internal/products/productV2Utils.js";
|
||||
import { getCusPaymentMethod } from "@/external/stripe/stripeCusUtils.js";
|
||||
import { getProductCheckPreview } from "./getProductCheckPreview.js";
|
||||
|
||||
export const handleProductCheck = async ({
|
||||
req,
|
||||
@@ -40,6 +41,7 @@ export const handleProductCheck = async ({
|
||||
|
||||
entityId: entity_id,
|
||||
entityData: entity_data,
|
||||
withEntities: true,
|
||||
}),
|
||||
ProductService.getFull({
|
||||
db,
|
||||
@@ -62,19 +64,28 @@ export const handleProductCheck = async ({
|
||||
);
|
||||
|
||||
let preview = with_preview
|
||||
? await getAttachPreview({
|
||||
db,
|
||||
? await getProductCheckPreview({
|
||||
req,
|
||||
customer,
|
||||
org,
|
||||
env,
|
||||
product: product!,
|
||||
cusProducts,
|
||||
features,
|
||||
product,
|
||||
logger,
|
||||
shouldFormat: with_preview == "formatted",
|
||||
})
|
||||
: undefined;
|
||||
|
||||
// let preview = with_preview
|
||||
// ? await getAttachPreview({
|
||||
// db,
|
||||
// customer,
|
||||
// org,
|
||||
// env,
|
||||
// product: product!,
|
||||
// cusProducts,
|
||||
// features,
|
||||
// logger,
|
||||
// shouldFormat: with_preview == "formatted",
|
||||
// })
|
||||
// : undefined;
|
||||
|
||||
if (preview) {
|
||||
preview = {
|
||||
...preview,
|
||||
|
||||
@@ -197,6 +197,7 @@ export const handleUsageEvent = async ({
|
||||
payload,
|
||||
logger: console,
|
||||
db: req.db,
|
||||
throwError: true,
|
||||
});
|
||||
} else {
|
||||
await addTaskToQueue({
|
||||
|
||||
@@ -12,7 +12,12 @@ import {
|
||||
Replaceable,
|
||||
InsertReplaceable,
|
||||
} from "@autumn/shared";
|
||||
import { generateId, notNullish, nullish } from "@/utils/genUtils.js";
|
||||
import {
|
||||
formatUnixToDate,
|
||||
generateId,
|
||||
notNullish,
|
||||
nullish,
|
||||
} from "@/utils/genUtils.js";
|
||||
|
||||
import { Customer } from "@autumn/shared";
|
||||
import { FullProduct } from "@autumn/shared";
|
||||
@@ -61,13 +66,13 @@ export const initCusPrice = ({
|
||||
export const initCusProduct = ({
|
||||
customer,
|
||||
product,
|
||||
subscriptionId,
|
||||
// subscriptionId,
|
||||
// subscriptionScheduleId,
|
||||
// lastInvoiceId,
|
||||
cusProdId,
|
||||
startsAt,
|
||||
subscriptionScheduleId,
|
||||
optionsList,
|
||||
freeTrial,
|
||||
lastInvoiceId,
|
||||
trialEndsAt,
|
||||
subscriptionStatus,
|
||||
canceledAt,
|
||||
@@ -82,13 +87,13 @@ export const initCusProduct = ({
|
||||
}: {
|
||||
customer: Customer;
|
||||
product: FullProduct;
|
||||
subscriptionId: string | undefined | null;
|
||||
// subscriptionId: string | undefined | null;
|
||||
// subscriptionScheduleId?: string | null;
|
||||
// lastInvoiceId?: string | null;
|
||||
cusProdId: string;
|
||||
startsAt?: number;
|
||||
subscriptionScheduleId?: string | null;
|
||||
optionsList: FeatureOptions[];
|
||||
freeTrial: FreeTrial | null;
|
||||
lastInvoiceId?: string | null;
|
||||
trialEndsAt?: number | null;
|
||||
subscriptionStatus?: CusProductStatus;
|
||||
canceledAt?: number | null;
|
||||
@@ -122,12 +127,12 @@ export const initCusProduct = ({
|
||||
? CusProductStatus.Scheduled
|
||||
: CusProductStatus.Active,
|
||||
|
||||
processor: {
|
||||
type: ProcessorType.Stripe,
|
||||
subscription_id: subscriptionId,
|
||||
subscription_schedule_id: subscriptionScheduleId,
|
||||
last_invoice_id: lastInvoiceId,
|
||||
},
|
||||
// processor: {
|
||||
// type: ProcessorType.Stripe,
|
||||
// subscription_id: subscriptionId,
|
||||
// subscription_schedule_id: subscriptionScheduleId,
|
||||
// last_invoice_id: lastInvoiceId,
|
||||
// },
|
||||
|
||||
starts_at: startsAt || Date.now(),
|
||||
trial_ends_at: trialEnds,
|
||||
@@ -267,11 +272,11 @@ export const createFullCusProduct = async ({
|
||||
db,
|
||||
attachParams,
|
||||
startsAt,
|
||||
subscriptionId,
|
||||
// subscriptionId,
|
||||
nextResetAt,
|
||||
disableFreeTrial = false,
|
||||
lastInvoiceId = null,
|
||||
trialEndsAt = null,
|
||||
trialEndsAt,
|
||||
subscriptionStatus,
|
||||
canceledAt = null,
|
||||
createdAt = null,
|
||||
@@ -289,12 +294,12 @@ export const createFullCusProduct = async ({
|
||||
db: DrizzleCli;
|
||||
attachParams: InsertCusProductParams;
|
||||
startsAt?: number;
|
||||
subscriptionId?: string;
|
||||
// subscriptionId?: string;
|
||||
nextResetAt?: number;
|
||||
billLaterOnly?: boolean;
|
||||
disableFreeTrial?: boolean;
|
||||
lastInvoiceId?: string | null;
|
||||
trialEndsAt?: number | null;
|
||||
trialEndsAt?: number;
|
||||
subscriptionStatus?: CusProductStatus;
|
||||
canceledAt?: number | null;
|
||||
createdAt?: number | null;
|
||||
@@ -311,22 +316,26 @@ export const createFullCusProduct = async ({
|
||||
}) => {
|
||||
disableFreeTrial = attachParams.disableFreeTrial || disableFreeTrial;
|
||||
|
||||
let { customer, product, prices, entitlements, optionsList, freeTrial, org } =
|
||||
let { customer, product, prices, entitlements, optionsList, org, freeTrial } =
|
||||
attachParams;
|
||||
|
||||
let attachReplaceables = attachParams.replaceables || [];
|
||||
|
||||
// Try to get current cus product or set to null...
|
||||
let curCusProduct;
|
||||
try {
|
||||
curCusProduct = await getExistingCusProduct({
|
||||
db,
|
||||
cusProducts: attachParams.cusProducts,
|
||||
product,
|
||||
internalCustomerId: customer.internal_id,
|
||||
internalEntityId: attachParams.internalEntityId,
|
||||
});
|
||||
} catch (error) {}
|
||||
let curCusProduct = await getExistingCusProduct({
|
||||
db,
|
||||
cusProducts: attachParams.cusProducts,
|
||||
product,
|
||||
internalCustomerId: customer.internal_id,
|
||||
internalEntityId: attachParams.internalEntityId,
|
||||
});
|
||||
|
||||
freeTrial = disableFreeTrial ? null : freeTrial;
|
||||
|
||||
if (carryOverTrial && curCusProduct?.free_trial) {
|
||||
freeTrial = curCusProduct.free_trial;
|
||||
trialEndsAt = curCusProduct.trial_ends_at || undefined;
|
||||
}
|
||||
|
||||
let attachReplaceables = attachParams.replaceables || [];
|
||||
|
||||
const existingCusProduct = searchCusProducts({
|
||||
internalProductId: product.internal_id,
|
||||
@@ -359,6 +368,7 @@ export const createFullCusProduct = async ({
|
||||
for (const entitlement of entitlements) {
|
||||
const options = getEntOptions(optionsList, entitlement);
|
||||
const relatedPrice = getEntRelatedPrice(entitlement, prices);
|
||||
const now = attachParams.now || Date.now();
|
||||
|
||||
const cusEnt: any = initCusEntitlement({
|
||||
entitlement,
|
||||
@@ -366,15 +376,17 @@ export const createFullCusProduct = async ({
|
||||
cusProductId: cusProdId,
|
||||
options: options || undefined,
|
||||
nextResetAt,
|
||||
freeTrial: disableFreeTrial ? null : freeTrial,
|
||||
freeTrial,
|
||||
relatedPrice,
|
||||
// existingCusEnt,
|
||||
// keepResetIntervals,
|
||||
trialEndsAt,
|
||||
anchorToUnix,
|
||||
entities: attachParams.entities || [],
|
||||
carryExistingUsages,
|
||||
curCusProduct: curCusProduct as FullCusProduct,
|
||||
replaceables: attachReplaceables,
|
||||
now,
|
||||
});
|
||||
|
||||
cusEnts.push(cusEnt);
|
||||
@@ -414,21 +426,25 @@ export const createFullCusProduct = async ({
|
||||
}
|
||||
|
||||
// 5. create customer product
|
||||
if (carryOverTrial && curCusProduct?.free_trial_id) {
|
||||
freeTrial = curCusProduct.free_trial || null;
|
||||
trialEndsAt = curCusProduct.trial_ends_at || null;
|
||||
}
|
||||
|
||||
// let freeTrial = disableFreeTrial ? null : freeTrial;
|
||||
// if (carryOverTrial && curCusProduct?.free_trial_id) {
|
||||
// logger.info(`Free trial ID: ${curCusProduct.free_trial_id}`);
|
||||
// logger.info(
|
||||
// `Trial ends at: ${formatUnixToDate(curCusProduct.trial_ends_at)}`,
|
||||
// );
|
||||
// freeTrial = curCusProduct.free_trial || null;
|
||||
// trialEndsAt = curCusProduct.trial_ends_at || null;
|
||||
// }
|
||||
|
||||
let entityId = customer.entity?.id;
|
||||
const cusProd = initCusProduct({
|
||||
cusProdId,
|
||||
customer,
|
||||
product,
|
||||
subscriptionId,
|
||||
startsAt,
|
||||
optionsList,
|
||||
freeTrial: disableFreeTrial ? null : freeTrial,
|
||||
lastInvoiceId,
|
||||
trialEndsAt,
|
||||
subscriptionStatus,
|
||||
canceledAt,
|
||||
|
||||
@@ -18,7 +18,7 @@ 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 { formatUnixToDate, generateId, notNullish } from "@/utils/genUtils.js";
|
||||
import { getBillingType } from "@/internal/products/prices/priceUtils.js";
|
||||
import { applyTrialToEntitlement } from "@/internal/products/entitlements/entitlementUtils.js";
|
||||
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
|
||||
@@ -26,6 +26,7 @@ import { getNextEntitlementReset } from "@/utils/timeUtils.js";
|
||||
import { subtractFromUnixTillAligned } from "@/internal/products/prices/billingIntervalUtils.js";
|
||||
import { UTCDate } from "@date-fns/utc";
|
||||
import { entitlementLinkedToEntity } from "@/internal/api/entities/entityUtils.js";
|
||||
import { initNextResetAt } from "../cusProducts/insertCusProduct/initCusEnt/initNextResetAt.js";
|
||||
|
||||
export const initCusEntEntities = ({
|
||||
entitlement,
|
||||
@@ -71,73 +72,6 @@ export const initCusEntEntities = ({
|
||||
return newEntities;
|
||||
};
|
||||
|
||||
const initCusEntNextResetAt = ({
|
||||
entitlement,
|
||||
nextResetAt,
|
||||
keepResetIntervals,
|
||||
existingCusEnt,
|
||||
freeTrial,
|
||||
anchorToUnix,
|
||||
}: {
|
||||
entitlement: EntitlementWithFeature;
|
||||
nextResetAt?: number;
|
||||
keepResetIntervals?: boolean;
|
||||
existingCusEnt?: FullCustomerEntitlement;
|
||||
freeTrial: FreeTrial | null;
|
||||
anchorToUnix?: number;
|
||||
}) => {
|
||||
// 1. If entitlement is boolean, or unlimited, or lifetime, then next reset at is null
|
||||
if (
|
||||
entitlement.feature.type === FeatureType.Boolean ||
|
||||
entitlement.allowance_type === AllowanceType.Unlimited ||
|
||||
entitlement.interval == EntInterval.Lifetime
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (nextResetAt) {
|
||||
return nextResetAt;
|
||||
}
|
||||
|
||||
// 3. If keepResetIntervals is true, return existing next reset at...
|
||||
if (keepResetIntervals && existingCusEnt?.next_reset_at) {
|
||||
return existingCusEnt.next_reset_at;
|
||||
}
|
||||
|
||||
// 4. Calculate next reset at...
|
||||
let nextResetAtCalculated = null;
|
||||
let trialEndTimestamp = freeTrialToStripeTimestamp({ freeTrial });
|
||||
if (
|
||||
freeTrial &&
|
||||
applyTrialToEntitlement(entitlement, freeTrial) &&
|
||||
trialEndTimestamp
|
||||
) {
|
||||
nextResetAtCalculated = new UTCDate(trialEndTimestamp! * 1000);
|
||||
}
|
||||
|
||||
let resetInterval = entitlement.interval as EntInterval;
|
||||
|
||||
nextResetAtCalculated = getNextEntitlementReset(
|
||||
nextResetAtCalculated,
|
||||
resetInterval,
|
||||
).getTime();
|
||||
|
||||
// If anchorToUnix, align next reset at to anchorToUnix...
|
||||
if (anchorToUnix && nextResetAtCalculated) {
|
||||
nextResetAtCalculated = subtractFromUnixTillAligned({
|
||||
targetUnix: anchorToUnix,
|
||||
originalUnix: nextResetAtCalculated,
|
||||
});
|
||||
}
|
||||
|
||||
// console.log(
|
||||
// "NEXT RESET AT",
|
||||
// format(new Date(nextResetAtCalculated), "dd MMM yyyy HH:mm:ss")
|
||||
// );
|
||||
|
||||
return nextResetAtCalculated;
|
||||
};
|
||||
|
||||
const initCusEntBalance = ({
|
||||
entitlement,
|
||||
curCusProduct,
|
||||
@@ -253,13 +187,15 @@ export const initCusEntitlement = ({
|
||||
options,
|
||||
nextResetAt,
|
||||
relatedPrice,
|
||||
existingCusEnt,
|
||||
keepResetIntervals = false,
|
||||
// existingCusEnt,
|
||||
// keepResetIntervals = false,
|
||||
trialEndsAt,
|
||||
anchorToUnix,
|
||||
entities,
|
||||
carryExistingUsages = false,
|
||||
curCusProduct,
|
||||
replaceables,
|
||||
now,
|
||||
}: {
|
||||
entitlement: EntitlementWithFeature;
|
||||
customer: Customer;
|
||||
@@ -268,13 +204,15 @@ export const initCusEntitlement = ({
|
||||
options?: FeatureOptions;
|
||||
nextResetAt?: number;
|
||||
relatedPrice?: Price;
|
||||
existingCusEnt?: FullCustomerEntitlement;
|
||||
keepResetIntervals?: boolean;
|
||||
// existingCusEnt?: FullCustomerEntitlement;
|
||||
// keepResetIntervals?: boolean;
|
||||
trialEndsAt?: number;
|
||||
anchorToUnix?: number;
|
||||
entities: Entity[];
|
||||
carryExistingUsages?: boolean;
|
||||
curCusProduct?: FullCusProduct;
|
||||
replaceables: AttachReplaceable[];
|
||||
now: number;
|
||||
}) => {
|
||||
let { newBalance, newEntities } = initCusEntBalance({
|
||||
entitlement,
|
||||
@@ -289,13 +227,15 @@ export const initCusEntitlement = ({
|
||||
(newBalance || 0) -
|
||||
replaceables.filter((r) => r.ent.id === entitlement.id).length;
|
||||
|
||||
let nextResetAtValue = initCusEntNextResetAt({
|
||||
let nextResetAtValue = initNextResetAt({
|
||||
entitlement,
|
||||
nextResetAt,
|
||||
keepResetIntervals,
|
||||
existingCusEnt,
|
||||
// keepResetIntervals,
|
||||
// existingCusEnt,
|
||||
trialEndsAt,
|
||||
freeTrial,
|
||||
anchorToUnix,
|
||||
now,
|
||||
});
|
||||
|
||||
// 3. Define expires at (TODO next time...)
|
||||
|
||||
@@ -4,11 +4,12 @@ import {
|
||||
AttachParams,
|
||||
AttachResultSchema,
|
||||
} from "../../../cusProducts/AttachParams.js";
|
||||
import { APIVersion, AttachConfig } from "@autumn/shared";
|
||||
import { APIVersion, AttachBranch, AttachConfig } from "@autumn/shared";
|
||||
import { attachToInsertParams } from "@/internal/products/productUtils.js";
|
||||
import { SuccessCode } from "@autumn/shared";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { handlePaidProduct } from "./handlePaidProduct.js";
|
||||
import { attachParamsToCurCusProduct } from "../../attachUtils/convertAttachParams.js";
|
||||
|
||||
export const handleAddProduct = async ({
|
||||
req,
|
||||
@@ -17,7 +18,7 @@ export const handleAddProduct = async ({
|
||||
config,
|
||||
}: {
|
||||
req: ExtendedRequest;
|
||||
res: any;
|
||||
res?: any;
|
||||
attachParams: AttachParams;
|
||||
config: AttachConfig;
|
||||
}) => {
|
||||
@@ -41,13 +42,20 @@ export const handleAddProduct = async ({
|
||||
const batchInsert = [];
|
||||
|
||||
for (const product of products) {
|
||||
let curCusProduct = attachParamsToCurCusProduct({ attachParams });
|
||||
let anchorToUnix = undefined;
|
||||
|
||||
if (curCusProduct && config.branch == AttachBranch.NewVersion) {
|
||||
anchorToUnix = curCusProduct.created_at;
|
||||
}
|
||||
|
||||
batchInsert.push(
|
||||
createFullCusProduct({
|
||||
db: req.db,
|
||||
attachParams: attachToInsertParams(attachParams, product),
|
||||
subscriptionId: undefined,
|
||||
billLaterOnly: true,
|
||||
carryExistingUsages: config.carryUsage,
|
||||
anchorToUnix,
|
||||
logger,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -72,8 +72,7 @@ export const getUsageInvoiceItems = async ({
|
||||
});
|
||||
|
||||
if (!sub) continue;
|
||||
|
||||
if (interval !== subToAutumnInterval(sub)) continue;
|
||||
if (interval && interval !== subToAutumnInterval(sub)) continue;
|
||||
|
||||
cusEntIds.push(cusEnt.id);
|
||||
|
||||
|
||||
@@ -19,16 +19,16 @@ import {
|
||||
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
|
||||
import { attachToInsertParams } from "@/internal/products/productUtils.js";
|
||||
import { insertInvoiceFromAttach } from "@/internal/invoices/invoiceUtils.js";
|
||||
import { updateStripeSubs } from "./updateStripeSubs.js";
|
||||
import { updateSubsDiffInt } from "./updateSubsDiffInt.js";
|
||||
|
||||
export const handleUpgradeFunction = async ({
|
||||
export const handleUpgradeDiffInterval = async ({
|
||||
req,
|
||||
res,
|
||||
attachParams,
|
||||
config,
|
||||
}: {
|
||||
req: ExtendedRequest;
|
||||
res: any;
|
||||
res?: any;
|
||||
attachParams: AttachParams;
|
||||
config: AttachConfig;
|
||||
}) => {
|
||||
@@ -53,7 +53,7 @@ export const handleUpgradeFunction = async ({
|
||||
});
|
||||
|
||||
logger.info("1. Updating current subscriptions in Stripe");
|
||||
let { newSubs, invoice, newInvoiceIds } = await updateStripeSubs({
|
||||
let { newSubs, invoice, newInvoiceIds } = await updateSubsDiffInt({
|
||||
db: req.db,
|
||||
curCusProduct,
|
||||
stripeCli,
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import Stripe from "stripe";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { createStripeSub } from "@/external/stripe/stripeSubUtils/createStripeSub.js";
|
||||
import { updateStripeSubscription } from "@/external/stripe/stripeSubUtils/updateStripeSub.js";
|
||||
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
||||
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
|
||||
import { AttachConfig, FullCusProduct } from "@autumn/shared";
|
||||
import { subItemInCusProduct } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
|
||||
import { updateCurSchedules } from "./updateCurSchedules.js";
|
||||
import { getStripeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
|
||||
import { addSubItemsToRemove } from "../attachFuncUtils.js";
|
||||
import { updateStripeSub } from "../../attachUtils/updateStripeSub/updateStripeSub.js";
|
||||
import {
|
||||
createUsageInvoiceItems,
|
||||
resetUsageBalances,
|
||||
} from "./createUsageInvoiceItems.js";
|
||||
|
||||
export const updateStripeSubs = async ({
|
||||
export const updateSubsDiffInt = async ({
|
||||
db,
|
||||
stripeCli,
|
||||
curCusProduct,
|
||||
@@ -53,9 +55,18 @@ export const updateStripeSubs = async ({
|
||||
now: attachParams.now,
|
||||
});
|
||||
|
||||
// 2. Create prorations for single use items
|
||||
let { invoiceItems, cusEntIds } = await createUsageInvoiceItems({
|
||||
db,
|
||||
attachParams,
|
||||
cusProduct: curCusProduct,
|
||||
stripeSubs,
|
||||
logger,
|
||||
});
|
||||
|
||||
// 3. Update current subscription
|
||||
logger.info("1.2: Updating current subscription");
|
||||
const { updatedSub, latestInvoice, cusEntIds } = await updateStripeSub({
|
||||
const { updatedSub, latestInvoice } = await updateStripeSub({
|
||||
db,
|
||||
attachParams,
|
||||
config,
|
||||
@@ -66,6 +77,12 @@ export const updateStripeSubs = async ({
|
||||
stripeSubs,
|
||||
});
|
||||
|
||||
await resetUsageBalances({
|
||||
db,
|
||||
cusEntIds,
|
||||
cusProduct: curCusProduct,
|
||||
});
|
||||
|
||||
let newSubs = [updatedSub!];
|
||||
const newInvoiceIds = latestInvoice ? [latestInvoice.id] : [];
|
||||
|
||||
@@ -10,6 +10,7 @@ import { APIVersion, AttachConfig, CusProductStatus } from "@autumn/shared";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { updateSubsByInt } from "./updateSubsSameInt.js";
|
||||
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
|
||||
import { formatUnixToDate } from "@/utils/genUtils.js";
|
||||
|
||||
export const handleUpgradeSameInterval = async ({
|
||||
req,
|
||||
@@ -18,7 +19,7 @@ export const handleUpgradeSameInterval = async ({
|
||||
config,
|
||||
}: {
|
||||
req: ExtendedRequest;
|
||||
res: any;
|
||||
res?: any;
|
||||
attachParams: AttachParams;
|
||||
config: AttachConfig;
|
||||
}) => {
|
||||
@@ -58,31 +59,38 @@ export const handleUpgradeSameInterval = async ({
|
||||
});
|
||||
|
||||
logger.info(`3. Creating new cus product`);
|
||||
|
||||
logger.info(
|
||||
`Anchoring to unix: ${formatUnixToDate(stripeSubs[0].current_period_end * 1000)}`,
|
||||
);
|
||||
await createFullCusProduct({
|
||||
db: req.db,
|
||||
attachParams: attachToInsertParams(attachParams, attachParams.products[0]),
|
||||
subscriptionIds: curCusProduct!.subscription_ids || [],
|
||||
disableFreeTrial: config.disableTrial,
|
||||
carryExistingUsages: config.carryUsage,
|
||||
carryOverTrial: config.carryTrial,
|
||||
anchorToUnix: stripeSubs[0].current_period_end * 1000,
|
||||
logger,
|
||||
});
|
||||
|
||||
let apiVersion = attachParams.org.api_version || APIVersion.v1;
|
||||
if (apiVersion >= APIVersion.v1_1) {
|
||||
res.status(200).json(
|
||||
AttachResultSchema.parse({
|
||||
customer_id: attachParams.customer.id,
|
||||
product_ids: attachParams.products.map((p) => p.id),
|
||||
code: "updated_product_successfully",
|
||||
if (res) {
|
||||
let apiVersion = attachParams.org.api_version || APIVersion.v1;
|
||||
if (apiVersion >= APIVersion.v1_1) {
|
||||
res.status(200).json(
|
||||
AttachResultSchema.parse({
|
||||
customer_id: attachParams.customer.id,
|
||||
product_ids: attachParams.products.map((p) => p.id),
|
||||
code: "updated_product_successfully",
|
||||
message: `Successfully updated product`,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: `Successfully updated product`,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: `Successfully updated product`,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ import { handleAttachRaceCondition } from "@/external/redis/redisUtils.js";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js";
|
||||
import { AttachBodySchema } from "./models/AttachBody.js";
|
||||
import { processAttachBody } from "./attachUtils/processAttachBody.js";
|
||||
import { processAttachBody } from "./attachUtils/attachParams/processAttachBody.js";
|
||||
import { handleAttachPreview } from "./handleAttachPreview/handleAttachPreview.js";
|
||||
import { handleAttach } from "./handleAttach.js";
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import {
|
||||
APIVersion,
|
||||
Feature,
|
||||
FullCusProduct,
|
||||
FullCustomer,
|
||||
FullProduct,
|
||||
} from "@autumn/shared";
|
||||
import { getStripeCusData } from "./attachParamsUtils/getStripeCusData.js";
|
||||
import { getFreeTrialAfterFingerprint } from "@/internal/products/free-trials/freeTrialUtils.js";
|
||||
import { orgToVersion } from "@/utils/versionUtils.js";
|
||||
|
||||
export const checkToAttachParams = async ({
|
||||
req,
|
||||
customer,
|
||||
product,
|
||||
logger,
|
||||
}: {
|
||||
req: ExtendedRequest;
|
||||
customer: FullCustomer;
|
||||
product: FullProduct;
|
||||
logger: any;
|
||||
}) => {
|
||||
const { org, env, db } = req;
|
||||
|
||||
const apiVersion =
|
||||
orgToVersion({
|
||||
org,
|
||||
reqApiVersion: req.apiVersion,
|
||||
}) || APIVersion.v1;
|
||||
|
||||
const stripeCli = createStripeCli({ org, env });
|
||||
let stripeCusData = await getStripeCusData({
|
||||
stripeCli,
|
||||
stripeId: customer.processor?.id,
|
||||
});
|
||||
|
||||
let freeTrial = await getFreeTrialAfterFingerprint({
|
||||
db,
|
||||
freeTrial: product.free_trial,
|
||||
fingerprint: customer.fingerprint,
|
||||
internalCustomerId: customer.internal_id,
|
||||
multipleAllowed: org.config.multiple_trials,
|
||||
productId: product.id,
|
||||
});
|
||||
|
||||
const { stripeCus, paymentMethod, now } = stripeCusData;
|
||||
|
||||
const attachParams: AttachParams = {
|
||||
stripeCli,
|
||||
stripeCus,
|
||||
now,
|
||||
paymentMethod,
|
||||
|
||||
customer,
|
||||
products: [product],
|
||||
optionsList: [],
|
||||
prices: product.prices,
|
||||
entitlements: product.entitlements,
|
||||
freeTrial,
|
||||
replaceables: [],
|
||||
|
||||
// Others
|
||||
req,
|
||||
org: req.org,
|
||||
entities: customer.entities,
|
||||
features: req.features,
|
||||
internalEntityId: customer.entity?.internal_id,
|
||||
cusProducts: customer.customer_products,
|
||||
|
||||
// Others
|
||||
apiVersion,
|
||||
// successUrl: attachBody.success_url,
|
||||
// invoiceOnly: attachBody.invoice_only,
|
||||
// billingAnchor: attachBody.billing_cycle_anchor,
|
||||
// metadata: attachBody.metadata,
|
||||
// disableFreeTrial: attachBody.free_trial === false || false,
|
||||
// checkoutSessionParams: attachBody.checkout_session_params,
|
||||
// isCustom: attachBody.is_custom,
|
||||
};
|
||||
|
||||
return attachParams;
|
||||
};
|
||||
@@ -1,11 +1,10 @@
|
||||
import { listCusPaymentMethods } from "@/external/stripe/stripeCusUtils.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { AttachBody } from "../models/AttachBody.js";
|
||||
import { AttachBody } from "../../models/AttachBody.js";
|
||||
import { processAttachBody } from "./processAttachBody.js";
|
||||
import { orgToVersion } from "@/utils/versionUtils.js";
|
||||
import { APIVersion } from "@autumn/shared";
|
||||
import { AttachParams } from "../../cusProducts/AttachParams.js";
|
||||
import Stripe from "stripe";
|
||||
import { AttachParams } from "../../../cusProducts/AttachParams.js";
|
||||
|
||||
export const getAttachParams = async ({
|
||||
req,
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { AttachBody } from "../models/AttachBody.js";
|
||||
import { AttachBody } from "../../models/AttachBody.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import {
|
||||
CreateFreeTrial,
|
||||
@@ -12,9 +12,9 @@ import {
|
||||
} from "@autumn/shared";
|
||||
import { ProductService } from "@/internal/products/ProductService.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import { getOrCreateCustomer } from "../../cusUtils/getOrCreateCustomer.js";
|
||||
import { getExistingCusProducts } from "../../cusProducts/cusProductUtils/getExistingCusProducts.js";
|
||||
import { mapOptionsList } from "./mapOptionsList.js";
|
||||
import { getOrCreateCustomer } from "../../../cusUtils/getOrCreateCustomer.js";
|
||||
import { getExistingCusProducts } from "../../../cusProducts/cusProductUtils/getExistingCusProducts.js";
|
||||
import { mapOptionsList } from "../mapOptionsList.js";
|
||||
import {
|
||||
getFreeTrialAfterFingerprint,
|
||||
handleNewFreeTrial,
|
||||
@@ -22,12 +22,12 @@ import {
|
||||
import {
|
||||
cusProductToEnts,
|
||||
cusProductToPrices,
|
||||
} from "../../cusProducts/cusProductUtils/convertCusProduct.js";
|
||||
} from "../../../cusProducts/cusProductUtils/convertCusProduct.js";
|
||||
import { handleNewProductItems } from "@/internal/products/product-items/productItemUtils/handleNewProductItems.js";
|
||||
import { getEntsWithFeature } from "@/internal/products/entitlements/entitlementUtils.js";
|
||||
import { isMainProduct } from "@/internal/products/productUtils/classifyProduct.js";
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
import { getStripeCusData } from "../getAttachParams/getStripeCusData.js";
|
||||
import { getStripeCusData } from "./attachParamsUtils/getStripeCusData.js";
|
||||
|
||||
const getProductsForAttach = async ({
|
||||
req,
|
||||
@@ -134,6 +134,7 @@ const getPricesAndEnts = async ({
|
||||
fingerprint: customer.fingerprint,
|
||||
internalCustomerId: customer.internal_id,
|
||||
multipleAllowed: org.config.multiple_trials,
|
||||
productId: freeTrialProduct.id,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -194,6 +195,7 @@ const getPricesAndEnts = async ({
|
||||
fingerprint: customer.fingerprint,
|
||||
internalCustomerId: customer.internal_id,
|
||||
multipleAllowed: org.config.multiple_trials,
|
||||
productId: product.id,
|
||||
});
|
||||
|
||||
const prodIsMain = isMainProduct({ product: products[0], prices });
|
||||
@@ -236,6 +238,7 @@ export const processAttachBody = async ({
|
||||
stripeCli,
|
||||
stripeId: customer.processor?.id,
|
||||
});
|
||||
|
||||
const { stripeCus, paymentMethod, now } = stripeCusData;
|
||||
|
||||
const {
|
||||
@@ -252,13 +255,6 @@ export const processAttachBody = async ({
|
||||
products,
|
||||
});
|
||||
|
||||
if (attachBody.free_trial === false) {
|
||||
throw new RecaseError({
|
||||
message: "Free trial is not allowed",
|
||||
code: ErrCode.InvalidRequest,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
customer,
|
||||
products,
|
||||
@@ -80,6 +80,8 @@ export const getAttachConfig = async ({
|
||||
branch == AttachBranch.Downgrade ||
|
||||
attachBody.free_trial === false;
|
||||
|
||||
let carryTrial = branch === AttachBranch.NewVersion;
|
||||
|
||||
let sameIntervals = intervalsAreSame({ attachParams });
|
||||
|
||||
let config: AttachConfig = {
|
||||
@@ -92,22 +94,8 @@ export const getAttachConfig = async ({
|
||||
invoiceOnly: flags.invoiceOnly,
|
||||
disableMerge: org.config.merge_billing_cycles === false,
|
||||
sameIntervals,
|
||||
carryTrial,
|
||||
};
|
||||
|
||||
return { flags, config };
|
||||
};
|
||||
|
||||
const webhookToConfig = ({ org, env }: { org: Organization; env: AppEnv }) => {
|
||||
const config: AttachConfig = {
|
||||
branch: AttachBranch.NewVersion, // not needed...
|
||||
carryUsage: false, // not needed...
|
||||
onlyCheckout: false,
|
||||
proration: ProrationBehavior.Immediately,
|
||||
disableTrial: false,
|
||||
invoiceOnly: false,
|
||||
disableMerge: false,
|
||||
sameIntervals: false,
|
||||
};
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
AttachResultSchema,
|
||||
} from "../../cusProducts/AttachParams.js";
|
||||
import { AttachBranch, AttachFunction } from "@autumn/shared";
|
||||
import { handleUpgradeFunction } from "../attachFunctions/upgradeDiffIntFlow/handleUpgradeDiffInt.js";
|
||||
import { handleUpgradeDiffInterval } from "../attachFunctions/upgradeDiffIntFlow/handleUpgradeDiffInt.js";
|
||||
import { handleCreateCheckout } from "../../add-product/handleCreateCheckout.js";
|
||||
import { handleAddProduct } from "../attachFunctions/addProductFlow/handleAddProduct.js";
|
||||
import { AttachBody } from "../models/AttachBody.js";
|
||||
@@ -213,7 +213,7 @@ export const runAttachFunction = async ({
|
||||
}
|
||||
|
||||
if (attachFunction == AttachFunction.UpgradeDiffInterval) {
|
||||
return await handleUpgradeFunction({
|
||||
return await handleUpgradeDiffInterval({
|
||||
req,
|
||||
res,
|
||||
attachParams,
|
||||
|
||||
@@ -103,12 +103,13 @@ export const updateStripeSub = async ({
|
||||
}
|
||||
|
||||
// 2. Create prorations for single use items
|
||||
let { cusEntIds } = await createUsageInvoiceItems({
|
||||
let { invoiceItems, cusEntIds } = await createUsageInvoiceItems({
|
||||
db,
|
||||
attachParams,
|
||||
cusProduct: curMainProduct!,
|
||||
stripeSubs,
|
||||
logger,
|
||||
interval: config.sameIntervals ? interval : undefined,
|
||||
});
|
||||
|
||||
// 3. Create prorations for continuous use items
|
||||
|
||||
@@ -2,7 +2,7 @@ import { handleAttachRaceCondition } from "@/external/redis/redisUtils.js";
|
||||
import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import { AttachBodySchema } from "./models/AttachBody.js";
|
||||
import { getAttachParams } from "./attachUtils/getAttachParams.js";
|
||||
import { getAttachParams } from "./attachUtils/attachParams/getAttachParams.js";
|
||||
import { getAttachBranch } from "./attachUtils/getAttachBranch.js";
|
||||
import { getAttachConfig } from "./attachUtils/getAttachConfig.js";
|
||||
import { handleAttachErrors } from "./attachUtils/handleAttachErrors.js";
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { AttachParams } from "../../cusProducts/AttachParams.js";
|
||||
import { AttachBody } from "../models/AttachBody.js";
|
||||
import { getAttachBranch } from "../attachUtils/getAttachBranch.js";
|
||||
import { getAttachConfig } from "../attachUtils/getAttachConfig.js";
|
||||
import { AttachFunction } from "@autumn/shared";
|
||||
import { getAttachFunction } from "../attachUtils/getAttachFunction.js";
|
||||
import { cusProductToProduct } from "../../cusProducts/cusProductUtils/convertCusProduct.js";
|
||||
import { attachParamToCusProducts } from "../attachUtils/convertAttachParams.js";
|
||||
import { getDowngradeProductPreview } from "./getDowngradeProductPreview.js";
|
||||
import { getNewProductPreview } from "./getNewProductPreview.js";
|
||||
import { getUpgradeProductPreview } from "./getUpgradeProductPreview.js";
|
||||
|
||||
export const attachParamsToPreview = async ({
|
||||
req,
|
||||
attachParams,
|
||||
attachBody,
|
||||
logger,
|
||||
}: {
|
||||
req: ExtendedRequest;
|
||||
attachParams: AttachParams;
|
||||
attachBody: AttachBody;
|
||||
logger: any;
|
||||
}) => {
|
||||
// Handle existing product
|
||||
const branch = await getAttachBranch({
|
||||
req,
|
||||
attachBody,
|
||||
attachParams,
|
||||
fromPreview: true,
|
||||
});
|
||||
|
||||
const { flags, config } = await getAttachConfig({
|
||||
req,
|
||||
attachParams,
|
||||
attachBody,
|
||||
branch,
|
||||
});
|
||||
|
||||
const func = await getAttachFunction({
|
||||
branch,
|
||||
attachParams,
|
||||
attachBody,
|
||||
config,
|
||||
});
|
||||
|
||||
logger.info("--------------------------------");
|
||||
logger.info(`ATTACH PREVIEW (org: ${attachParams.org.id})`);
|
||||
logger.info(`Branch: ${branch}, Function: ${func}`);
|
||||
|
||||
let now = attachParams.now || Date.now();
|
||||
|
||||
let preview: any = null;
|
||||
|
||||
if (
|
||||
func == AttachFunction.AddProduct ||
|
||||
func == AttachFunction.CreateCheckout ||
|
||||
func == AttachFunction.OneOff
|
||||
) {
|
||||
preview = await getNewProductPreview({
|
||||
attachParams,
|
||||
now,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
|
||||
if (func == AttachFunction.ScheduleProduct) {
|
||||
preview = await getDowngradeProductPreview({
|
||||
attachParams,
|
||||
now,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
func == AttachFunction.UpgradeDiffInterval ||
|
||||
func == AttachFunction.UpdatePrepaidQuantity ||
|
||||
func == AttachFunction.UpgradeSameInterval
|
||||
) {
|
||||
preview = await getUpgradeProductPreview({
|
||||
req,
|
||||
attachParams,
|
||||
branch,
|
||||
now,
|
||||
});
|
||||
}
|
||||
|
||||
const { curMainProduct, curScheduledProduct } = attachParamToCusProducts({
|
||||
attachParams,
|
||||
});
|
||||
|
||||
return {
|
||||
branch,
|
||||
func,
|
||||
...preview,
|
||||
current_product: curMainProduct
|
||||
? cusProductToProduct({
|
||||
cusProduct: curMainProduct,
|
||||
})
|
||||
: null,
|
||||
scheduled_product: curScheduledProduct,
|
||||
};
|
||||
};
|
||||
@@ -5,14 +5,9 @@ import {
|
||||
} from "../attachUtils/convertAttachParams.js";
|
||||
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import {
|
||||
getFirstInterval,
|
||||
getLastInterval,
|
||||
} from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
|
||||
import { subToAutumnInterval } from "@/external/stripe/utils.js";
|
||||
import { getLastInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
|
||||
import { getItemsForNewProduct } from "@/internal/invoices/previewItemUtils/getItemsForNewProduct.js";
|
||||
import { getItemsForCurProduct } from "@/internal/invoices/previewItemUtils/getItemsForCurProduct.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import { getOptions } from "@/internal/api/entitled/checkUtils.js";
|
||||
import { mapToProductItems } from "@/internal/products/productV2Utils.js";
|
||||
import Stripe from "stripe";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import { getAttachBranch } from "../attachUtils/getAttachBranch.js";
|
||||
import { getAttachParams } from "../attachUtils/getAttachParams.js";
|
||||
import { getAttachParams } from "../attachUtils/attachParams/getAttachParams.js";
|
||||
import { AttachBodySchema } from "../models/AttachBody.js";
|
||||
import { getAttachConfig } from "../attachUtils/getAttachConfig.js";
|
||||
import { getAttachFunction } from "../attachUtils/getAttachFunction.js";
|
||||
@@ -8,12 +8,11 @@ import { ExtendedResponse } from "@/utils/models/Request.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { getNewProductPreview } from "./getNewProductPreview.js";
|
||||
import { getUpgradeProductPreview } from "./getUpgradeProductPreview.js";
|
||||
import { getStripeNow } from "@/utils/scriptUtils/testClockUtils.js";
|
||||
import { getUpdateEntsPreview } from "./getUpdateEntsPreview.js";
|
||||
import { getDowngradeProductPreview } from "./getDowngradeProductPreview.js";
|
||||
import { attachParamToCusProducts } from "../attachUtils/convertAttachParams.js";
|
||||
import { cusProductToProduct } from "../../cusProducts/cusProductUtils/convertCusProduct.js";
|
||||
import { AttachFunction } from "@autumn/shared";
|
||||
import { attachParamsToPreview } from "./attachParamsToPreview.js";
|
||||
|
||||
export const handleAttachPreview = (req: any, res: any) =>
|
||||
routeHandler({
|
||||
@@ -29,82 +28,92 @@ export const handleAttachPreview = (req: any, res: any) =>
|
||||
attachBody,
|
||||
});
|
||||
|
||||
// Handle existing product
|
||||
const branch = await getAttachBranch({
|
||||
req,
|
||||
attachBody,
|
||||
attachParams,
|
||||
fromPreview: true,
|
||||
});
|
||||
|
||||
const { flags, config } = await getAttachConfig({
|
||||
const attachPreview = await attachParamsToPreview({
|
||||
req,
|
||||
attachParams,
|
||||
attachBody,
|
||||
branch,
|
||||
logger,
|
||||
});
|
||||
|
||||
const func = await getAttachFunction({
|
||||
branch,
|
||||
attachParams,
|
||||
attachBody,
|
||||
config,
|
||||
});
|
||||
res.status(200).json(attachPreview);
|
||||
return;
|
||||
|
||||
logger.info("--------------------------------");
|
||||
logger.info(`ATTACH PREVIEW (org: ${attachParams.org.id})`);
|
||||
logger.info(`Branch: ${branch}, Function: ${func}`);
|
||||
// // Handle existing product
|
||||
// const branch = await getAttachBranch({
|
||||
// req,
|
||||
// attachBody,
|
||||
// attachParams,
|
||||
// fromPreview: true,
|
||||
// });
|
||||
|
||||
let now = attachParams.now || Date.now();
|
||||
// const { flags, config } = await getAttachConfig({
|
||||
// req,
|
||||
// attachParams,
|
||||
// attachBody,
|
||||
// branch,
|
||||
// });
|
||||
|
||||
let preview: any = null;
|
||||
// const func = await getAttachFunction({
|
||||
// branch,
|
||||
// attachParams,
|
||||
// attachBody,
|
||||
// config,
|
||||
// });
|
||||
|
||||
if (
|
||||
func == AttachFunction.AddProduct ||
|
||||
func == AttachFunction.CreateCheckout ||
|
||||
func == AttachFunction.OneOff
|
||||
) {
|
||||
preview = await getNewProductPreview({
|
||||
attachParams,
|
||||
now,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
// logger.info("--------------------------------");
|
||||
// logger.info(`ATTACH PREVIEW (org: ${attachParams.org.id})`);
|
||||
// logger.info(`Branch: ${branch}, Function: ${func}`);
|
||||
|
||||
if (func == AttachFunction.ScheduleProduct) {
|
||||
preview = await getDowngradeProductPreview({
|
||||
attachParams,
|
||||
now,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
// let now = attachParams.now || Date.now();
|
||||
|
||||
if (
|
||||
func == AttachFunction.UpgradeDiffInterval ||
|
||||
func == AttachFunction.UpdatePrepaidQuantity ||
|
||||
func == AttachFunction.UpgradeSameInterval
|
||||
) {
|
||||
preview = await getUpgradeProductPreview({
|
||||
req,
|
||||
attachParams,
|
||||
branch,
|
||||
now,
|
||||
});
|
||||
}
|
||||
// let preview: any = null;
|
||||
|
||||
const { curMainProduct, curScheduledProduct } = attachParamToCusProducts({
|
||||
attachParams,
|
||||
});
|
||||
// if (
|
||||
// func == AttachFunction.AddProduct ||
|
||||
// func == AttachFunction.CreateCheckout ||
|
||||
// func == AttachFunction.OneOff
|
||||
// ) {
|
||||
// preview = await getNewProductPreview({
|
||||
// attachParams,
|
||||
// now,
|
||||
// logger,
|
||||
// });
|
||||
// }
|
||||
|
||||
res.status(200).json({
|
||||
branch,
|
||||
...preview,
|
||||
current_product: curMainProduct
|
||||
? cusProductToProduct({
|
||||
cusProduct: curMainProduct,
|
||||
})
|
||||
: null,
|
||||
scheduled_product: curScheduledProduct,
|
||||
});
|
||||
// if (func == AttachFunction.ScheduleProduct) {
|
||||
// preview = await getDowngradeProductPreview({
|
||||
// attachParams,
|
||||
// now,
|
||||
// logger,
|
||||
// });
|
||||
// }
|
||||
|
||||
// if (
|
||||
// func == AttachFunction.UpgradeDiffInterval ||
|
||||
// func == AttachFunction.UpdatePrepaidQuantity ||
|
||||
// func == AttachFunction.UpgradeSameInterval
|
||||
// ) {
|
||||
// preview = await getUpgradeProductPreview({
|
||||
// req,
|
||||
// attachParams,
|
||||
// branch,
|
||||
// now,
|
||||
// });
|
||||
// }
|
||||
|
||||
// const { curMainProduct, curScheduledProduct } = attachParamToCusProducts({
|
||||
// attachParams,
|
||||
// });
|
||||
|
||||
// res.status(200).json({
|
||||
// branch,
|
||||
// ...preview,
|
||||
// current_product: curMainProduct
|
||||
// ? cusProductToProduct({
|
||||
// cusProduct: curMainProduct,
|
||||
// })
|
||||
// : null,
|
||||
// scheduled_product: curScheduledProduct,
|
||||
// });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -73,6 +73,7 @@ export type AttachParams = {
|
||||
|
||||
export type InsertCusProductParams = {
|
||||
req?: any;
|
||||
now?: number;
|
||||
|
||||
customer: FullCustomer;
|
||||
org: Organization;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { applyTrialToEntitlement } from "@/internal/products/entitlements/entitlementUtils.js";
|
||||
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
|
||||
import { subtractFromUnixTillAligned } from "@/internal/products/prices/billingIntervalUtils.js";
|
||||
import { formatUnixToDate } from "@/utils/genUtils.js";
|
||||
import { getNextEntitlementReset } from "@/utils/timeUtils.js";
|
||||
import {
|
||||
EntInterval,
|
||||
AllowanceType,
|
||||
EntitlementWithFeature,
|
||||
FeatureType,
|
||||
FreeTrial,
|
||||
} from "@autumn/shared";
|
||||
import { UTCDate } from "@date-fns/utc";
|
||||
|
||||
export const initNextResetAt = ({
|
||||
entitlement,
|
||||
nextResetAt,
|
||||
trialEndsAt,
|
||||
freeTrial,
|
||||
anchorToUnix,
|
||||
now,
|
||||
}: {
|
||||
entitlement: EntitlementWithFeature;
|
||||
nextResetAt?: number;
|
||||
trialEndsAt?: number;
|
||||
freeTrial: FreeTrial | null;
|
||||
anchorToUnix?: number;
|
||||
now: number;
|
||||
}) => {
|
||||
// 1. If entitlement is boolean, or unlimited, or lifetime, then next reset at is null
|
||||
if (
|
||||
entitlement.feature.type === FeatureType.Boolean ||
|
||||
entitlement.allowance_type === AllowanceType.Unlimited ||
|
||||
entitlement.interval == EntInterval.Lifetime
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. If nextResetAt is provided, return it...
|
||||
if (nextResetAt) {
|
||||
return nextResetAt;
|
||||
}
|
||||
|
||||
// 3. Calculate next reset at...
|
||||
let nextResetAtCalculated = null;
|
||||
let trialEndTimestamp = trialEndsAt
|
||||
? Math.round(trialEndsAt / 1000)
|
||||
: freeTrial
|
||||
? freeTrialToStripeTimestamp({ freeTrial, now })
|
||||
: null;
|
||||
|
||||
if (
|
||||
freeTrial &&
|
||||
applyTrialToEntitlement(entitlement, freeTrial) &&
|
||||
trialEndTimestamp
|
||||
) {
|
||||
nextResetAtCalculated = new UTCDate(trialEndTimestamp! * 1000);
|
||||
}
|
||||
|
||||
let resetInterval = entitlement.interval as EntInterval;
|
||||
|
||||
nextResetAtCalculated = getNextEntitlementReset(
|
||||
nextResetAtCalculated || new UTCDate(now),
|
||||
resetInterval,
|
||||
).getTime();
|
||||
|
||||
// If anchorToUnix, align next reset at to anchorToUnix...
|
||||
if (anchorToUnix && nextResetAtCalculated) {
|
||||
nextResetAtCalculated = subtractFromUnixTillAligned({
|
||||
targetUnix: anchorToUnix,
|
||||
originalUnix: nextResetAtCalculated,
|
||||
});
|
||||
}
|
||||
|
||||
return nextResetAtCalculated;
|
||||
};
|
||||
@@ -106,6 +106,3 @@ devRouter.delete("/api_key/:id", withOrgAuth, async (req: any, res) => {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// am_live_3ZaPDgqt7K4GkdirAU9oDFT3
|
||||
// am_test_3ZTRcHdEsdAdoxvSUGL8L35x
|
||||
|
||||
@@ -9,7 +9,6 @@ import { featureRouter } from "./features/featureRouter.js";
|
||||
import { productRouter } from "./products/internalProductRouter.js";
|
||||
import { devRouter } from "./dev/devRouter.js";
|
||||
import { cusRouter } from "./customers/internalCusRouter.js";
|
||||
import { testRouter } from "./test/testRouter.js";
|
||||
import { onboardingRouter } from "./orgs/onboarding/onboardingRouter.js";
|
||||
import { handlePostOrg } from "./orgs/handlers/handlePostOrg.js";
|
||||
import { Autumn } from "autumn-js";
|
||||
@@ -31,7 +30,6 @@ mainRouter.use("/features", withOrgAuth, featureRouter);
|
||||
mainRouter.use("/products", withOrgAuth, productRouter);
|
||||
mainRouter.use("/dev", devRouter);
|
||||
mainRouter.use("/customers", withOrgAuth, cusRouter);
|
||||
mainRouter.use("/test", testRouter);
|
||||
|
||||
mainRouter.use(
|
||||
"/api/autumn",
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import { ProductService } from "@/internal/products/ProductService.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { CusProductStatus, ErrCode } from "@autumn/shared";
|
||||
import {
|
||||
BillingType,
|
||||
CusProductStatus,
|
||||
ErrCode,
|
||||
UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
import { routeHandler } from "@/utils/routerUtils.js";
|
||||
import express from "express";
|
||||
import { constructMigrationJob } from "@/internal/migrations/migrationUtils.js";
|
||||
import { MigrationService } from "@/internal/migrations/MigrationService.js";
|
||||
import { JobName } from "@/queue/JobName.js";
|
||||
import { addTaskToQueue } from "@/queue/queueUtils.js";
|
||||
import { pricesOnlyOneOff } from "@/internal/products/prices/priceUtils.js";
|
||||
import {
|
||||
getBillingType,
|
||||
pricesOnlyOneOff,
|
||||
} from "@/internal/products/prices/priceUtils.js";
|
||||
import { isFreeProduct } from "@/internal/products/productUtils.js";
|
||||
import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js";
|
||||
import { findPrepaidPrice } from "../products/prices/priceUtils/findPriceUtils.js";
|
||||
|
||||
export const migrationRouter = express.Router();
|
||||
|
||||
@@ -19,7 +28,7 @@ migrationRouter.post("", async (req: any, res: any) => {
|
||||
res,
|
||||
action: "migrate",
|
||||
handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
|
||||
const { orgId, env, db } = req;
|
||||
const { orgId, env, db, features } = req;
|
||||
|
||||
const { from_product_id, from_version, to_product_id, to_version } =
|
||||
req.body;
|
||||
@@ -68,6 +77,60 @@ migrationRouter.post("", async (req: any, res: any) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (fromProduct.is_add_on || toProduct.is_add_on) {
|
||||
throw new RecaseError({
|
||||
message: `Cannot migrate customers for add on products`,
|
||||
code: ErrCode.InvalidRequest,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
for (const price of toProduct.prices) {
|
||||
let billingType = getBillingType(price.config);
|
||||
if (billingType != BillingType.UsageInAdvance) continue;
|
||||
|
||||
let config = price.config as UsagePriceConfig;
|
||||
let internalFeatureId = config.internal_feature_id;
|
||||
let feature = features.find((f) => f.internal_id == internalFeatureId)!;
|
||||
|
||||
for (const price of fromProduct.prices) {
|
||||
let prepaidPrice = findPrepaidPrice({
|
||||
prices: fromProduct.prices,
|
||||
internalFeatureId,
|
||||
});
|
||||
|
||||
if (!prepaidPrice) {
|
||||
throw new RecaseError({
|
||||
message: `New product has prepaid price for feature ${feature.name}, but old product does not, can't perform migration`,
|
||||
code: ErrCode.InvalidRequest,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!isFreeProduct(fromProduct.prices) &&
|
||||
isFreeProduct(toProduct.prices)
|
||||
) {
|
||||
throw new RecaseError({
|
||||
message: `Cannot migrate customers from paid product to free product`,
|
||||
code: ErrCode.InvalidRequest,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
isFreeProduct(fromProduct.prices) &&
|
||||
!isFreeProduct(toProduct.prices)
|
||||
) {
|
||||
throw new RecaseError({
|
||||
message: `Cannot migrate customers from free product to paid product`,
|
||||
code: ErrCode.InvalidRequest,
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
// 1. Create migration JOB
|
||||
let migrationJob = constructMigrationJob({
|
||||
fromProduct,
|
||||
@@ -24,6 +24,11 @@ import { getBillingType } from "@/internal/products/prices/priceUtils.js";
|
||||
import { FeatureOptions } from "@autumn/shared";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
|
||||
import { CusService } from "@/internal/customers/CusService.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
import { migrationToAttachParams } from "../migrationUtils/migrationToAttachParams.js";
|
||||
import { runMigrationAttach } from "../migrationUtils/runMigrationAttach.js";
|
||||
|
||||
export const migrateCustomer = async ({
|
||||
db,
|
||||
@@ -49,81 +54,110 @@ export const migrateCustomer = async ({
|
||||
features: Feature[];
|
||||
}) => {
|
||||
try {
|
||||
let cusProducts = await CusProductService.list({
|
||||
const stripeCli = createStripeCli({ org, env });
|
||||
let fullCus = await CusService.getFull({
|
||||
db,
|
||||
internalCustomerId: customer.internal_id,
|
||||
inStatuses: [CusProductStatus.Active, CusProductStatus.PastDue],
|
||||
idOrInternalId: customer.id!,
|
||||
orgId,
|
||||
env,
|
||||
withEntities: true,
|
||||
});
|
||||
|
||||
let entities = await EntityService.list({
|
||||
// 1. Build req object
|
||||
let req = {
|
||||
db,
|
||||
internalCustomerId: customer.internal_id,
|
||||
});
|
||||
orgId,
|
||||
env,
|
||||
org,
|
||||
features,
|
||||
logtail: logger,
|
||||
timestamp: Date.now(),
|
||||
} as ExtendedRequest;
|
||||
|
||||
let curCusProduct = cusProducts.find(
|
||||
const cusProducts = fullCus.customer_products;
|
||||
const filteredCusProducts = cusProducts.filter(
|
||||
(cp: FullCusProduct) => cp.product.internal_id == fromProduct.internal_id,
|
||||
);
|
||||
|
||||
if (!curCusProduct) {
|
||||
logger.error(
|
||||
`Customer ${customer.id} does not have a ${fromProduct.internal_id} cus product, skipping migration`,
|
||||
);
|
||||
return false;
|
||||
for (const cusProduct of filteredCusProducts) {
|
||||
const attachParams = await migrationToAttachParams({
|
||||
req,
|
||||
stripeCli,
|
||||
customer: fullCus,
|
||||
cusProduct,
|
||||
newProduct: toProduct,
|
||||
});
|
||||
|
||||
await runMigrationAttach({
|
||||
req,
|
||||
attachParams,
|
||||
});
|
||||
}
|
||||
|
||||
let attachParams: AttachParams = {
|
||||
org,
|
||||
customer,
|
||||
products: [toProduct],
|
||||
prices: toProduct.prices,
|
||||
entitlements: toProduct.entitlements,
|
||||
freeTrial: toProduct.free_trial || null,
|
||||
features,
|
||||
optionsList: curCusProduct.options,
|
||||
entities,
|
||||
cusProducts,
|
||||
fromMigration: true,
|
||||
};
|
||||
// // let cusProducts = await CusProductService.list({
|
||||
// // db,
|
||||
// // internalCustomerId: customer.internal_id,
|
||||
// // inStatuses: [CusProductStatus.Active, CusProductStatus.PastDue],
|
||||
// // });
|
||||
|
||||
// Get prepaid prices
|
||||
let prepaidPrices = toProduct.prices.filter(
|
||||
(price: Price) =>
|
||||
getBillingType(price.config!) === BillingType.UsageInAdvance,
|
||||
);
|
||||
// // let entities = await EntityService.list({
|
||||
// // db,
|
||||
// // internalCustomerId: customer.internal_id,
|
||||
// // });
|
||||
|
||||
for (const prepaidPrice of prepaidPrices) {
|
||||
let config = prepaidPrice.config as UsagePriceConfig;
|
||||
// let attachParams: AttachParams = {
|
||||
// org,
|
||||
// customer,
|
||||
// products: [toProduct],
|
||||
// prices: toProduct.prices,
|
||||
// entitlements: toProduct.entitlements,
|
||||
// freeTrial: toProduct.free_trial || null,
|
||||
// features,
|
||||
// optionsList: curCusProduct.options,
|
||||
// entities,
|
||||
// cusProducts,
|
||||
// fromMigration: true,
|
||||
// };
|
||||
|
||||
let newPrepaid = curCusProduct.options.find(
|
||||
(option: FeatureOptions) =>
|
||||
option.internal_feature_id === config.internal_feature_id,
|
||||
);
|
||||
// // Get prepaid prices
|
||||
// let prepaidPrices = toProduct.prices.filter(
|
||||
// (price: Price) =>
|
||||
// getBillingType(price.config!) === BillingType.UsageInAdvance,
|
||||
// );
|
||||
|
||||
if (!newPrepaid) {
|
||||
curCusProduct.options.push({
|
||||
feature_id: config.feature_id,
|
||||
internal_feature_id: config.internal_feature_id,
|
||||
quantity: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
// for (const prepaidPrice of prepaidPrices) {
|
||||
// let config = prepaidPrice.config as UsagePriceConfig;
|
||||
|
||||
await handleUpgrade({
|
||||
req: {
|
||||
db,
|
||||
orgId,
|
||||
env,
|
||||
logtail: logger,
|
||||
},
|
||||
res: null,
|
||||
attachParams,
|
||||
curCusProduct,
|
||||
curFullProduct: fromProduct,
|
||||
fromReq: false,
|
||||
carryExistingUsages: true,
|
||||
prorationBehavior: ProrationBehavior.None,
|
||||
newVersion: true,
|
||||
});
|
||||
// let newPrepaid = curCusProduct.options.find(
|
||||
// (option: FeatureOptions) =>
|
||||
// option.internal_feature_id === config.internal_feature_id,
|
||||
// );
|
||||
|
||||
// if (!newPrepaid) {
|
||||
// curCusProduct.options.push({
|
||||
// feature_id: config.feature_id,
|
||||
// internal_feature_id: config.internal_feature_id,
|
||||
// quantity: 0,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
|
||||
// await handleUpgrade({
|
||||
// req: {
|
||||
// db,
|
||||
// orgId,
|
||||
// env,
|
||||
// logtail: logger,
|
||||
// },
|
||||
// res: null,
|
||||
// attachParams,
|
||||
// curCusProduct,
|
||||
// curFullProduct: fromProduct,
|
||||
// fromReq: false,
|
||||
// carryExistingUsages: true,
|
||||
// prorationBehavior: ProrationBehavior.None,
|
||||
// newVersion: true,
|
||||
// });
|
||||
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
@@ -131,25 +165,40 @@ export const migrateCustomer = async ({
|
||||
`Migration failed for customer ${customer.id}, job id: ${migrationJob.id}`,
|
||||
);
|
||||
logger.error(error);
|
||||
if (error instanceof RecaseError) {
|
||||
logger.error(`Recase error: ${error.message} (${error.code})`);
|
||||
} else if (error.type === "StripeError") {
|
||||
logger.error(`Stripe error: ${error.message} (${error.code})`);
|
||||
} else {
|
||||
logger.error("Unknown error:", error);
|
||||
}
|
||||
// logger.error(
|
||||
// `Migration failed for customer ${customer.id}, job id: ${migrationJob.id}`,
|
||||
// );
|
||||
// logger.error(error);
|
||||
// if (error instanceof RecaseError) {
|
||||
// logger.error(`Recase error: ${error.message} (${error.code})`);
|
||||
// } else if (error.type === "StripeError") {
|
||||
// logger.error(`Stripe error: ${error.message} (${error.code})`);
|
||||
// } else {
|
||||
// logger.error("Unknown error:", error);
|
||||
// }
|
||||
|
||||
await MigrationService.insertError({
|
||||
db,
|
||||
data: constructMigrationError({
|
||||
migrationJobId: migrationJob.id,
|
||||
internalCustomerId: customer.internal_id,
|
||||
data: error.data || error,
|
||||
code: error.code || "unknown",
|
||||
message: error.message || "unknown",
|
||||
}),
|
||||
});
|
||||
// await MigrationService.insertError({
|
||||
// db,
|
||||
// data: constructMigrationError({
|
||||
// migrationJobId: migrationJob.id,
|
||||
// internalCustomerId: customer.internal_id,
|
||||
// data: error.data || error,
|
||||
// code: error.code || "unknown",
|
||||
// message: error.message || "unknown",
|
||||
// }),
|
||||
// });
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// let curCusProduct = cusProducts.find(
|
||||
// (cp: FullCusProduct) => cp.product.internal_id == fromProduct.internal_id,
|
||||
// );
|
||||
|
||||
// if (!curCusProduct) {
|
||||
// logger.error(
|
||||
// `Customer ${customer.id} does not have a ${fromProduct.internal_id} cus product, skipping migration`,
|
||||
// );
|
||||
// return false;
|
||||
// }
|
||||
|
||||
@@ -7,14 +7,9 @@ import {
|
||||
MigrationJob,
|
||||
MigrationJobStep,
|
||||
} from "@autumn/shared";
|
||||
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import { MigrationService } from "../MigrationService.js";
|
||||
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
|
||||
import { createStripeCli } from "@/external/stripe/utils.js";
|
||||
|
||||
import { migrateCustomer } from "./migrateCustomer.js";
|
||||
import { sendMigrationEmail } from "./sendMigrationEmail.js";
|
||||
import { createStripePriceIFNotExist } from "@/external/stripe/createStripePrice/createStripePrice.js";
|
||||
@@ -53,8 +48,6 @@ export const migrateCustomers = async ({
|
||||
orgId,
|
||||
});
|
||||
|
||||
org.config.bill_upgrade_immediately = false;
|
||||
|
||||
// Create stripe prices if they don't exist
|
||||
let stripeCli = createStripeCli({ org, env });
|
||||
let batchCreate = [];
|
||||
@@ -175,9 +168,9 @@ export const migrateCustomers = async ({
|
||||
},
|
||||
});
|
||||
|
||||
await sendMigrationEmail({
|
||||
db,
|
||||
migrationJobId: migrationJob.id,
|
||||
org,
|
||||
});
|
||||
// await sendMigrationEmail({
|
||||
// db,
|
||||
// migrationJobId: migrationJob.id,
|
||||
// org,
|
||||
// });
|
||||
};
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { getStripeCusData } from "@/internal/customers/attach/attachUtils/attachParams/attachParamsUtils/getStripeCusData.js";
|
||||
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import {
|
||||
APIVersion,
|
||||
FullCusProduct,
|
||||
FullCustomer,
|
||||
FullProduct,
|
||||
} from "@autumn/shared";
|
||||
import Stripe from "stripe";
|
||||
|
||||
export const migrationToAttachParams = async ({
|
||||
req,
|
||||
stripeCli,
|
||||
customer,
|
||||
cusProduct,
|
||||
newProduct,
|
||||
}: {
|
||||
req: ExtendedRequest;
|
||||
stripeCli: Stripe;
|
||||
customer: FullCustomer;
|
||||
cusProduct: FullCusProduct;
|
||||
newProduct: FullProduct;
|
||||
}): Promise<AttachParams> => {
|
||||
const { org } = req;
|
||||
|
||||
const apiVersion = org.config.api_version || APIVersion.v1;
|
||||
const internalEntityId = cusProduct.internal_entity_id || undefined;
|
||||
// const entityId = customer.entities.find(
|
||||
// (e) => e.internal_id == internalEntityId,
|
||||
// )?.id;
|
||||
|
||||
const { stripeCus, paymentMethod, now } = await getStripeCusData({
|
||||
stripeCli,
|
||||
stripeId: customer.processor?.id,
|
||||
});
|
||||
|
||||
const attachParams: AttachParams = {
|
||||
stripeCli,
|
||||
stripeCus,
|
||||
now,
|
||||
paymentMethod,
|
||||
|
||||
customer,
|
||||
products: [newProduct],
|
||||
optionsList: cusProduct.options,
|
||||
prices: newProduct.prices,
|
||||
entitlements: newProduct.entitlements,
|
||||
freeTrial: newProduct.free_trial || null,
|
||||
replaceables: [],
|
||||
|
||||
req,
|
||||
org: req.org,
|
||||
entities: customer.entities,
|
||||
features: req.features,
|
||||
internalEntityId,
|
||||
cusProducts: customer.customer_products,
|
||||
|
||||
// Others
|
||||
apiVersion,
|
||||
// successUrl: attachBody.success_url,
|
||||
// invoiceOnly: attachBody.invoice_only,
|
||||
// billingAnchor: attachBody.billing_cycle_anchor,
|
||||
// metadata: attachBody.metadata,
|
||||
// disableFreeTrial: attachBody.free_trial === false || false,
|
||||
// checkoutSessionParams: attachBody.checkout_session_params,
|
||||
// isCustom: attachBody.is_custom,
|
||||
};
|
||||
|
||||
return attachParams;
|
||||
};
|
||||
@@ -0,0 +1,84 @@
|
||||
import { handleAddProduct } from "@/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.js";
|
||||
import { handleUpgradeDiffInterval } from "@/internal/customers/attach/attachFunctions/upgradeDiffIntFlow/handleUpgradeDiffInt.js";
|
||||
import { handleUpgradeSameInterval } from "@/internal/customers/attach/attachFunctions/upgradeSameIntFlow/handleUpgradeSameInt.js";
|
||||
import { intervalsAreSame } from "@/internal/customers/attach/attachUtils/getAttachConfig.js";
|
||||
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
||||
import { isFreeProduct } from "@/internal/products/productUtils.js";
|
||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||
import {
|
||||
AttachBranch,
|
||||
AttachConfig,
|
||||
AttachFunction,
|
||||
ProrationBehavior,
|
||||
} from "@autumn/shared";
|
||||
|
||||
const getAttachFunction = async ({
|
||||
attachParams,
|
||||
}: {
|
||||
attachParams: AttachParams;
|
||||
}) => {
|
||||
if (isFreeProduct(attachParams.prices)) {
|
||||
return AttachFunction.AddProduct;
|
||||
}
|
||||
|
||||
const sameIntervals = intervalsAreSame({ attachParams });
|
||||
|
||||
if (sameIntervals) {
|
||||
return AttachFunction.UpgradeSameInterval;
|
||||
}
|
||||
|
||||
return AttachFunction.UpgradeDiffInterval;
|
||||
};
|
||||
|
||||
export const runMigrationAttach = async ({
|
||||
req,
|
||||
attachParams,
|
||||
}: {
|
||||
req: ExtendedRequest;
|
||||
attachParams: AttachParams;
|
||||
}) => {
|
||||
const { logtail: logger } = req;
|
||||
const sameIntervals = intervalsAreSame({ attachParams });
|
||||
const branch = AttachBranch.NewVersion;
|
||||
|
||||
// Set config
|
||||
let config: AttachConfig = {
|
||||
onlyCheckout: false,
|
||||
carryUsage: true,
|
||||
branch,
|
||||
proration: ProrationBehavior.None,
|
||||
disableTrial: true,
|
||||
invoiceOnly: false,
|
||||
disableMerge: false,
|
||||
sameIntervals,
|
||||
carryTrial: true,
|
||||
};
|
||||
|
||||
let attachFunction = await getAttachFunction({ attachParams });
|
||||
|
||||
let customer = attachParams.customer;
|
||||
logger.info(`--------------------------------`);
|
||||
logger.info(
|
||||
`Running migration for ${customer.id}, function: ${attachFunction}`,
|
||||
);
|
||||
|
||||
if (attachFunction == AttachFunction.AddProduct) {
|
||||
return await handleAddProduct({
|
||||
req,
|
||||
attachParams,
|
||||
config,
|
||||
});
|
||||
} else if (attachFunction == AttachFunction.UpgradeSameInterval) {
|
||||
return await handleUpgradeSameInterval({
|
||||
req,
|
||||
attachParams,
|
||||
config,
|
||||
});
|
||||
} else if (attachFunction == AttachFunction.UpgradeDiffInterval) {
|
||||
return await handleUpgradeDiffInterval({
|
||||
req,
|
||||
attachParams,
|
||||
config,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,3 +1,9 @@
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
|
||||
import { nullish } from "@/utils/genUtils.js";
|
||||
import { numberWithCommas } from "tests/utils/general/numberUtils.js";
|
||||
import { getFeatureName } from "@/internal/features/utils/displayUtils.js";
|
||||
|
||||
import {
|
||||
ProductV2,
|
||||
Feature,
|
||||
@@ -7,15 +13,9 @@ import {
|
||||
ErrCode,
|
||||
Infinite,
|
||||
FullCusProduct,
|
||||
CusProductStatus,
|
||||
} from "@autumn/shared";
|
||||
import { features } from "process";
|
||||
import { isPriceItem } from "../product-items/productItemUtils/getItemType.js";
|
||||
import { isFeaturePriceItem } from "../product-items/productItemUtils/getItemType.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { nullish } from "@/utils/genUtils.js";
|
||||
import { numberWithCommas } from "tests/utils/general/numberUtils.js";
|
||||
import { getFeatureName } from "@/internal/features/utils/displayUtils.js";
|
||||
|
||||
export const sortProductItems = (items: ProductItem[], features: Feature[]) => {
|
||||
items.sort((a, b) => {
|
||||
@@ -131,7 +131,7 @@ export const getPricecnPrice = ({
|
||||
if (isPriceItem(priceItem)) {
|
||||
return {
|
||||
primaryText: getPriceText({ item: priceItem, org }),
|
||||
secondaryText: `per ${priceItem.interval}`,
|
||||
secondaryText: priceItem.interval ? `per ${priceItem.interval}` : " ",
|
||||
};
|
||||
} else {
|
||||
let feature = features.find((f) => f.id == priceItem.feature_id);
|
||||
|
||||
@@ -80,23 +80,24 @@ export const priceToInvoiceAmount = ({
|
||||
// 1. If fixed price, just return amount
|
||||
|
||||
let amount = 0;
|
||||
|
||||
if (isFixedPrice({ price })) {
|
||||
amount = (price.config as FixedPriceConfig).amount;
|
||||
}
|
||||
|
||||
const config = price.config as UsagePriceConfig;
|
||||
let billingType = getBillingType(config);
|
||||
|
||||
if (!nullish(quantity) && !nullish(overage)) {
|
||||
throw new Error(
|
||||
`getAmountForPrice: quantity or overage is required, autumn price: ${price.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (billingType == BillingType.UsageInAdvance) {
|
||||
amount = getAmountForQuantity({ price, quantity: quantity! });
|
||||
} else {
|
||||
amount = getAmountForQuantity({ price, quantity: overage! });
|
||||
const config = price.config as UsagePriceConfig;
|
||||
let billingType = getBillingType(config);
|
||||
|
||||
if (!nullish(quantity) && !nullish(overage)) {
|
||||
throw new Error(
|
||||
`getAmountForPrice: quantity or overage is required, autumn price: ${price.id}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (billingType == BillingType.UsageInAdvance) {
|
||||
amount = getAmountForQuantity({ price, quantity: quantity! });
|
||||
} else {
|
||||
amount = getAmountForQuantity({ price, quantity: overage! });
|
||||
}
|
||||
}
|
||||
|
||||
if (proration) {
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
|
||||
import { Router } from "express";
|
||||
|
||||
export const testRouter = Router();
|
||||
@@ -36,6 +36,17 @@ const initWorker = ({
|
||||
let worker = new Worker(
|
||||
"autumn",
|
||||
async (job: Job) => {
|
||||
try {
|
||||
logtail.use((log: any) => {
|
||||
return {
|
||||
...log,
|
||||
task: job.name,
|
||||
data: job.data,
|
||||
workerId: id,
|
||||
};
|
||||
});
|
||||
} catch (error) {}
|
||||
|
||||
if (job.name == JobName.GenerateFeatureDisplay) {
|
||||
await runSaveFeatureDisplayTask({
|
||||
db,
|
||||
|
||||
@@ -149,7 +149,7 @@ export const createUpgradeProrationInvoice = async ({
|
||||
stripeSubId: sub.id,
|
||||
});
|
||||
|
||||
const { invoice: paidInvoice } = await payForInvoice({
|
||||
const { invoice: paidInvoice, error } = await payForInvoice({
|
||||
stripeCli,
|
||||
paymentMethod,
|
||||
invoiceId: finalInvoice.id,
|
||||
|
||||
@@ -279,10 +279,12 @@ export const runUpdateUsageTask = async ({
|
||||
payload,
|
||||
logger,
|
||||
db,
|
||||
throwError = false,
|
||||
}: {
|
||||
payload: any;
|
||||
logger: any;
|
||||
db: DrizzleCli;
|
||||
throwError?: boolean;
|
||||
}) => {
|
||||
try {
|
||||
// 1. Update customer balance
|
||||
@@ -321,19 +323,11 @@ export const runUpdateUsageTask = async ({
|
||||
}
|
||||
console.log(" ✅ Customer balance updated");
|
||||
} catch (error) {
|
||||
if (logger) {
|
||||
logger.use((log: any) => {
|
||||
return {
|
||||
...log,
|
||||
task: JobName.UpdateUsage,
|
||||
data: payload,
|
||||
};
|
||||
});
|
||||
logger.error(`ERROR UPDATING USAGE`);
|
||||
logger.error(error);
|
||||
|
||||
logger.error(`ERROR UPDATING USAGE`);
|
||||
logger.error(error);
|
||||
} else {
|
||||
console.log(error);
|
||||
if (throwError) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,15 +4,17 @@ export const formatAmount = ({
|
||||
org,
|
||||
amount,
|
||||
maxFractionDigits = 2,
|
||||
minFractionDigits = 0,
|
||||
}: {
|
||||
org?: Organization;
|
||||
amount: number;
|
||||
maxFractionDigits?: number;
|
||||
minFractionDigits?: number;
|
||||
}) => {
|
||||
return new Intl.NumberFormat(undefined, {
|
||||
style: "currency",
|
||||
currency: org?.default_currency || "USD",
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
minimumFractionDigits: minFractionDigits || 0,
|
||||
maximumFractionDigits: maxFractionDigits || 2,
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
@@ -10,16 +10,19 @@ import {
|
||||
export const constructFeatureItem = ({
|
||||
featureId,
|
||||
includedUsage = 150,
|
||||
interval = ProductItemInterval.Month,
|
||||
entityFeatureId,
|
||||
}: {
|
||||
featureId: string;
|
||||
includedUsage?: number;
|
||||
interval?: ProductItemInterval;
|
||||
entityFeatureId?: string;
|
||||
}) => {
|
||||
let item: ProductItem = {
|
||||
feature_id: featureId,
|
||||
included_usage: includedUsage,
|
||||
entity_feature_id: entityFeatureId,
|
||||
interval: interval,
|
||||
};
|
||||
|
||||
return item;
|
||||
@@ -29,11 +32,13 @@ export const constructPrepaidItem = ({
|
||||
featureId,
|
||||
price,
|
||||
billingUnits = 100,
|
||||
includedUsage = 0,
|
||||
isOneOff = false,
|
||||
}: {
|
||||
featureId: string;
|
||||
price: number;
|
||||
billingUnits?: number;
|
||||
includedUsage?: number;
|
||||
isOneOff?: boolean;
|
||||
}) => {
|
||||
let item: ProductItem = {
|
||||
@@ -44,6 +49,7 @@ export const constructPrepaidItem = ({
|
||||
billing_units: billingUnits || 100,
|
||||
|
||||
interval: isOneOff ? null : ProductItemInterval.Month,
|
||||
included_usage: includedUsage,
|
||||
};
|
||||
|
||||
return item;
|
||||
@@ -54,11 +60,18 @@ export const constructArrearItem = ({
|
||||
includedUsage = 10000,
|
||||
price = 0.1,
|
||||
billingUnits = 1000,
|
||||
config = {
|
||||
// on_increase: OnIncrease.BillImmediately,
|
||||
// on_decrease: OnDecrease.None,
|
||||
on_increase: OnIncrease.ProrateImmediately,
|
||||
on_decrease: OnDecrease.ProrateImmediately,
|
||||
},
|
||||
}: {
|
||||
featureId: string;
|
||||
includedUsage?: number;
|
||||
price?: number;
|
||||
billingUnits?: number;
|
||||
config?: ProductItemConfig;
|
||||
}) => {
|
||||
let item: ProductItem = {
|
||||
feature_id: featureId,
|
||||
@@ -68,6 +81,7 @@ export const constructArrearItem = ({
|
||||
billing_units: billingUnits,
|
||||
interval: ProductItemInterval.Month,
|
||||
reset_usage_when_enabled: true,
|
||||
config,
|
||||
};
|
||||
|
||||
return item;
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
|
||||
import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
|
||||
import { cusProductToPrices } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
|
||||
import { CusService } from "@/internal/customers/CusService.js";
|
||||
import { findContUsePrice } from "@/internal/products/prices/priceUtils/findPriceUtils.js";
|
||||
import { AppEnv, Organization } from "@autumn/shared";
|
||||
import { expect } from "chai";
|
||||
import Stripe from "stripe";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
|
||||
export const expectSubQuantityCorrect = async ({
|
||||
stripeCli,
|
||||
productId,
|
||||
usage,
|
||||
itemQuantity,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
customerId,
|
||||
numReplaceables = 0,
|
||||
}: {
|
||||
stripeCli: Stripe;
|
||||
productId: string;
|
||||
usage: number;
|
||||
itemQuantity: number;
|
||||
db: DrizzleCli;
|
||||
org: Organization;
|
||||
env: AppEnv;
|
||||
customerId: string;
|
||||
numReplaceables?: number;
|
||||
}) => {
|
||||
const fullCus = await CusService.getFull({
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
idOrInternalId: customerId,
|
||||
});
|
||||
|
||||
let cusProduct = fullCus.customer_products.find(
|
||||
(cp) => cp.product_id === productId,
|
||||
);
|
||||
|
||||
let stripeSubs = await getStripeSubs({
|
||||
stripeCli,
|
||||
subIds: cusProduct?.subscription_ids,
|
||||
});
|
||||
|
||||
let subItems = stripeSubs.flatMap((sub) => sub.items.data);
|
||||
let prices = cusProductToPrices({ cusProduct: cusProduct! });
|
||||
|
||||
let contPrice = findContUsePrice({ prices });
|
||||
|
||||
let subItem = findStripeItemForPrice({
|
||||
price: contPrice!,
|
||||
stripeItems: subItems,
|
||||
});
|
||||
|
||||
expect(subItem).to.exist;
|
||||
expect(subItem!.quantity).to.equal(itemQuantity);
|
||||
|
||||
// Check num replaceables correct
|
||||
let cusEnts = cusProduct?.customer_entitlements;
|
||||
let cusEnt = cusEnts?.find((ent) => ent.feature_id === TestFeature.Users);
|
||||
|
||||
expect(cusEnt).to.exist;
|
||||
expect(cusEnt?.replaceables.length).to.equal(numReplaceables);
|
||||
|
||||
let expectedBalance = cusEnt!.entitlement.allowance! - usage;
|
||||
expect(cusEnt!.balance).to.equal(expectedBalance);
|
||||
|
||||
return {
|
||||
stripeSubs,
|
||||
cusProduct,
|
||||
};
|
||||
};
|
||||
183
server/tests/attach/migrations/migration1.ts
Normal file
183
server/tests/attach/migrations/migration1.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { expect } from "chai";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||
import { AppEnv, Organization, ProductV2 } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import Stripe from "stripe";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { setupBefore } from "tests/before.js";
|
||||
import { createProducts } from "tests/utils/productUtils.js";
|
||||
import { addPrefixToProducts, runAttachTest } from "../utils.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { replaceItems } from "../utils.js";
|
||||
import { timeout } from "@/utils/genUtils.js";
|
||||
import { advanceTestClock } from "tests/utils/stripeUtils.js";
|
||||
import { addWeeks } from "date-fns";
|
||||
import { defaultApiVersion } from "tests/constants.js";
|
||||
import { runMigrationTest } from "./runMigrationTest.js";
|
||||
|
||||
let messagesItem = constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 500,
|
||||
});
|
||||
|
||||
let wordsItem = constructFeatureItem({
|
||||
featureId: TestFeature.Words,
|
||||
includedUsage: 100,
|
||||
});
|
||||
|
||||
export let free = constructProduct({
|
||||
items: [messagesItem, wordsItem],
|
||||
type: "free",
|
||||
isDefault: false,
|
||||
});
|
||||
|
||||
const testCase = "migrations1";
|
||||
|
||||
describe(`${chalk.yellowBright(`${testCase}: Testing migration for free product`)}`, () => {
|
||||
let customerId = testCase;
|
||||
let autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
|
||||
let testClockId: string;
|
||||
let db: DrizzleCli, org: Organization, env: AppEnv;
|
||||
let stripeCli: Stripe;
|
||||
|
||||
let curUnix = new Date().getTime();
|
||||
|
||||
before(async function () {
|
||||
await setupBefore(this);
|
||||
const { autumnJs } = this;
|
||||
db = this.db;
|
||||
org = this.org;
|
||||
env = this.env;
|
||||
|
||||
stripeCli = this.stripeCli;
|
||||
|
||||
addPrefixToProducts({
|
||||
products: [free],
|
||||
prefix: testCase,
|
||||
});
|
||||
|
||||
await createProducts({
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
autumn,
|
||||
products: [free],
|
||||
customerId,
|
||||
});
|
||||
|
||||
const { testClockId: testClockId1 } = await initCustomer({
|
||||
autumn: autumnJs,
|
||||
customerId,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
attachPm: "success",
|
||||
});
|
||||
|
||||
testClockId = testClockId1!;
|
||||
});
|
||||
|
||||
it("should attach free product", async function () {
|
||||
await runAttachTest({
|
||||
autumn,
|
||||
customerId,
|
||||
product: free,
|
||||
stripeCli,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
});
|
||||
});
|
||||
|
||||
let newFree: ProductV2;
|
||||
let increaseMessagesBy = 100;
|
||||
let reduceWordsBy = 50;
|
||||
it("should update product to new version", async function () {
|
||||
newFree = structuredClone(free);
|
||||
|
||||
let newItems = replaceItems({
|
||||
items: free.items,
|
||||
featureId: TestFeature.Messages,
|
||||
newItem: constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage:
|
||||
(messagesItem.included_usage as number) + increaseMessagesBy,
|
||||
}),
|
||||
});
|
||||
|
||||
newItems = replaceItems({
|
||||
items: newItems,
|
||||
featureId: TestFeature.Words,
|
||||
newItem: constructFeatureItem({
|
||||
featureId: TestFeature.Words,
|
||||
includedUsage: (wordsItem.included_usage as number) - reduceWordsBy,
|
||||
}),
|
||||
});
|
||||
|
||||
newFree.items = newItems;
|
||||
|
||||
await autumn.products.update(free.id, {
|
||||
items: newItems,
|
||||
});
|
||||
});
|
||||
|
||||
it("should attach track usage and get correct balance", async function () {
|
||||
let wordsUsage = 25;
|
||||
let messagesUsage = 20;
|
||||
await autumn.track({
|
||||
customer_id: customerId,
|
||||
value: wordsUsage,
|
||||
feature_id: TestFeature.Words,
|
||||
});
|
||||
|
||||
await autumn.track({
|
||||
customer_id: customerId,
|
||||
value: messagesUsage,
|
||||
feature_id: TestFeature.Messages,
|
||||
});
|
||||
|
||||
await advanceTestClock({
|
||||
stripeCli,
|
||||
testClockId,
|
||||
advanceTo: addWeeks(Date.now(), 1).getTime(),
|
||||
});
|
||||
|
||||
let customer = await autumn.customers.get(customerId);
|
||||
|
||||
await autumn.migrate({
|
||||
from_product_id: free.id,
|
||||
to_product_id: newFree.id,
|
||||
from_version: 1,
|
||||
to_version: 2,
|
||||
});
|
||||
|
||||
await timeout(4000);
|
||||
|
||||
// 1. Get features
|
||||
customer = await autumn.customers.get(customerId);
|
||||
|
||||
await runMigrationTest({
|
||||
autumn,
|
||||
stripeCli,
|
||||
customerId,
|
||||
fromProduct: free,
|
||||
toProduct: newFree,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
usage: [
|
||||
{
|
||||
featureId: TestFeature.Words,
|
||||
value: wordsUsage,
|
||||
},
|
||||
{
|
||||
featureId: TestFeature.Messages,
|
||||
value: messagesUsage,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
163
server/tests/attach/migrations/migration2.ts
Normal file
163
server/tests/attach/migrations/migration2.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||
import {
|
||||
AppEnv,
|
||||
BillingInterval,
|
||||
Organization,
|
||||
ProductItemInterval,
|
||||
ProductV2,
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import Stripe from "stripe";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { setupBefore } from "tests/before.js";
|
||||
import { createProducts } from "tests/utils/productUtils.js";
|
||||
import { addPrefixToProducts, runAttachTest } from "../utils.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { replaceItems } from "../utils.js";
|
||||
import { timeout } from "@/utils/genUtils.js";
|
||||
import { advanceTestClock } from "tests/utils/stripeUtils.js";
|
||||
import { addWeeks } from "date-fns";
|
||||
import { defaultApiVersion } from "tests/constants.js";
|
||||
import { runMigrationTest } from "./runMigrationTest.js";
|
||||
|
||||
let wordsItem = constructArrearItem({
|
||||
featureId: TestFeature.Words,
|
||||
});
|
||||
|
||||
export let pro = constructProduct({
|
||||
items: [wordsItem],
|
||||
type: "pro",
|
||||
isDefault: false,
|
||||
});
|
||||
|
||||
const testCase = "migrations2";
|
||||
|
||||
describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro usage product`)}`, () => {
|
||||
let customerId = testCase;
|
||||
let autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
|
||||
let testClockId: string;
|
||||
let db: DrizzleCli, org: Organization, env: AppEnv;
|
||||
let stripeCli: Stripe;
|
||||
|
||||
let curUnix = new Date().getTime();
|
||||
|
||||
before(async function () {
|
||||
await setupBefore(this);
|
||||
const { autumnJs } = this;
|
||||
db = this.db;
|
||||
org = this.org;
|
||||
env = this.env;
|
||||
|
||||
stripeCli = this.stripeCli;
|
||||
|
||||
addPrefixToProducts({
|
||||
products: [pro],
|
||||
prefix: testCase,
|
||||
});
|
||||
|
||||
await createProducts({
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
autumn,
|
||||
products: [pro],
|
||||
customerId,
|
||||
});
|
||||
|
||||
const { testClockId: testClockId1 } = await initCustomer({
|
||||
autumn: autumnJs,
|
||||
customerId,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
attachPm: "success",
|
||||
});
|
||||
|
||||
testClockId = testClockId1!;
|
||||
});
|
||||
|
||||
it("should attach free product", async function () {
|
||||
await runAttachTest({
|
||||
autumn,
|
||||
customerId,
|
||||
product: pro,
|
||||
stripeCli,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
});
|
||||
});
|
||||
|
||||
let newPro: ProductV2;
|
||||
let increaseWordsBy = 1500;
|
||||
it("should update product to new version", async function () {
|
||||
newPro = structuredClone(pro);
|
||||
|
||||
let newItems = replaceItems({
|
||||
items: pro.items,
|
||||
featureId: TestFeature.Words,
|
||||
newItem: constructArrearItem({
|
||||
featureId: TestFeature.Words,
|
||||
includedUsage: (wordsItem.included_usage as number) + increaseWordsBy,
|
||||
}),
|
||||
});
|
||||
|
||||
newItems = replaceItems({
|
||||
items: newItems,
|
||||
interval: BillingInterval.Month,
|
||||
newItem: {
|
||||
price: 50,
|
||||
interval: ProductItemInterval.Month,
|
||||
},
|
||||
});
|
||||
|
||||
newPro.items = newItems;
|
||||
await autumn.products.update(pro.id, {
|
||||
items: newItems,
|
||||
});
|
||||
});
|
||||
|
||||
it("should attach track usage and get correct balance", async function () {
|
||||
let wordsUsage = 120000;
|
||||
await autumn.track({
|
||||
customer_id: customerId,
|
||||
value: wordsUsage,
|
||||
feature_id: TestFeature.Words,
|
||||
});
|
||||
|
||||
await advanceTestClock({
|
||||
stripeCli,
|
||||
testClockId,
|
||||
advanceTo: addWeeks(Date.now(), 1).getTime(),
|
||||
});
|
||||
|
||||
await autumn.migrate({
|
||||
from_product_id: pro.id,
|
||||
to_product_id: newPro.id,
|
||||
from_version: 1,
|
||||
to_version: 2,
|
||||
});
|
||||
|
||||
await timeout(4000);
|
||||
|
||||
await runMigrationTest({
|
||||
autumn,
|
||||
stripeCli,
|
||||
customerId,
|
||||
fromProduct: pro,
|
||||
toProduct: newPro,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
usage: [
|
||||
{
|
||||
featureId: TestFeature.Words,
|
||||
value: wordsUsage,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
159
server/tests/attach/migrations/migration3.ts
Normal file
159
server/tests/attach/migrations/migration3.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||
import {
|
||||
AppEnv,
|
||||
BillingInterval,
|
||||
Organization,
|
||||
ProductItemInterval,
|
||||
ProductV2,
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import Stripe from "stripe";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { setupBefore } from "tests/before.js";
|
||||
import { createProducts } from "tests/utils/productUtils.js";
|
||||
import { addPrefixToProducts, runAttachTest } from "../utils.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { replaceItems } from "../utils.js";
|
||||
|
||||
import { defaultApiVersion } from "tests/constants.js";
|
||||
import { runMigrationTest } from "./runMigrationTest.js";
|
||||
import { timeout } from "@/utils/genUtils.js";
|
||||
import { advanceTestClock } from "tests/utils/stripeUtils.js";
|
||||
import { addDays } from "date-fns";
|
||||
|
||||
let wordsItem = constructArrearItem({
|
||||
featureId: TestFeature.Words,
|
||||
});
|
||||
|
||||
export let pro = constructProduct({
|
||||
items: [wordsItem],
|
||||
type: "pro",
|
||||
isDefault: false,
|
||||
trial: true,
|
||||
});
|
||||
|
||||
const testCase = "migrations3";
|
||||
|
||||
describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro with trial`)}`, () => {
|
||||
let customerId = testCase;
|
||||
let autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
|
||||
let testClockId: string;
|
||||
let db: DrizzleCli, org: Organization, env: AppEnv;
|
||||
let stripeCli: Stripe;
|
||||
|
||||
let curUnix = new Date().getTime();
|
||||
|
||||
before(async function () {
|
||||
await setupBefore(this);
|
||||
const { autumnJs } = this;
|
||||
db = this.db;
|
||||
org = this.org;
|
||||
env = this.env;
|
||||
|
||||
stripeCli = this.stripeCli;
|
||||
|
||||
addPrefixToProducts({
|
||||
products: [pro],
|
||||
prefix: testCase,
|
||||
});
|
||||
|
||||
await createProducts({
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
autumn,
|
||||
products: [pro],
|
||||
customerId,
|
||||
});
|
||||
|
||||
const { testClockId: testClockId1 } = await initCustomer({
|
||||
autumn: autumnJs,
|
||||
customerId,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
attachPm: "success",
|
||||
});
|
||||
|
||||
testClockId = testClockId1!;
|
||||
});
|
||||
|
||||
it("should attach free product", async function () {
|
||||
await runAttachTest({
|
||||
autumn,
|
||||
customerId,
|
||||
product: pro,
|
||||
stripeCli,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
});
|
||||
});
|
||||
|
||||
let newPro: ProductV2;
|
||||
let increaseWordsBy = 1500;
|
||||
it("should update product to new version", async function () {
|
||||
newPro = structuredClone(pro);
|
||||
|
||||
let newItems = replaceItems({
|
||||
items: pro.items,
|
||||
featureId: TestFeature.Words,
|
||||
newItem: constructArrearItem({
|
||||
featureId: TestFeature.Words,
|
||||
includedUsage: (wordsItem.included_usage as number) + increaseWordsBy,
|
||||
}),
|
||||
});
|
||||
|
||||
newItems = replaceItems({
|
||||
items: newItems,
|
||||
interval: BillingInterval.Month,
|
||||
newItem: {
|
||||
price: 50,
|
||||
interval: ProductItemInterval.Month,
|
||||
},
|
||||
});
|
||||
|
||||
newPro.items = newItems;
|
||||
newPro.version = 2;
|
||||
await autumn.products.update(pro.id, {
|
||||
items: newItems,
|
||||
});
|
||||
});
|
||||
|
||||
it("should attach track usage and get correct balance", async function () {
|
||||
let wordsUsage = 120000;
|
||||
await autumn.track({
|
||||
customer_id: customerId,
|
||||
value: wordsUsage,
|
||||
feature_id: TestFeature.Words,
|
||||
});
|
||||
|
||||
await advanceTestClock({
|
||||
stripeCli,
|
||||
testClockId,
|
||||
advanceTo: addDays(Date.now(), 4).getTime(),
|
||||
});
|
||||
|
||||
await timeout(5000);
|
||||
|
||||
await runMigrationTest({
|
||||
autumn,
|
||||
stripeCli,
|
||||
customerId,
|
||||
fromProduct: pro,
|
||||
toProduct: newPro,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
usage: [
|
||||
{
|
||||
featureId: TestFeature.Words,
|
||||
value: wordsUsage,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
146
server/tests/attach/migrations/migration4.ts
Normal file
146
server/tests/attach/migrations/migration4.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||
import {
|
||||
AppEnv,
|
||||
BillingInterval,
|
||||
Organization,
|
||||
ProductItemInterval,
|
||||
ProductV2,
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import Stripe from "stripe";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { setupBefore } from "tests/before.js";
|
||||
import { createProducts } from "tests/utils/productUtils.js";
|
||||
import { addPrefixToProducts, runAttachTest } from "../utils.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { replaceItems } from "../utils.js";
|
||||
|
||||
import { defaultApiVersion } from "tests/constants.js";
|
||||
import { runMigrationTest } from "./runMigrationTest.js";
|
||||
import { timeout } from "@/utils/genUtils.js";
|
||||
import { advanceTestClock } from "tests/utils/stripeUtils.js";
|
||||
import { addDays } from "date-fns";
|
||||
|
||||
let wordsItem = constructArrearItem({
|
||||
featureId: TestFeature.Words,
|
||||
});
|
||||
|
||||
export let pro = constructProduct({
|
||||
items: [wordsItem],
|
||||
type: "pro",
|
||||
isDefault: false,
|
||||
});
|
||||
|
||||
let newWordsItem = constructArrearItem({
|
||||
featureId: TestFeature.Words,
|
||||
includedUsage: 120100,
|
||||
});
|
||||
|
||||
let proWithTrial = constructProduct({
|
||||
items: [newWordsItem],
|
||||
type: "pro",
|
||||
isDefault: false,
|
||||
trial: true,
|
||||
});
|
||||
|
||||
const testCase = "migrations4";
|
||||
|
||||
describe(`${chalk.yellowBright(`${testCase}: Testing migration for pro with trial`)}`, () => {
|
||||
let customerId = testCase;
|
||||
let autumn: AutumnInt = new AutumnInt({ version: defaultApiVersion });
|
||||
let testClockId: string;
|
||||
let db: DrizzleCli, org: Organization, env: AppEnv;
|
||||
let stripeCli: Stripe;
|
||||
|
||||
let curUnix = new Date().getTime();
|
||||
|
||||
before(async function () {
|
||||
await setupBefore(this);
|
||||
const { autumnJs } = this;
|
||||
db = this.db;
|
||||
org = this.org;
|
||||
env = this.env;
|
||||
|
||||
stripeCli = this.stripeCli;
|
||||
|
||||
addPrefixToProducts({
|
||||
products: [pro],
|
||||
prefix: testCase,
|
||||
});
|
||||
|
||||
await createProducts({
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
autumn,
|
||||
products: [pro],
|
||||
customerId,
|
||||
});
|
||||
|
||||
const { testClockId: testClockId1 } = await initCustomer({
|
||||
autumn: autumnJs,
|
||||
customerId,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
attachPm: "success",
|
||||
});
|
||||
|
||||
testClockId = testClockId1!;
|
||||
});
|
||||
|
||||
it("should attach free product", async function () {
|
||||
await runAttachTest({
|
||||
autumn,
|
||||
customerId,
|
||||
product: pro,
|
||||
stripeCli,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
});
|
||||
});
|
||||
|
||||
it("should update product to new version", async function () {
|
||||
await autumn.products.update(pro.id, {
|
||||
items: proWithTrial.items,
|
||||
});
|
||||
});
|
||||
|
||||
it("should attach track usage and get correct balance", async function () {
|
||||
let wordsUsage = 120000;
|
||||
await autumn.track({
|
||||
customer_id: customerId,
|
||||
value: wordsUsage,
|
||||
feature_id: TestFeature.Words,
|
||||
});
|
||||
|
||||
await timeout(4000);
|
||||
|
||||
// await advanceTestClock({
|
||||
// stripeCli,
|
||||
// testClockId,
|
||||
// advanceTo: addDays(Date.now(), 4).getTime(),
|
||||
// });
|
||||
|
||||
await runMigrationTest({
|
||||
autumn,
|
||||
stripeCli,
|
||||
customerId,
|
||||
fromProduct: pro,
|
||||
toProduct: proWithTrial,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
usage: [
|
||||
{
|
||||
featureId: TestFeature.Words,
|
||||
value: wordsUsage,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
115
server/tests/attach/migrations/runMigrationTest.ts
Normal file
115
server/tests/attach/migrations/runMigrationTest.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { ProductV2, Organization } from "@autumn/shared";
|
||||
import { AppEnv } from "autumn-js";
|
||||
import {
|
||||
expectSubItemsCorrect,
|
||||
getSubsFromCusId,
|
||||
} from "tests/utils/expectUtils/expectSubUtils.js";
|
||||
import Stripe from "stripe";
|
||||
import { expect } from "chai";
|
||||
import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js";
|
||||
import { expectResetAtCorrect } from "tests/utils/expectUtils/expectAttach/expectResetAtCorrect.js";
|
||||
import { isFreeProductV2 } from "@/internal/products/productUtils/classifyProduct.js";
|
||||
import { expectTrialEndsAtCorrect } from "tests/utils/expectUtils/expectAttach/expectTrialEndsAt.js";
|
||||
import { timeout } from "@/utils/genUtils.js";
|
||||
|
||||
export const expectSubsSame = ({
|
||||
subsBefore,
|
||||
subsAfter,
|
||||
}: {
|
||||
subsBefore: Stripe.Subscription[];
|
||||
subsAfter: Stripe.Subscription[];
|
||||
}) => {
|
||||
let invoicesBefore = subsBefore.map((sub) => sub.latest_invoice);
|
||||
let invoicesAfter = subsAfter.map((sub) => sub.latest_invoice);
|
||||
let subIdsBefore = subsBefore.map((sub) => sub.id);
|
||||
let subIdsAfter = subsAfter.map((sub) => sub.id);
|
||||
let periodEndsBefore = subsBefore.map((sub) => sub.current_period_end);
|
||||
let periodEndsAfter = subsAfter.map((sub) => sub.current_period_end);
|
||||
|
||||
expect(invoicesAfter).to.deep.equal(invoicesBefore);
|
||||
expect(subIdsAfter).to.deep.equal(subIdsBefore);
|
||||
expect(periodEndsAfter).to.deep.equal(periodEndsBefore);
|
||||
};
|
||||
|
||||
export const runMigrationTest = async ({
|
||||
autumn,
|
||||
stripeCli,
|
||||
customerId,
|
||||
fromProduct,
|
||||
toProduct,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
usage,
|
||||
numInvoices = 1,
|
||||
}: {
|
||||
autumn: AutumnInt;
|
||||
stripeCli: Stripe;
|
||||
customerId: string;
|
||||
fromProduct: ProductV2;
|
||||
toProduct: ProductV2;
|
||||
db: DrizzleCli;
|
||||
org: Organization;
|
||||
env: AppEnv;
|
||||
usage?: {
|
||||
featureId: string;
|
||||
value: number;
|
||||
}[];
|
||||
numInvoices?: number;
|
||||
}) => {
|
||||
const { subs: subsBefore } = await getSubsFromCusId({
|
||||
stripeCli,
|
||||
customerId,
|
||||
productId: fromProduct.id,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
});
|
||||
|
||||
const cusBefore = await autumn.customers.get(customerId);
|
||||
|
||||
await autumn.migrate({
|
||||
from_product_id: fromProduct.id,
|
||||
to_product_id: toProduct.id,
|
||||
from_version: fromProduct.version,
|
||||
to_version: toProduct.version,
|
||||
});
|
||||
|
||||
await timeout(5000);
|
||||
|
||||
const { subs: subsAfter } = await getSubsFromCusId({
|
||||
stripeCli,
|
||||
customerId,
|
||||
productId: toProduct.id,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
});
|
||||
|
||||
expectSubsSame({ subsBefore, subsAfter });
|
||||
|
||||
const cusAfter = await autumn.customers.get(customerId);
|
||||
expectFeaturesCorrect({
|
||||
customer: cusAfter,
|
||||
product: toProduct,
|
||||
usage,
|
||||
});
|
||||
|
||||
expectResetAtCorrect({ cusBefore, cusAfter });
|
||||
expectTrialEndsAtCorrect({ cusBefore, cusAfter });
|
||||
|
||||
await expectSubItemsCorrect({
|
||||
stripeCli,
|
||||
customerId,
|
||||
product: toProduct,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
});
|
||||
|
||||
if (!isFreeProductV2({ product: toProduct })) {
|
||||
expect(cusAfter.invoices.length).to.equal(numInvoices);
|
||||
}
|
||||
};
|
||||
@@ -17,17 +17,17 @@ import { addPrefixToProducts } from "../utils.js";
|
||||
|
||||
// UNCOMMENT FROM HERE
|
||||
let pro = constructProduct({
|
||||
id: "attach1_pro",
|
||||
id: "pro",
|
||||
items: [constructArrearItem({ featureId: TestFeature.Words })],
|
||||
type: "pro",
|
||||
});
|
||||
let premium = constructProduct({
|
||||
id: "attach1_premium",
|
||||
id: "premium",
|
||||
items: [constructArrearItem({ featureId: TestFeature.Words })],
|
||||
type: "premium",
|
||||
});
|
||||
let growth = constructProduct({
|
||||
id: "attach1_growth",
|
||||
id: "growth",
|
||||
items: [constructArrearItem({ featureId: TestFeature.Words })],
|
||||
type: "growth",
|
||||
});
|
||||
@@ -60,6 +60,9 @@ describe(`${chalk.yellowBright("attach/upgrade1: Testing usage upgrades")}`, ()
|
||||
await createProducts({
|
||||
autumn: autumnJs,
|
||||
products: [pro, premium, growth],
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
customerId,
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
AppEnv,
|
||||
AttachBranch,
|
||||
BillingInterval,
|
||||
Customer,
|
||||
FeatureOptions,
|
||||
Organization,
|
||||
ProductItem,
|
||||
@@ -29,6 +28,7 @@ import {
|
||||
import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js";
|
||||
import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js";
|
||||
import { Decimal } from "decimal.js";
|
||||
import { isFreeProductV2 } from "@/internal/products/productUtils/classifyProduct.js";
|
||||
|
||||
export const runAttachTest = async ({
|
||||
autumn,
|
||||
@@ -102,11 +102,14 @@ export const runAttachTest = async ({
|
||||
).filter(notNullish);
|
||||
const multiInterval = intervals.length > 1;
|
||||
|
||||
expectInvoicesCorrect({
|
||||
customer,
|
||||
first: multiInterval ? undefined : { productId: product.id, total },
|
||||
second: multiInterval ? { productId: product.id, total } : undefined,
|
||||
});
|
||||
const freeProduct = isFreeProductV2({ product });
|
||||
if (!freeProduct) {
|
||||
expectInvoicesCorrect({
|
||||
customer,
|
||||
first: multiInterval ? undefined : { productId: product.id, total },
|
||||
second: multiInterval ? { productId: product.id, total } : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
if (!skipFeatureCheck) {
|
||||
expectFeaturesCorrect({
|
||||
@@ -118,7 +121,7 @@ export const runAttachTest = async ({
|
||||
}
|
||||
|
||||
const branch = preview.branch;
|
||||
if (branch == AttachBranch.OneOff) {
|
||||
if (branch == AttachBranch.OneOff || freeProduct) {
|
||||
return;
|
||||
}
|
||||
await expectSubItemsCorrect({
|
||||
|
||||
3
server/tests/constants.ts
Normal file
3
server/tests/constants.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { APIVersion } from "@autumn/shared";
|
||||
|
||||
export const defaultApiVersion = APIVersion.v1_2;
|
||||
@@ -17,7 +17,7 @@ import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { expect } from "chai";
|
||||
import { expectSubQuantityCorrect } from "../../attach/entities/expectEntity.js";
|
||||
import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js";
|
||||
import { addWeeks } from "date-fns";
|
||||
import { timeout } from "@/utils/genUtils.js";
|
||||
import { advanceTestClock } from "tests/utils/stripeUtils.js";
|
||||
|
||||
196
server/tests/contUse/entities/entity2.ts
Normal file
196
server/tests/contUse/entities/entity2.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||
import {
|
||||
APIVersion,
|
||||
AppEnv,
|
||||
OnDecrease,
|
||||
OnIncrease,
|
||||
Organization,
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import Stripe from "stripe";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { setupBefore } from "tests/before.js";
|
||||
import { createProducts } from "tests/utils/productUtils.js";
|
||||
import { addPrefixToProducts, runAttachTest } from "../../attach/utils.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import {
|
||||
calcProrationAndExpectInvoice,
|
||||
expectSubQuantityCorrect,
|
||||
} from "tests/utils/expectUtils/expectContUseUtils.js";
|
||||
import { addWeeks } from "date-fns";
|
||||
import { advanceTestClock } from "tests/utils/stripeUtils.js";
|
||||
import { timeout } from "@/utils/genUtils.js";
|
||||
|
||||
let userItem = constructArrearProratedItem({
|
||||
featureId: TestFeature.Users,
|
||||
pricePerUnit: 50,
|
||||
includedUsage: 1,
|
||||
config: {
|
||||
on_increase: OnIncrease.ProrateImmediately,
|
||||
on_decrease: OnDecrease.ProrateImmediately,
|
||||
},
|
||||
});
|
||||
|
||||
export let pro = constructProduct({
|
||||
items: [userItem],
|
||||
type: "pro",
|
||||
});
|
||||
|
||||
const testCase = "entity2";
|
||||
|
||||
describe(`${chalk.yellowBright(`attach/entities/${testCase}: Testing entities, prorate now`)}`, () => {
|
||||
let customerId = testCase;
|
||||
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
|
||||
let testClockId: string;
|
||||
let db: DrizzleCli, org: Organization, env: AppEnv;
|
||||
let stripeCli: Stripe;
|
||||
let curUnix = new Date().getTime();
|
||||
|
||||
before(async function () {
|
||||
await setupBefore(this);
|
||||
const { autumnJs } = this;
|
||||
db = this.db;
|
||||
org = this.org;
|
||||
env = this.env;
|
||||
|
||||
stripeCli = this.stripeCli;
|
||||
|
||||
addPrefixToProducts({
|
||||
products: [pro],
|
||||
prefix: testCase,
|
||||
});
|
||||
|
||||
await createProducts({
|
||||
autumn,
|
||||
products: [pro],
|
||||
customerId,
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
});
|
||||
|
||||
const { testClockId: testClockId1 } = await initCustomer({
|
||||
autumn: autumnJs,
|
||||
customerId,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
attachPm: "success",
|
||||
});
|
||||
|
||||
testClockId = testClockId1!;
|
||||
});
|
||||
|
||||
let usage = 0;
|
||||
let firstEntities = [
|
||||
{
|
||||
id: "1",
|
||||
name: "test",
|
||||
featureId: TestFeature.Users,
|
||||
},
|
||||
];
|
||||
|
||||
it("should create entity, then attach pro", async function () {
|
||||
await autumn.entities.create(customerId, firstEntities);
|
||||
usage += 1;
|
||||
|
||||
await runAttachTest({
|
||||
autumn,
|
||||
customerId,
|
||||
product: pro,
|
||||
stripeCli,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
usage: [
|
||||
{
|
||||
featureId: TestFeature.Users,
|
||||
value: usage,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
const newEntities = [
|
||||
{
|
||||
id: "2",
|
||||
name: "test",
|
||||
featureId: TestFeature.Users,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "test2",
|
||||
featureId: TestFeature.Users,
|
||||
},
|
||||
];
|
||||
|
||||
it("should create 2 entities and have correct invoice", async function () {
|
||||
curUnix = await advanceTestClock({
|
||||
stripeCli,
|
||||
testClockId,
|
||||
advanceTo: addWeeks(new Date(), 2).getTime(),
|
||||
waitForSeconds: 10,
|
||||
});
|
||||
|
||||
await autumn.entities.create(customerId, newEntities);
|
||||
usage += newEntities.length;
|
||||
|
||||
const { stripeSubs } = await expectSubQuantityCorrect({
|
||||
stripeCli,
|
||||
productId: pro.id,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
customerId,
|
||||
usage,
|
||||
itemQuantity: usage,
|
||||
});
|
||||
|
||||
await calcProrationAndExpectInvoice({
|
||||
autumn,
|
||||
stripeSubs,
|
||||
customerId,
|
||||
quantity: newEntities.length,
|
||||
unitPrice: userItem.price!,
|
||||
curUnix,
|
||||
numInvoices: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it("should delete 1 entity and have correct invoice amount", async function () {
|
||||
curUnix = await advanceTestClock({
|
||||
stripeCli,
|
||||
testClockId,
|
||||
advanceTo: addWeeks(curUnix, 1).getTime(),
|
||||
waitForSeconds: 10,
|
||||
});
|
||||
|
||||
await timeout(5000);
|
||||
|
||||
await autumn.entities.delete(customerId, newEntities[0].id);
|
||||
usage -= 1;
|
||||
|
||||
const { stripeSubs } = await expectSubQuantityCorrect({
|
||||
stripeCli,
|
||||
productId: pro.id,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
customerId,
|
||||
usage,
|
||||
});
|
||||
|
||||
await calcProrationAndExpectInvoice({
|
||||
autumn,
|
||||
stripeSubs,
|
||||
customerId,
|
||||
quantity: -1,
|
||||
unitPrice: userItem.price!,
|
||||
curUnix,
|
||||
numInvoices: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,9 @@
|
||||
// Handling per entity features!
|
||||
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { expect } from "chai";
|
||||
import { timeout } from "@/utils/genUtils.js";
|
||||
import { useEntityBalanceAndExpect } from "tests/utils/expectUtils/expectContUse/expectEntityUtils.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||
import {
|
||||
@@ -20,13 +25,6 @@ import {
|
||||
constructArrearProratedItem,
|
||||
constructFeatureItem,
|
||||
} from "@/utils/scriptUtils/constructItem.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { expect } from "chai";
|
||||
import { expectSubQuantityCorrect } from "../../attach/entities/expectEntity.js";
|
||||
import { addHours, addMonths, addWeeks } from "date-fns";
|
||||
import { advanceTestClock } from "tests/utils/stripeUtils.js";
|
||||
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
|
||||
import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js";
|
||||
|
||||
let userItem = constructArrearProratedItem({
|
||||
featureId: TestFeature.Users,
|
||||
@@ -141,63 +139,101 @@ describe(`${chalk.yellowBright(`attach/entities/${testCase}: Testing per entity
|
||||
await autumn.entities.create(customerId, newEntities);
|
||||
usage += newEntities.length;
|
||||
|
||||
return;
|
||||
|
||||
let customer = await autumn.customers.get(customerId, {
|
||||
expand: [CusExpand.Entities],
|
||||
});
|
||||
|
||||
let balance = await autumn.check({
|
||||
let res = await autumn.check({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
});
|
||||
|
||||
expect(balance.quantity).to.equal(
|
||||
expect(res.balance).to.equal(
|
||||
(perEntityItem.included_usage as number) * usage,
|
||||
);
|
||||
|
||||
for (const entity of customer.entities) {
|
||||
let balance = await autumn.check({
|
||||
let entRes = await autumn.check({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
entity_id: entity.id,
|
||||
});
|
||||
|
||||
expect(balance.quantity).to.equal(perEntityItem.included_usage);
|
||||
expect(entRes.balance).to.equal(perEntityItem.included_usage);
|
||||
}
|
||||
});
|
||||
|
||||
return;
|
||||
// 1. Use from main balance...
|
||||
it("should use from top level balance", async function () {
|
||||
let deduction = 600;
|
||||
let perEntityIncluded = perEntityItem.included_usage as number;
|
||||
|
||||
it("should advance clock to next cycle and have correct invoice", async function () {
|
||||
await advanceTestClock({
|
||||
stripeCli,
|
||||
testClockId,
|
||||
advanceTo: addHours(
|
||||
addMonths(new Date(), 1),
|
||||
hoursToFinalizeInvoice,
|
||||
).getTime(),
|
||||
await autumn.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: deduction,
|
||||
});
|
||||
await timeout(5000);
|
||||
|
||||
let { balance } = await autumn.check({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
});
|
||||
|
||||
usage -= 2; // 2 entities deleted
|
||||
expect(balance).to.equal(perEntityIncluded * usage - deduction);
|
||||
});
|
||||
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
const invoices = customer.invoices;
|
||||
|
||||
let basePrice = getBasePrice({ product: pro });
|
||||
expect(invoices.length).to.equal(2);
|
||||
expect(invoices[0].total).to.equal(basePrice); // 0 entities
|
||||
|
||||
await expectSubQuantityCorrect({
|
||||
stripeCli,
|
||||
productId: pro.id,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
it("should use from entity balance", async function () {
|
||||
await useEntityBalanceAndExpect({
|
||||
autumn,
|
||||
customerId,
|
||||
usage,
|
||||
itemQuantity: usage,
|
||||
numReplaceables: 0,
|
||||
featureId: TestFeature.Messages,
|
||||
entityId: "2",
|
||||
});
|
||||
|
||||
await useEntityBalanceAndExpect({
|
||||
autumn,
|
||||
customerId,
|
||||
featureId: TestFeature.Messages,
|
||||
entityId: "3",
|
||||
});
|
||||
});
|
||||
|
||||
// Delete one entity and create a new one and master balance should be same
|
||||
let deletedEntityId = "2";
|
||||
let newEntity = {
|
||||
id: "4",
|
||||
name: "test",
|
||||
featureId: TestFeature.Users,
|
||||
};
|
||||
it("should delete one entity and create a new one", async function () {
|
||||
let { balance: masterBalanceBefore } = await autumn.check({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
});
|
||||
|
||||
let { balance: entityBalanceBefore } = await autumn.check({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
entity_id: deletedEntityId,
|
||||
});
|
||||
|
||||
await autumn.entities.delete(customerId, deletedEntityId);
|
||||
await autumn.entities.create(customerId, [newEntity]);
|
||||
|
||||
let { balance: masterBalanceAfter } = await autumn.check({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
});
|
||||
|
||||
expect(masterBalanceAfter).to.equal(masterBalanceBefore);
|
||||
|
||||
let { balance: entityBalanceAfter } = await autumn.check({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
entity_id: newEntity.id,
|
||||
});
|
||||
|
||||
expect(entityBalanceAfter).to.equal(entityBalanceBefore);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,185 @@
|
||||
// test payment failures
|
||||
|
||||
// test update product / delete entity mix -- should have correct sub item quantity...
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||
import {
|
||||
APIVersion,
|
||||
AppEnv,
|
||||
ErrCode,
|
||||
OnDecrease,
|
||||
OnIncrease,
|
||||
Organization,
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import Stripe from "stripe";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { setupBefore } from "tests/before.js";
|
||||
import { createProducts } from "tests/utils/productUtils.js";
|
||||
import { addPrefixToProducts, runAttachTest } from "../../attach/utils.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js";
|
||||
import { CusService } from "@/internal/customers/CusService.js";
|
||||
import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js";
|
||||
import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js";
|
||||
|
||||
let userItem = constructArrearProratedItem({
|
||||
featureId: TestFeature.Users,
|
||||
pricePerUnit: 50,
|
||||
includedUsage: 1,
|
||||
config: {
|
||||
on_increase: OnIncrease.BillImmediately,
|
||||
on_decrease: OnDecrease.None,
|
||||
},
|
||||
});
|
||||
|
||||
export let pro = constructProduct({
|
||||
items: [userItem],
|
||||
type: "pro",
|
||||
});
|
||||
|
||||
const testCase = "entity5";
|
||||
|
||||
describe(`${chalk.yellowBright(`contUse/${testCase}: Testing create entity payment fail`)}`, () => {
|
||||
let customerId = testCase;
|
||||
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
|
||||
let testClockId: string;
|
||||
let db: DrizzleCli, org: Organization, env: AppEnv;
|
||||
let stripeCli: Stripe;
|
||||
let curUnix = new Date().getTime();
|
||||
|
||||
before(async function () {
|
||||
await setupBefore(this);
|
||||
const { autumnJs } = this;
|
||||
db = this.db;
|
||||
org = this.org;
|
||||
env = this.env;
|
||||
|
||||
stripeCli = this.stripeCli;
|
||||
|
||||
addPrefixToProducts({
|
||||
products: [pro],
|
||||
prefix: testCase,
|
||||
});
|
||||
|
||||
await createProducts({
|
||||
autumn,
|
||||
products: [pro],
|
||||
customerId,
|
||||
db,
|
||||
orgId: org.id,
|
||||
env,
|
||||
});
|
||||
|
||||
const { testClockId: testClockId1 } = await initCustomer({
|
||||
autumn: autumnJs,
|
||||
customerId,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
attachPm: "success",
|
||||
});
|
||||
|
||||
testClockId = testClockId1!;
|
||||
});
|
||||
|
||||
let usage = 0;
|
||||
let firstEntities = [
|
||||
{
|
||||
id: "1",
|
||||
name: "test",
|
||||
featureId: TestFeature.Users,
|
||||
},
|
||||
];
|
||||
|
||||
it("should create one entity, then attach pro", async function () {
|
||||
await autumn.entities.create(customerId, firstEntities);
|
||||
usage += firstEntities.length;
|
||||
|
||||
await runAttachTest({
|
||||
autumn,
|
||||
customerId,
|
||||
product: pro,
|
||||
stripeCli,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
usage: [
|
||||
{
|
||||
featureId: TestFeature.Users,
|
||||
value: usage,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should attach failed payment method", async function () {
|
||||
let fullCus = await CusService.getFull({
|
||||
db,
|
||||
idOrInternalId: customerId,
|
||||
orgId: org.id,
|
||||
env,
|
||||
});
|
||||
|
||||
await attachFailedPaymentMethod({
|
||||
stripeCli,
|
||||
customer: fullCus,
|
||||
});
|
||||
});
|
||||
|
||||
it("should try to create entities and fail", async function () {
|
||||
await expectAutumnError({
|
||||
errMessage: "Your card was declined.",
|
||||
func: async () => {
|
||||
await autumn.entities.create(customerId, [
|
||||
{
|
||||
id: "2",
|
||||
name: "test",
|
||||
featureId: TestFeature.Users,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
name: "test",
|
||||
featureId: TestFeature.Users,
|
||||
},
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
||||
await expectSubQuantityCorrect({
|
||||
stripeCli,
|
||||
productId: pro.id,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
customerId,
|
||||
usage,
|
||||
numReplaceables: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("should track usage for users and fail", async function () {
|
||||
await expectAutumnError({
|
||||
errMessage: "Your card was declined.",
|
||||
func: async () => {
|
||||
return await autumn.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Users,
|
||||
value: 2,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
await expectSubQuantityCorrect({
|
||||
stripeCli,
|
||||
productId: pro.id,
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
customerId,
|
||||
usage,
|
||||
numReplaceables: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,8 +21,8 @@ import { timeout } from "@/utils/genUtils.js";
|
||||
import { advanceTestClock } from "tests/utils/stripeUtils.js";
|
||||
import { addPrefixToProducts } from "tests/attach/utils.js";
|
||||
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
|
||||
import { expectSubQuantityCorrect } from "tests/attach/entities/expectEntity.js";
|
||||
import { expectUpcomingItemsCorrect } from "tests/utils/expectUtils/expectContUseUtils.js";
|
||||
import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js";
|
||||
|
||||
let userItem = constructArrearProratedItem({
|
||||
featureId: TestFeature.Users,
|
||||
|
||||
@@ -17,7 +17,7 @@ import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.j
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { addPrefixToProducts, replaceItems } from "tests/attach/utils.js";
|
||||
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
|
||||
import { expectSubQuantityCorrect } from "tests/attach/entities/expectEntity.js";
|
||||
import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js";
|
||||
import { attachNewContUseAndExpectCorrect } from "tests/utils/expectUtils/expectContUse/expectUpdateContUse.js";
|
||||
|
||||
let userItem = constructArrearProratedItem({
|
||||
|
||||
@@ -17,7 +17,7 @@ import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.j
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { addPrefixToProducts, replaceItems } from "tests/attach/utils.js";
|
||||
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
|
||||
import { expectSubQuantityCorrect } from "tests/attach/entities/expectEntity.js";
|
||||
import { expectSubQuantityCorrect } from "tests/utils/expectUtils/expectContUseUtils.js";
|
||||
import { attachNewContUseAndExpectCorrect } from "tests/utils/expectUtils/expectContUse/expectUpdateContUse.js";
|
||||
import { expect } from "chai";
|
||||
import { advanceTestClock } from "tests/utils/stripeUtils.js";
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Customer } from "autumn-js";
|
||||
import { expect } from "chai";
|
||||
|
||||
export const expectResetAtCorrect = ({
|
||||
cusBefore,
|
||||
cusAfter,
|
||||
}: {
|
||||
cusBefore: Customer;
|
||||
cusAfter: Customer;
|
||||
}) => {
|
||||
const featuresBefore = cusBefore.features;
|
||||
const featuresAfter = cusAfter.features;
|
||||
|
||||
for (const featureId in featuresBefore) {
|
||||
const featureBefore = featuresBefore[featureId];
|
||||
if (!featureBefore.next_reset_at) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const featureAfter = featuresAfter[featureId];
|
||||
expect(featureAfter.next_reset_at).to.be.approximately(
|
||||
featureBefore.next_reset_at,
|
||||
10000,
|
||||
`reset for ${featureId} should be within 10 seconds`,
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Customer } from "autumn-js";
|
||||
import { expect } from "chai";
|
||||
|
||||
export const expectTrialEndsAtCorrect = ({
|
||||
cusBefore,
|
||||
cusAfter,
|
||||
}: {
|
||||
cusBefore: Customer;
|
||||
cusAfter: Customer;
|
||||
}) => {
|
||||
let productsBefore = cusBefore.products;
|
||||
let productsAfter = cusAfter.products;
|
||||
|
||||
for (const productBefore of productsBefore) {
|
||||
// @ts-ignore
|
||||
let trialEndsAtBefore = productBefore.trial_ends_at;
|
||||
|
||||
if (!trialEndsAtBefore) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const productAfter = productsAfter.find((p) => p.id === productBefore.id);
|
||||
// @ts-ignore
|
||||
expect(productAfter?.trial_ends_at).to.equal(trialEndsAtBefore);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Decimal } from "decimal.js";
|
||||
import { timeout } from "@/utils/genUtils.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { expect } from "chai";
|
||||
|
||||
export const useEntityBalanceAndExpect = async ({
|
||||
autumn,
|
||||
customerId,
|
||||
featureId,
|
||||
entityId,
|
||||
}: {
|
||||
autumn: AutumnInt;
|
||||
customerId: string;
|
||||
featureId: string;
|
||||
entityId: string;
|
||||
}) => {
|
||||
let deduction = new Decimal(Math.random() * 400)
|
||||
.toDecimalPlaces(5)
|
||||
.toNumber();
|
||||
|
||||
let balanceBefore = await autumn.check({
|
||||
customer_id: customerId,
|
||||
feature_id: featureId,
|
||||
entity_id: entityId,
|
||||
});
|
||||
|
||||
await autumn.track({
|
||||
customer_id: customerId,
|
||||
feature_id: featureId,
|
||||
value: deduction,
|
||||
entity_id: entityId,
|
||||
});
|
||||
await timeout(3000);
|
||||
|
||||
let balanceAfter = await autumn.check({
|
||||
customer_id: customerId,
|
||||
feature_id: featureId,
|
||||
entity_id: entityId,
|
||||
});
|
||||
|
||||
let expectedBalance = new Decimal(balanceBefore.balance!)
|
||||
.sub(deduction)
|
||||
.toNumber();
|
||||
|
||||
expect(balanceAfter.balance).to.equal(
|
||||
expectedBalance,
|
||||
"Entity balance should be correct",
|
||||
);
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import { AppEnv, FullCustomer, Organization } from "@autumn/shared";
|
||||
import { expect } from "chai";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
|
||||
export const expectSubQuantityCorrect = async ({
|
||||
stripeCli,
|
||||
@@ -18,6 +19,7 @@ export const expectSubQuantityCorrect = async ({
|
||||
org,
|
||||
env,
|
||||
customerId,
|
||||
itemQuantity,
|
||||
numReplaceables = 0,
|
||||
}: {
|
||||
stripeCli: Stripe;
|
||||
@@ -27,6 +29,7 @@ export const expectSubQuantityCorrect = async ({
|
||||
org: Organization;
|
||||
env: AppEnv;
|
||||
customerId: string;
|
||||
itemQuantity?: number;
|
||||
numReplaceables?: number;
|
||||
}) => {
|
||||
const fullCus = await CusService.getFull({
|
||||
@@ -56,7 +59,7 @@ export const expectSubQuantityCorrect = async ({
|
||||
});
|
||||
|
||||
expect(subItem).to.exist;
|
||||
expect(subItem!.quantity).to.equal(usage);
|
||||
expect(subItem!.quantity).to.equal(itemQuantity || usage);
|
||||
|
||||
// Check num replaceables correct
|
||||
let cusEnts = cusProduct?.customer_entitlements;
|
||||
@@ -67,6 +70,12 @@ export const expectSubQuantityCorrect = async ({
|
||||
|
||||
let expectedBalance = cusEnt!.entitlement.allowance! - usage;
|
||||
expect(cusEnt!.balance).to.equal(expectedBalance);
|
||||
|
||||
return {
|
||||
fullCus,
|
||||
cusProduct,
|
||||
stripeSubs,
|
||||
};
|
||||
};
|
||||
|
||||
export const expectUpcomingItemsCorrect = async ({
|
||||
@@ -112,3 +121,45 @@ export const expectUpcomingItemsCorrect = async ({
|
||||
|
||||
expect(lines[0].amount).to.equal(Math.round(proratedAmount * 100));
|
||||
};
|
||||
|
||||
export const calcProrationAndExpectInvoice = async ({
|
||||
autumn,
|
||||
stripeSubs,
|
||||
customerId,
|
||||
quantity,
|
||||
unitPrice,
|
||||
curUnix,
|
||||
numInvoices,
|
||||
}: {
|
||||
autumn: AutumnInt;
|
||||
stripeSubs: Stripe.Subscription[];
|
||||
customerId: string;
|
||||
quantity: number;
|
||||
unitPrice: number;
|
||||
curUnix: number;
|
||||
numInvoices: number;
|
||||
}) => {
|
||||
let customer = await autumn.customers.get(customerId);
|
||||
let invoices = customer.invoices;
|
||||
|
||||
let sub = stripeSubs[0];
|
||||
let amount = quantity * unitPrice;
|
||||
let proratedAmount = calculateProrationAmount({
|
||||
amount,
|
||||
periodStart: sub.current_period_start * 1000,
|
||||
periodEnd: sub.current_period_end * 1000,
|
||||
now: curUnix,
|
||||
allowNegative: true,
|
||||
});
|
||||
|
||||
proratedAmount = Number(proratedAmount.toFixed(2));
|
||||
|
||||
expect(invoices.length).to.equal(
|
||||
numInvoices,
|
||||
`Should have ${numInvoices} invoices`,
|
||||
);
|
||||
expect(invoices[0].total).to.equal(
|
||||
proratedAmount,
|
||||
"Latest invoice should be equals to calculated prorated amount",
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,24 +4,35 @@ import AutumnError from "@/external/autumn/autumnCli.js";
|
||||
|
||||
export const expectAutumnError = async ({
|
||||
errCode,
|
||||
errMessage,
|
||||
func,
|
||||
}: {
|
||||
errCode: string;
|
||||
errCode?: string;
|
||||
errMessage?: string;
|
||||
func: () => Promise<any>;
|
||||
}) => {
|
||||
try {
|
||||
await func();
|
||||
let result = await func();
|
||||
|
||||
assert.fail(
|
||||
`Expected to receive autumn error ${errCode}, but received none`,
|
||||
);
|
||||
} catch (error: any) {
|
||||
// 1. Expect error to be instance of AutumnError
|
||||
|
||||
expect(error, "Error should be instance of AutumnError").to.be.instanceOf(
|
||||
AutumnError,
|
||||
);
|
||||
|
||||
// 2. Expect error code to be the same as the one passed in
|
||||
expect(error.code, `Error code should be ${errCode}`).to.equal(errCode);
|
||||
if (errMessage) {
|
||||
expect(error.message, `Error message should be ${errMessage}`).to.equal(
|
||||
errMessage,
|
||||
);
|
||||
}
|
||||
|
||||
if (errCode) {
|
||||
// 2. Expect error code to be the same as the one passed in
|
||||
expect(error.code, `Error code should be ${errCode}`).to.equal(errCode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15,4 +15,5 @@ export interface AttachConfig {
|
||||
invoiceOnly: boolean;
|
||||
disableMerge: boolean;
|
||||
sameIntervals: boolean;
|
||||
carryTrial: boolean;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { FullCusProduct } from "../cusProductModels/cusProductModels.js";
|
||||
import { FreeTrial } from "../productModels/freeTrialModels/freeTrialModels.js";
|
||||
import { FullProduct } from "../productModels/productModels.js";
|
||||
import { UsageModel } from "../productV2Models/productItemModels/productItemModels.js";
|
||||
import { AttachBranch } from "./attachEnums/AttachBranch.js";
|
||||
import { AttachFunction } from "./attachEnums/AttachFunction.js";
|
||||
|
||||
export interface PreviewLineItem {
|
||||
amount: number | undefined;
|
||||
@@ -11,16 +14,18 @@ export interface PreviewLineItem {
|
||||
}
|
||||
|
||||
export interface AttachPreview {
|
||||
func: AttachFunction;
|
||||
branch: AttachBranch;
|
||||
options: any;
|
||||
new_items: any;
|
||||
due_today: {
|
||||
line_items: PreviewLineItem[];
|
||||
total: string;
|
||||
total: number;
|
||||
};
|
||||
due_next_cycle: {
|
||||
line_items: PreviewLineItem[];
|
||||
due_at: number;
|
||||
};
|
||||
free_trial?: FreeTrial | null;
|
||||
current_product?: FullProduct;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,6 @@ import { Infinite } from "../productModels/productEnums.js";
|
||||
import { ProductResponse } from "../productV2Models/productResponseModels.js";
|
||||
|
||||
export enum AttachScenario {
|
||||
// AlreadyAttached = "already_attached",
|
||||
// AlreadyScheduled = "already_scheduled",
|
||||
|
||||
Scheduled = "scheduled",
|
||||
Active = "active",
|
||||
New = "new",
|
||||
@@ -71,7 +68,7 @@ export enum FeaturePreviewScenario {
|
||||
FeatureFlag = "feature_flag",
|
||||
}
|
||||
|
||||
export interface CheckFeatureFormattedPreview {
|
||||
export interface CheckFeaturePreview {
|
||||
title: string;
|
||||
message: string;
|
||||
|
||||
@@ -80,6 +77,4 @@ export interface CheckFeatureFormattedPreview {
|
||||
feature_name: string;
|
||||
|
||||
products: ProductResponse[];
|
||||
// next_main_product: ProductResponse | null;
|
||||
// next_add_on_product: ProductResponse | null;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export default function ProductChangeDialog(params?: ProductChangeDialogProps) {
|
||||
let sum = 0;
|
||||
optionsInput.forEach((option) => {
|
||||
if (option.price && option.quantity) {
|
||||
sum += option.price * option.quantity;
|
||||
sum += option.price * (option.quantity / option.billing_units);
|
||||
}
|
||||
});
|
||||
setPrepaidTotals(sum);
|
||||
|
||||
Reference in New Issue
Block a user