add product
This commit is contained in:
@@ -34,6 +34,7 @@ import {
|
|||||||
priceToIntervalKey,
|
priceToIntervalKey,
|
||||||
} from "@/internal/products/prices/priceUtils/convertPrice.js";
|
} from "@/internal/products/prices/priceUtils/convertPrice.js";
|
||||||
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
||||||
|
import { ItemSet } from "@/utils/models/ItemSet.js";
|
||||||
|
|
||||||
const getIntervalToPrices = (prices: Price[]) => {
|
const getIntervalToPrices = (prices: Price[]) => {
|
||||||
const intervalToPrices: Record<string, Price[]> = {};
|
const intervalToPrices: Record<string, Price[]> = {};
|
||||||
@@ -296,9 +297,21 @@ export const getStripeSubItems2 = async ({
|
|||||||
if (price.config.interval === BillingInterval.OneOff) {
|
if (price.config.interval === BillingInterval.OneOff) {
|
||||||
invoiceItems.push(lineItem);
|
invoiceItems.push(lineItem);
|
||||||
} else {
|
} else {
|
||||||
subItems.push(lineItem);
|
subItems.push({
|
||||||
|
...lineItem,
|
||||||
|
autumnPrice: price,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { subItems, invoiceItems, usageFeatures };
|
return { subItems, invoiceItems, usageFeatures } as ItemSet;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sanitizeSubItems = (subItems: any[]) => {
|
||||||
|
return subItems.map((si) => {
|
||||||
|
const { autumnPrice, ...rest } = si;
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
};
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ export const findStripeItemForPrice = ({
|
|||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
config.stripe_price_id == si.price?.id ||
|
config.stripe_price_id == si.price?.id ||
|
||||||
config.stripe_product_id == si.price?.product
|
config.stripe_product_id == si.price?.product ||
|
||||||
|
config.stripe_empty_price_id == si.price?.id
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
|
|||||||
import { getAlignedIntervalUnix } from "@/internal/products/prices/billingIntervalUtils.js";
|
import { getAlignedIntervalUnix } from "@/internal/products/prices/billingIntervalUtils.js";
|
||||||
import { getEarliestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
|
import { getEarliestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
|
||||||
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
||||||
|
import { sanitizeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
|
||||||
|
import { ItemSet } from "@/utils/models/ItemSet.js";
|
||||||
|
|
||||||
// Get payment method
|
// Get payment method
|
||||||
|
|
||||||
@@ -35,11 +37,7 @@ export const createStripeSub2 = async ({
|
|||||||
attachParams: AttachParams;
|
attachParams: AttachParams;
|
||||||
config: AttachConfig;
|
config: AttachConfig;
|
||||||
anchorToUnix?: number;
|
anchorToUnix?: number;
|
||||||
itemSet: {
|
itemSet: ItemSet;
|
||||||
subItems: Stripe.SubscriptionItem[];
|
|
||||||
invoiceItems: any[];
|
|
||||||
usageFeatures: string[];
|
|
||||||
};
|
|
||||||
earliestInterval?: IntervalConfig | null;
|
earliestInterval?: IntervalConfig | null;
|
||||||
}) => {
|
}) => {
|
||||||
const { customer, invoiceOnly, freeTrial, org, now, reward } = attachParams;
|
const { customer, invoiceOnly, freeTrial, org, now, reward } = attachParams;
|
||||||
@@ -94,7 +92,7 @@ export const createStripeSub2 = async ({
|
|||||||
const subscription = await stripeCli.subscriptions.create({
|
const subscription = await stripeCli.subscriptions.create({
|
||||||
...paymentMethodData,
|
...paymentMethodData,
|
||||||
customer: customer.processor.id,
|
customer: customer.processor.id,
|
||||||
items: subItems as any,
|
items: sanitizeSubItems(subItems),
|
||||||
// items: subItems as any,
|
// items: subItems as any,
|
||||||
billing_mode: { type: "flexible" },
|
billing_mode: { type: "flexible" },
|
||||||
trial_end: freeTrialToStripeTimestamp({ freeTrial, now }),
|
trial_end: freeTrialToStripeTimestamp({ freeTrial, now }),
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ import { addBillingIntervalUnix } from "@/internal/products/prices/billingInterv
|
|||||||
import { getSmallestInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
|
import { getSmallestInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
|
||||||
import RecaseError from "@/utils/errorUtils.js";
|
import RecaseError from "@/utils/errorUtils.js";
|
||||||
import { handleCreateCheckout } from "@/internal/customers/add-product/handleCreateCheckout.js";
|
import { handleCreateCheckout } from "@/internal/customers/add-product/handleCreateCheckout.js";
|
||||||
|
import { getCustomerSub } from "../../attachUtils/convertAttachParams.js";
|
||||||
|
import { paramsToSubItems } from "../../mergeUtils/paramsToSubItems.js";
|
||||||
|
import { updateStripeSub2 } from "../upgradeFlow/updateStripeSub2.js";
|
||||||
|
|
||||||
export const handlePaidProduct = async ({
|
export const handlePaidProduct = async ({
|
||||||
req,
|
req,
|
||||||
@@ -74,68 +77,95 @@ export const handlePaidProduct = async ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mergeSub = await cusProductToSub({
|
// let mergeSub = await cusProductToSub({
|
||||||
cusProduct: mergeCusProduct!,
|
// cusProduct: mergeCusProduct!,
|
||||||
stripeCli,
|
// stripeCli,
|
||||||
});
|
// });
|
||||||
|
|
||||||
let billingCycleAnchorUnix = undefined;
|
const mergeSub = await getCustomerSub({ attachParams });
|
||||||
if (attachParams.billingAnchor) {
|
let sub: Stripe.Subscription | null;
|
||||||
billingCycleAnchorUnix = attachParams.billingAnchor;
|
|
||||||
}
|
|
||||||
|
|
||||||
const earliestInterval = getSmallestInterval({ prices: attachParams.prices });
|
|
||||||
|
|
||||||
|
// 1. If merge sub
|
||||||
if (mergeSub) {
|
if (mergeSub) {
|
||||||
const { end } = subToPeriodStartEnd({ sub: mergeSub });
|
// Update
|
||||||
billingCycleAnchorUnix = end * 1000;
|
sub = mergeSub;
|
||||||
}
|
|
||||||
|
|
||||||
let newSub;
|
const newItemSet = await paramsToSubItems({
|
||||||
try {
|
req,
|
||||||
newSub = await createStripeSub2({
|
sub,
|
||||||
db: req.db,
|
|
||||||
stripeCli,
|
|
||||||
attachParams,
|
attachParams,
|
||||||
itemSet,
|
|
||||||
anchorToUnix: billingCycleAnchorUnix,
|
|
||||||
earliestInterval,
|
|
||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
|
||||||
if (
|
const { updatedSub } = await updateStripeSub2({
|
||||||
error instanceof RecaseError &&
|
req,
|
||||||
!invoiceOnly &&
|
attachParams,
|
||||||
error.code == ErrCode.CreateStripeSubscriptionFailed
|
curSub: sub,
|
||||||
) {
|
itemSet: newItemSet,
|
||||||
return await handleCreateCheckout({
|
config,
|
||||||
req,
|
fromCreate: true,
|
||||||
res,
|
});
|
||||||
attachParams,
|
|
||||||
config,
|
sub = updatedSub;
|
||||||
});
|
|
||||||
|
// 1.
|
||||||
|
} else {
|
||||||
|
// 2. If merge sub interval
|
||||||
|
let billingCycleAnchorUnix = undefined;
|
||||||
|
if (attachParams.billingAnchor) {
|
||||||
|
billingCycleAnchorUnix = attachParams.billingAnchor;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw error;
|
const earliestInterval = getSmallestInterval({
|
||||||
}
|
prices: attachParams.prices,
|
||||||
|
});
|
||||||
|
|
||||||
subscriptions.push(newSub);
|
if (mergeSub) {
|
||||||
|
const { end } = subToPeriodStartEnd({ sub: mergeSub });
|
||||||
|
billingCycleAnchorUnix = end * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
const anchorToUnix = getEarliestPeriodEnd({ sub: newSub }) * 1000;
|
try {
|
||||||
|
sub = await createStripeSub2({
|
||||||
const batchInsertInvoice: any = [];
|
|
||||||
for (const sub of subscriptions) {
|
|
||||||
if (!sub.latest_invoice) continue;
|
|
||||||
batchInsertInvoice.push(
|
|
||||||
insertInvoiceFromAttach({
|
|
||||||
db: req.db,
|
db: req.db,
|
||||||
stripeInvoice: sub.latest_invoice as Stripe.Invoice,
|
stripeCli,
|
||||||
attachParams,
|
attachParams,
|
||||||
logger,
|
itemSet,
|
||||||
})
|
anchorToUnix: billingCycleAnchorUnix,
|
||||||
);
|
earliestInterval,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
if (
|
||||||
|
error instanceof RecaseError &&
|
||||||
|
!invoiceOnly &&
|
||||||
|
error.code == ErrCode.CreateStripeSubscriptionFailed
|
||||||
|
) {
|
||||||
|
return await handleCreateCheckout({
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
attachParams,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const invoices = await Promise.all(batchInsertInvoice);
|
|
||||||
|
subscriptions.push(sub);
|
||||||
|
|
||||||
|
let invoice: Stripe.Invoice | undefined;
|
||||||
|
if (sub?.latest_invoice) {
|
||||||
|
invoice = await insertInvoiceFromAttach({
|
||||||
|
db: req.db,
|
||||||
|
stripeInvoice: sub.latest_invoice as Stripe.Invoice,
|
||||||
|
attachParams,
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const anchorToUnix = getEarliestPeriodEnd({ sub }) * 1000;
|
||||||
|
|
||||||
if (config.invoiceCheckout) {
|
if (config.invoiceCheckout) {
|
||||||
return {
|
return {
|
||||||
@@ -176,7 +206,7 @@ export const handlePaidProduct = async ({
|
|||||||
product_ids: products.map((p) => p.id),
|
product_ids: products.map((p) => p.id),
|
||||||
customer_id: customer.id || customer.internal_id,
|
customer_id: customer.id || customer.internal_id,
|
||||||
invoice: invoiceOnly
|
invoice: invoiceOnly
|
||||||
? attachToInvoiceResponse({ invoice: invoices?.[0] })
|
? attachToInvoiceResponse({ invoice })
|
||||||
: undefined,
|
: undefined,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -186,7 +216,7 @@ export const handlePaidProduct = async ({
|
|||||||
message: `Successfully created subscriptions and attached ${products
|
message: `Successfully created subscriptions and attached ${products
|
||||||
.map((p) => p.name)
|
.map((p) => p.name)
|
||||||
.join(", ")} to ${customer.name}`,
|
.join(", ")} to ${customer.name}`,
|
||||||
invoice: invoiceOnly ? invoices?.[0] : undefined,
|
invoice: invoiceOnly ? invoice : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -264,3 +294,17 @@ export const handlePaidProduct = async ({
|
|||||||
// throw error;
|
// throw error;
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// const batchInsertInvoice: any = [];
|
||||||
|
// for (const sub of subscriptions) {
|
||||||
|
// if (!sub.latest_invoice) continue;
|
||||||
|
// batchInsertInvoice.push(
|
||||||
|
// insertInvoiceFromAttach({
|
||||||
|
// db: req.db,
|
||||||
|
// stripeInvoice: sub.latest_invoice as Stripe.Invoice,
|
||||||
|
// attachParams,
|
||||||
|
// logger,
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// const invoices = await Promise.all(batchInsertInvoice);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
getEarliestPeriodEnd,
|
getEarliestPeriodEnd,
|
||||||
subToPeriodStartEnd,
|
subToPeriodStartEnd,
|
||||||
} from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
|
} from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
|
||||||
|
import { paramsToSubItems } from "../../mergeUtils/paramsToSubItems.js";
|
||||||
|
|
||||||
export const handleUpgradeFlow = async ({
|
export const handleUpgradeFlow = async ({
|
||||||
req,
|
req,
|
||||||
@@ -52,14 +53,23 @@ export const handleUpgradeFlow = async ({
|
|||||||
config,
|
config,
|
||||||
});
|
});
|
||||||
|
|
||||||
const newSubItems = await removeCurCusProductItems({
|
// const newSubItems = await removeCurCusProductItems({
|
||||||
sub: curSub,
|
// sub: curSub,
|
||||||
cusProduct: curCusProduct!,
|
// cusProduct: curCusProduct!,
|
||||||
subItems: itemSet.subItems,
|
// subItems: itemSet.subItems,
|
||||||
|
// });
|
||||||
|
|
||||||
|
const newItemSet = await paramsToSubItems({
|
||||||
|
req,
|
||||||
|
sub: curSub!,
|
||||||
|
attachParams,
|
||||||
|
config,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (newSubItems.length > 0) {
|
const { subItems } = newItemSet;
|
||||||
itemSet.subItems = newSubItems;
|
|
||||||
|
if (subItems.length > 0) {
|
||||||
|
itemSet.subItems = subItems;
|
||||||
|
|
||||||
logger.info(`1. Updating subs with new items`);
|
logger.info(`1. Updating subs with new items`);
|
||||||
const res = await updateStripeSub2({
|
const res = await updateStripeSub2({
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import { createProrationInvoice } from "@/external/stripe/stripeSubUtils/updateS
|
|||||||
import { createAndFilterContUseItems } from "../../attachUtils/getContUseItems/createContUseInvoiceItems.js";
|
import { createAndFilterContUseItems } from "../../attachUtils/getContUseItems/createContUseInvoiceItems.js";
|
||||||
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
|
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
|
||||||
import { getContUseInvoiceItems } from "../../attachUtils/getContUseItems/getContUseInvoiceItems.js";
|
import { getContUseInvoiceItems } from "../../attachUtils/getContUseItems/getContUseInvoiceItems.js";
|
||||||
|
import { ItemSet } from "@/utils/models/ItemSet.js";
|
||||||
|
import { sanitizeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
|
||||||
|
|
||||||
export const updateStripeSub2 = async ({
|
export const updateStripeSub2 = async ({
|
||||||
req,
|
req,
|
||||||
@@ -18,15 +20,14 @@ export const updateStripeSub2 = async ({
|
|||||||
config,
|
config,
|
||||||
curSub,
|
curSub,
|
||||||
itemSet,
|
itemSet,
|
||||||
|
fromCreate = false,
|
||||||
}: {
|
}: {
|
||||||
req: ExtendedRequest;
|
req: ExtendedRequest;
|
||||||
attachParams: AttachParams;
|
attachParams: AttachParams;
|
||||||
config: AttachConfig;
|
config: AttachConfig;
|
||||||
curSub: Stripe.Subscription;
|
curSub: Stripe.Subscription;
|
||||||
itemSet: {
|
itemSet: ItemSet;
|
||||||
subItems: Stripe.SubscriptionItem[];
|
fromCreate?: boolean;
|
||||||
invoiceItems: Stripe.InvoiceItem[];
|
|
||||||
};
|
|
||||||
}) => {
|
}) => {
|
||||||
const { db, logger } = req;
|
const { db, logger } = req;
|
||||||
const { curMainProduct } = attachParamToCusProducts({ attachParams });
|
const { curMainProduct } = attachParamToCusProducts({ attachParams });
|
||||||
@@ -52,9 +53,13 @@ export const updateStripeSub2 = async ({
|
|||||||
// 1. Update subscription
|
// 1. Update subscription
|
||||||
|
|
||||||
let updatedSub = await stripeCli.subscriptions.update(curSub.id, {
|
let updatedSub = await stripeCli.subscriptions.update(curSub.id, {
|
||||||
items: itemSet.subItems,
|
items: sanitizeSubItems(itemSet.subItems),
|
||||||
proration_behavior:
|
proration_behavior:
|
||||||
proration == ProrationBehavior.None ? "none" : "create_prorations",
|
proration == ProrationBehavior.None
|
||||||
|
? "none"
|
||||||
|
: fromCreate
|
||||||
|
? "always_invoice"
|
||||||
|
: "create_prorations",
|
||||||
trial_end: trialEnd,
|
trial_end: trialEnd,
|
||||||
default_payment_method: paymentMethod?.id,
|
default_payment_method: paymentMethod?.id,
|
||||||
// add_invoice_items: itemSet.invoiceItems,
|
// add_invoice_items: itemSet.invoiceItems,
|
||||||
@@ -75,6 +80,13 @@ export const updateStripeSub2 = async ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fromCreate) {
|
||||||
|
return {
|
||||||
|
updatedSub,
|
||||||
|
latestInvoice: curSub.latest_invoice as Stripe.Invoice,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 2. Create prorations for single use items
|
// 2. Create prorations for single use items
|
||||||
let { invoiceItems, cusEntIds } = await createUsageInvoiceItems({
|
let { invoiceItems, cusEntIds } = await createUsageInvoiceItems({
|
||||||
db,
|
db,
|
||||||
|
|||||||
@@ -46,6 +46,64 @@ export const attachParamsToProduct = ({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getCustomerSub = async ({
|
||||||
|
attachParams,
|
||||||
|
}: {
|
||||||
|
attachParams: AttachParams;
|
||||||
|
}) => {
|
||||||
|
const { stripeCli } = attachParams;
|
||||||
|
const fullCus = attachParams.customer;
|
||||||
|
let cusProducts = fullCus.customer_products;
|
||||||
|
|
||||||
|
const targetGroup = attachParams.products[0].group;
|
||||||
|
const targetEntityId = attachParams.internalEntityId || null;
|
||||||
|
const targetProductId = attachParams.products[0].id;
|
||||||
|
|
||||||
|
cusProducts.sort((a, b) => {
|
||||||
|
// 1. Check same group
|
||||||
|
const aGroupMatches = a.product.group === targetGroup;
|
||||||
|
const bGroupMatches = b.product.group === targetGroup;
|
||||||
|
|
||||||
|
if (aGroupMatches && !bGroupMatches) return -1;
|
||||||
|
if (!aGroupMatches && bGroupMatches) return 1;
|
||||||
|
|
||||||
|
// 2. Check main product
|
||||||
|
const aMain = !a.product.is_add_on;
|
||||||
|
const bMain = !b.product.is_add_on;
|
||||||
|
|
||||||
|
if (aMain && !bMain) return -1;
|
||||||
|
if (!aMain && bMain) return 1;
|
||||||
|
|
||||||
|
// 3. Check same product
|
||||||
|
const aProductIdMatches = a.product.id === targetProductId;
|
||||||
|
const bProductIdMatches = b.product.id === targetProductId;
|
||||||
|
|
||||||
|
if (aProductIdMatches && !bProductIdMatches) return -1;
|
||||||
|
if (!aProductIdMatches && bProductIdMatches) return 1;
|
||||||
|
|
||||||
|
// 4. Check same entity
|
||||||
|
const aEntityIdMatches = (a.internal_entity_id || null) === targetEntityId;
|
||||||
|
const bEntityIdMatches = (b.internal_entity_id || null) === targetEntityId;
|
||||||
|
|
||||||
|
if (aEntityIdMatches && !bEntityIdMatches) return -1;
|
||||||
|
if (!aEntityIdMatches && bEntityIdMatches) return 1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const subId = cusProducts.flatMap((cp) => cp.subscription_ids || [])?.[0];
|
||||||
|
|
||||||
|
if (!subId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sub = await stripeCli.subscriptions.retrieve(subId, {
|
||||||
|
expand: ["items.data.price.tiers"],
|
||||||
|
});
|
||||||
|
|
||||||
|
return sub;
|
||||||
|
};
|
||||||
|
|
||||||
export const paramsToCurSub = async ({
|
export const paramsToCurSub = async ({
|
||||||
attachParams,
|
attachParams,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { isArrearPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
|
||||||
|
import { ItemSet } from "@/utils/models/ItemSet.js";
|
||||||
|
import Stripe from "stripe";
|
||||||
|
|
||||||
|
export const mergeNewSubItems = ({
|
||||||
|
itemSet,
|
||||||
|
curSubItems,
|
||||||
|
}: {
|
||||||
|
itemSet: ItemSet;
|
||||||
|
curSubItems: Stripe.SubscriptionItem[];
|
||||||
|
}) => {
|
||||||
|
// 1. Don't need to add arrear prices if they already exist...
|
||||||
|
let newSubItems = structuredClone(itemSet.subItems);
|
||||||
|
const newArrearSubItems: any[] = [];
|
||||||
|
|
||||||
|
newSubItems = newSubItems.filter((newSi) => {
|
||||||
|
const existingItem = curSubItems.find((si) => si.price?.id === newSi.price);
|
||||||
|
if (isArrearPrice({ price: newSi.autumnPrice }) && existingItem) {
|
||||||
|
newArrearSubItems.push(newSi);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Add new subItems
|
||||||
|
for (let i = 0; i < newSubItems.length; i++) {
|
||||||
|
const newItem = newSubItems[i];
|
||||||
|
const existingItem = curSubItems.find(
|
||||||
|
(si) => si.price?.id === newItem.price
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!existingItem) continue;
|
||||||
|
|
||||||
|
newSubItems[i] = {
|
||||||
|
id: existingItem.id,
|
||||||
|
quantity: (existingItem.quantity || 0) + (newItem.quantity || 0),
|
||||||
|
// price: newItem.price,
|
||||||
|
// autumnPrice: newItem.autumnPrice,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return newSubItems;
|
||||||
|
};
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import Stripe from "stripe";
|
||||||
|
import { AttachParams } from "../../cusProducts/AttachParams.js";
|
||||||
|
import { getStripeSubItems2 } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
|
||||||
|
import { AttachConfig, FullCusProduct } from "@autumn/shared";
|
||||||
|
import { attachParamToCusProducts } from "../attachUtils/convertAttachParams.js";
|
||||||
|
import { getExistingCusProducts } from "../../cusProducts/cusProductUtils/getExistingCusProducts.js";
|
||||||
|
|
||||||
|
import { ExtendedRequest } from "@/utils/models/Request.js";
|
||||||
|
import { cusProductToPrices } from "../../cusProducts/cusProductUtils/convertCusProduct.js";
|
||||||
|
import {
|
||||||
|
isArrearPrice,
|
||||||
|
isUsagePrice,
|
||||||
|
} from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
|
||||||
|
import {
|
||||||
|
findStripeItemForPrice,
|
||||||
|
subItemInCusProduct,
|
||||||
|
} from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
|
||||||
|
import { mergeNewSubItems } from "./mergeNewSubItems.js";
|
||||||
|
import { formatPrice } from "@/internal/products/prices/priceUtils.js";
|
||||||
|
|
||||||
|
const getCusProductsToRemove = ({
|
||||||
|
attachParams,
|
||||||
|
}: {
|
||||||
|
attachParams: AttachParams;
|
||||||
|
}) => {
|
||||||
|
const products = attachParams.products;
|
||||||
|
const cusProducts = attachParams.cusProducts;
|
||||||
|
|
||||||
|
const cusProductsToRemove: FullCusProduct[] = [];
|
||||||
|
for (const product of products) {
|
||||||
|
// Get cur main and cur same
|
||||||
|
const { curMainProduct, curSameProduct } = getExistingCusProducts({
|
||||||
|
product,
|
||||||
|
cusProducts: attachParams.cusProducts,
|
||||||
|
internalEntityId: attachParams.internalEntityId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. If product is an add on, and there's current same, add it
|
||||||
|
if (product.is_add_on && curSameProduct) {
|
||||||
|
cusProductsToRemove.push(curSameProduct);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. If product is a main product, add curMain
|
||||||
|
if (curMainProduct) {
|
||||||
|
cusProductsToRemove.push(curMainProduct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get unique cus products, by cusProduct.id
|
||||||
|
const uniqueCusProductsToRemove = cusProductsToRemove.filter(
|
||||||
|
(cusProduct, index, array) =>
|
||||||
|
array.findIndex((cp) => cp.id === cusProduct.id) === index
|
||||||
|
);
|
||||||
|
|
||||||
|
return uniqueCusProductsToRemove;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const paramsToSubItems = async ({
|
||||||
|
req,
|
||||||
|
sub,
|
||||||
|
attachParams,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
req: ExtendedRequest;
|
||||||
|
sub: Stripe.Subscription;
|
||||||
|
attachParams: AttachParams;
|
||||||
|
config: AttachConfig;
|
||||||
|
}) => {
|
||||||
|
const { logger } = req;
|
||||||
|
const curSubItems = sub.items.data;
|
||||||
|
const itemSet = await getStripeSubItems2({
|
||||||
|
attachParams,
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. Remove items related to cur cus product...
|
||||||
|
const cusProductsToRemove = getCusProductsToRemove({ attachParams });
|
||||||
|
|
||||||
|
const newSubItems = mergeNewSubItems({
|
||||||
|
itemSet,
|
||||||
|
curSubItems,
|
||||||
|
});
|
||||||
|
|
||||||
|
const allCusProducts = attachParams.customer.customer_products;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`REMOVING CUS PRODUCTS:`,
|
||||||
|
cusProductsToRemove.map((cp) => `${cp.product.id}`)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Remove items related to cus products to remove
|
||||||
|
for (const cusProduct of cusProductsToRemove) {
|
||||||
|
const prices = cusProductToPrices({ cusProduct });
|
||||||
|
// console.log(
|
||||||
|
// `Prices:`,
|
||||||
|
// prices.map((p) => formatPrice({ price: p }))
|
||||||
|
// );
|
||||||
|
for (const price of prices) {
|
||||||
|
const existingSubItem = findStripeItemForPrice({
|
||||||
|
price,
|
||||||
|
stripeItems: curSubItems,
|
||||||
|
stripeProdId: cusProduct.product.processor?.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingSubItem) continue;
|
||||||
|
|
||||||
|
// 1. If arrear price
|
||||||
|
if (isArrearPrice({ price })) {
|
||||||
|
if (
|
||||||
|
allCusProducts.some((cp) => {
|
||||||
|
if (cp.id === cusProduct.id) return false;
|
||||||
|
|
||||||
|
return subItemInCusProduct({
|
||||||
|
cusProduct: cp,
|
||||||
|
subItem: existingSubItem as Stripe.SubscriptionItem,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
itemSet.subItems.some((si) => si.price == existingSubItem.price?.id)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
newSubItems.push({
|
||||||
|
id: existingSubItem.id,
|
||||||
|
deleted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to handle quantity updates and deletion
|
||||||
|
const updateItemQuantity = (item: any, newQuantity: number) => {
|
||||||
|
if (newQuantity <= 0) {
|
||||||
|
item.deleted = true;
|
||||||
|
item.quantity = undefined;
|
||||||
|
} else {
|
||||||
|
item.quantity = newQuantity;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Get quantity to remove
|
||||||
|
const quantityToRemove = 1;
|
||||||
|
|
||||||
|
// 2. Check if item already exists in newSubItems
|
||||||
|
const existingItemIndex = newSubItems.findIndex(
|
||||||
|
(si) => si.id === existingSubItem.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingItemIndex !== -1) {
|
||||||
|
// Update existing item in newSubItems
|
||||||
|
const currentQuantity = newSubItems[existingItemIndex].quantity || 0;
|
||||||
|
const newQuantity = currentQuantity - quantityToRemove;
|
||||||
|
updateItemQuantity(newSubItems[existingItemIndex], newQuantity);
|
||||||
|
} else {
|
||||||
|
// Add new item to newSubItems
|
||||||
|
const currentQuantity = existingSubItem.quantity || 0;
|
||||||
|
const newQuantity = currentQuantity - quantityToRemove;
|
||||||
|
newSubItems.push({
|
||||||
|
id: existingSubItem.id,
|
||||||
|
quantity: newQuantity,
|
||||||
|
});
|
||||||
|
updateItemQuantity(newSubItems[newSubItems.length - 1], newQuantity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
subItems: newSubItems,
|
||||||
|
invoiceItems: itemSet.invoiceItems,
|
||||||
|
usageFeatures: itemSet.usageFeatures,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import { ExtendedRequest } from "@/utils/models/Request.js";
|
|
||||||
import { AttachParams } from "../../cusProducts/AttachParams.js";
|
|
||||||
import { AttachConfig, intervalsSame } from "@autumn/shared";
|
|
||||||
import { subToAutumnInterval } from "@/external/stripe/utils.js";
|
|
||||||
import Stripe from "stripe";
|
|
||||||
import { ItemSet } from "@/utils/models/ItemSet.js";
|
|
||||||
import { attachParamsToCurCusProduct } from "../attachUtils/convertAttachParams.js";
|
|
||||||
|
|
||||||
// 1. Create proration items for fixed prices
|
|
||||||
const createFixedPriceProrations = async ({
|
|
||||||
req,
|
|
||||||
attachParams,
|
|
||||||
config,
|
|
||||||
currentSubs,
|
|
||||||
itemSets,
|
|
||||||
}: {
|
|
||||||
req: ExtendedRequest;
|
|
||||||
attachParams: AttachParams;
|
|
||||||
currentSubs: Stripe.Subscription[];
|
|
||||||
config: AttachConfig;
|
|
||||||
itemSets: ItemSet[];
|
|
||||||
}) => {
|
|
||||||
const { stripeCli } = attachParams;
|
|
||||||
const curCusProduct = attachParamsToCurCusProduct({ attachParams });
|
|
||||||
|
|
||||||
for (const sub of currentSubs) {
|
|
||||||
const itemSet = itemSets.find((itemSet) =>
|
|
||||||
intervalsSame({
|
|
||||||
intervalA: itemSet,
|
|
||||||
intervalB: subToAutumnInterval(sub),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// const { newItems, shouldCancel } = mergeWithCurItems({
|
|
||||||
// sub,
|
|
||||||
// itemSet,
|
|
||||||
// curCusProduct,
|
|
||||||
// });
|
|
||||||
// subToNewItems.push({
|
|
||||||
// sub,
|
|
||||||
// newItems,
|
|
||||||
// shouldCancel,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (newItems.length == 0) continue;
|
|
||||||
// if (shouldCancel) {
|
|
||||||
// const preview = await stripeCli.invoices.createPreview({
|
|
||||||
// subscription: sub.id,
|
|
||||||
// subscription_details: {
|
|
||||||
// cancel_now: true,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// for (const lineItem of preview.lines.data) {
|
|
||||||
// await stripeCli.invoiceItems.create({
|
|
||||||
// customer: attachParams.customer.processor.id,
|
|
||||||
// amount: lineItem.amount,
|
|
||||||
// currency: lineItem.currency,
|
|
||||||
// description: lineItem.description || "",
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// const [originalPreview, previewInvoice] = await Promise.all([
|
|
||||||
// stripeCli.invoices.createPreview({
|
|
||||||
// subscription: sub.id,
|
|
||||||
// }),
|
|
||||||
// stripeCli.invoices.createPreview({
|
|
||||||
// subscription: sub.id,
|
|
||||||
// subscription_details: {
|
|
||||||
// items: newItems,
|
|
||||||
// },
|
|
||||||
// }),
|
|
||||||
// ]);
|
|
||||||
// for (const lineItem of previewInvoice.lines.data) {
|
|
||||||
// const inCurItems = originalPreview.lines.data.find(
|
|
||||||
// (i) => i.id == lineItem.id
|
|
||||||
// );
|
|
||||||
// if (!inCurItems) {
|
|
||||||
// // console.log(lineItem.description, lineItem.amount);
|
|
||||||
// // prorationItems.push();
|
|
||||||
// await stripeCli.invoiceItems.create({
|
|
||||||
// customer: attachParams.customer.processor.id,
|
|
||||||
// amount: lineItem.amount,
|
|
||||||
// currency: lineItem.currency,
|
|
||||||
// description: lineItem.description || "",
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
// import Stripe from "stripe";
|
|
||||||
// import { DrizzleCli } from "@/db/initDrizzle.js";
|
|
||||||
// import { createStripeSub } from "@/external/stripe/stripeSubUtils/createStripeSub.js";
|
|
||||||
// import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
|
|
||||||
// import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
|
|
||||||
// import {
|
|
||||||
// AttachConfig,
|
|
||||||
// FullCusProduct,
|
|
||||||
// intervalsSame,
|
|
||||||
// ProrationBehavior,
|
|
||||||
// } from "@autumn/shared";
|
|
||||||
// import { getStripeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
|
|
||||||
// import { ExtendedRequest } from "@/utils/models/Request.js";
|
|
||||||
|
|
||||||
// export const handleUnifiedAttach = async ({
|
|
||||||
// req,
|
|
||||||
// attachParams,
|
|
||||||
// config,
|
|
||||||
// }: {
|
|
||||||
// req: ExtendedRequest;
|
|
||||||
// attachParams: AttachParams;
|
|
||||||
// config: AttachConfig;
|
|
||||||
// }) => {
|
|
||||||
// // 1. Get upcoming item sets, and current subscriptions
|
|
||||||
// const itemSets = await getStripeSubItems({
|
|
||||||
// attachParams,
|
|
||||||
// carryExistingUsages: config.carryUsage,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const currentSubs = await getCurrentSubs({
|
|
||||||
// db: req.db,
|
|
||||||
// attachParams,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // 2. Create invoice items for prorations
|
|
||||||
// };
|
|
||||||
|
|
||||||
// Expire old cus product
|
|
||||||
// Create new full cus product
|
|
||||||
// Insert new invoices
|
|
||||||
|
|
||||||
// export const updateSubsDiffInt = async ({
|
|
||||||
// req,
|
|
||||||
// stripeCli,
|
|
||||||
// curCusProduct,
|
|
||||||
// attachParams,
|
|
||||||
// stripeSubs,
|
|
||||||
// config,
|
|
||||||
// }: {
|
|
||||||
// req: ExtendedRequest;
|
|
||||||
// stripeCli: Stripe;
|
|
||||||
// curCusProduct: FullCusProduct;
|
|
||||||
// attachParams: AttachParams;
|
|
||||||
// stripeSubs: Stripe.Subscription[];
|
|
||||||
// config: AttachConfig;
|
|
||||||
// }) => {
|
|
||||||
// const { db, logger } = req;
|
|
||||||
// const itemSets = await getStripeSubItems({
|
|
||||||
// attachParams,
|
|
||||||
// carryExistingUsages: config.carryUsage,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // let trialEnd = config.disableTrial
|
|
||||||
// // ? undefined
|
|
||||||
// // : freeTrialToStripeTimestamp({
|
|
||||||
// // freeTrial: attachParams.freeTrial,
|
|
||||||
// // now: attachParams.now,
|
|
||||||
// // });
|
|
||||||
|
|
||||||
// // 1. Create prorations for fixed price items
|
|
||||||
// const subToNewItems: any = [];
|
|
||||||
|
|
||||||
// for (const sub of stripeSubs) {
|
|
||||||
// const itemSet = itemSets.find((itemSet) =>
|
|
||||||
// intervalsSame({
|
|
||||||
// intervalA: itemSet,
|
|
||||||
// intervalB: subToAutumnInterval(sub),
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
|
|
||||||
// const { newItems, shouldCancel } = mergeWithCurItems({
|
|
||||||
// sub,
|
|
||||||
// itemSet,
|
|
||||||
// curCusProduct,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// subToNewItems.push({
|
|
||||||
// sub,
|
|
||||||
// newItems,
|
|
||||||
// shouldCancel,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (newItems.length == 0) continue;
|
|
||||||
|
|
||||||
// if (shouldCancel) {
|
|
||||||
// const preview = await stripeCli.invoices.createPreview({
|
|
||||||
// subscription: sub.id,
|
|
||||||
// subscription_details: {
|
|
||||||
// cancel_now: true,
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// for (const lineItem of preview.lines.data) {
|
|
||||||
// await stripeCli.invoiceItems.create({
|
|
||||||
// customer: attachParams.customer.processor.id,
|
|
||||||
// amount: lineItem.amount,
|
|
||||||
// currency: lineItem.currency,
|
|
||||||
// description: lineItem.description || "",
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// } else {
|
|
||||||
// const [originalPreview, previewInvoice] = await Promise.all([
|
|
||||||
// stripeCli.invoices.createPreview({
|
|
||||||
// subscription: sub.id,
|
|
||||||
// }),
|
|
||||||
// stripeCli.invoices.createPreview({
|
|
||||||
// subscription: sub.id,
|
|
||||||
// subscription_details: {
|
|
||||||
// items: newItems,
|
|
||||||
// },
|
|
||||||
// }),
|
|
||||||
// ]);
|
|
||||||
|
|
||||||
// for (const lineItem of previewInvoice.lines.data) {
|
|
||||||
// const inCurItems = originalPreview.lines.data.find(
|
|
||||||
// (i) => i.id == lineItem.id
|
|
||||||
// );
|
|
||||||
|
|
||||||
// if (!inCurItems) {
|
|
||||||
// // console.log(lineItem.description, lineItem.amount);
|
|
||||||
// // prorationItems.push();
|
|
||||||
// await stripeCli.invoiceItems.create({
|
|
||||||
// customer: attachParams.customer.processor.id,
|
|
||||||
// amount: lineItem.amount,
|
|
||||||
// currency: lineItem.currency,
|
|
||||||
// description: lineItem.description || "",
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// for (const subToNewItem of subToNewItems) {
|
|
||||||
// console.log(`Sub to new item`);
|
|
||||||
// console.log(`Sub ID: ${subToNewItem.sub.id}`);
|
|
||||||
// console.log(`New items: ${JSON.stringify(subToNewItem.newItems)}`);
|
|
||||||
// console.log(`Should cancel: ${subToNewItem.shouldCancel}`);
|
|
||||||
// console.log(`--------------------------------`);
|
|
||||||
// }
|
|
||||||
// throw new Error("Stop");
|
|
||||||
|
|
||||||
// // 2. Create prorations for single use items
|
|
||||||
// let { invoiceItems, cusEntIds } = await createUsageInvoiceItems({
|
|
||||||
// db,
|
|
||||||
// attachParams,
|
|
||||||
// cusProduct: curCusProduct,
|
|
||||||
// stripeSubs,
|
|
||||||
// logger,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // Create any new subs
|
|
||||||
// const createItemSets = itemSets.filter((itemSet) => {
|
|
||||||
// return !stripeSubs.some((sub) =>
|
|
||||||
// intervalsSame({
|
|
||||||
// intervalA: itemSet,
|
|
||||||
// intervalB: subToAutumnInterval(sub),
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const updatedSubs = [];
|
|
||||||
// const invoices = [];
|
|
||||||
|
|
||||||
// // 1. Create new subscriptions
|
|
||||||
// for (const itemSet of createItemSets) {
|
|
||||||
// const newSub = await createStripeSub({
|
|
||||||
// db,
|
|
||||||
// attachParams,
|
|
||||||
// itemSet,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// updatedSubs.push(newSub);
|
|
||||||
// invoices.push(newSub.latest_invoice as Stripe.Invoice);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 2. Update or cancel old subs
|
|
||||||
// for (const newItemSet of subToNewItems) {
|
|
||||||
// if (newItemSet.shouldCancel) {
|
|
||||||
// await stripeCli.subscriptions.cancel(newItemSet.sub.id, {
|
|
||||||
// prorate: false,
|
|
||||||
// cancellation_details: {
|
|
||||||
// comment: "autumn_upgrade",
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// } else {
|
|
||||||
// const intervalConfig = subToAutumnInterval(newItemSet.sub);
|
|
||||||
// const { updatedSub, latestInvoice } = await updateStripeSub({
|
|
||||||
// req,
|
|
||||||
// attachParams,
|
|
||||||
// config: {
|
|
||||||
// ...config,
|
|
||||||
// proration: ProrationBehavior.None,
|
|
||||||
// },
|
|
||||||
// stripeSubs: [newItemSet.sub],
|
|
||||||
// itemSet: {
|
|
||||||
// items: newItemSet.newItems,
|
|
||||||
// interval: intervalConfig.interval,
|
|
||||||
// intervalCount: intervalConfig.intervalCount,
|
|
||||||
// } as any,
|
|
||||||
// intervalConfig: subToAutumnInterval(newItemSet.sub),
|
|
||||||
// });
|
|
||||||
|
|
||||||
// updatedSubs.push(updatedSub!);
|
|
||||||
// if (latestInvoice) {
|
|
||||||
// invoices.push(latestInvoice);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// newSubs: stripeSubs,
|
|
||||||
// invoices,
|
|
||||||
// // invoice: latestInvoice,
|
|
||||||
// // newInvoiceIds,
|
|
||||||
// };
|
|
||||||
// };
|
|
||||||
|
|
||||||
// 2. Update / cancel old subscriptions
|
|
||||||
|
|
||||||
// // 3. Update current subscription
|
|
||||||
// logger.info("1.2: Updating current subscription");
|
|
||||||
// const { updatedSub, latestInvoice } = await updateStripeSub({
|
|
||||||
// req,
|
|
||||||
// attachParams,
|
|
||||||
// config,
|
|
||||||
// trialEnd,
|
|
||||||
// itemSet: firstItemSet,
|
|
||||||
// stripeSubs: [firstSub],
|
|
||||||
// });
|
|
||||||
|
|
||||||
// await resetUsageBalances({
|
|
||||||
// db,
|
|
||||||
// cusEntIds,
|
|
||||||
// cusProduct: curCusProduct,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// let newSubs = [updatedSub!];
|
|
||||||
// const newInvoiceIds = latestInvoice ? [latestInvoice.id] : [];
|
|
||||||
|
|
||||||
// // 4. Update current sub schedules if exist...
|
|
||||||
// logger.info("1.3 Updating current sub schedules");
|
|
||||||
// await updateCurSchedules({
|
|
||||||
// db,
|
|
||||||
// stripeCli,
|
|
||||||
// curCusProduct,
|
|
||||||
// attachParams,
|
|
||||||
// itemSets,
|
|
||||||
// logger,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // 5. Cancel other subscriptions
|
|
||||||
// for (const sub of stripeSubs.slice(1)) {
|
|
||||||
// logger.info(`1.4: canceling additional sub: ${sub.id}`);
|
|
||||||
|
|
||||||
// // Filter out
|
|
||||||
// await stripeCli.subscriptions.cancel(sub.id, {
|
|
||||||
// prorate: true,
|
|
||||||
// cancellation_details: {
|
|
||||||
// comment: "autumn_upgrade",
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // 6. Create subs for other intervals
|
|
||||||
// for (const itemSet of itemSets.slice(1)) {
|
|
||||||
// const newSub = await createStripeSub({
|
|
||||||
// db,
|
|
||||||
// stripeCli,
|
|
||||||
// customer: attachParams.customer,
|
|
||||||
// org: attachParams.org,
|
|
||||||
// itemSet,
|
|
||||||
// invoiceOnly: attachParams.invoiceOnly || false,
|
|
||||||
// freeTrial: attachParams.freeTrial,
|
|
||||||
// // anchorToUnix: updatedSub!.current_period_end! * 1000,
|
|
||||||
// now: attachParams.now,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// newSubs.push(newSub);
|
|
||||||
// const latestInvoice = newSub.latest_invoice as Stripe.Invoice;
|
|
||||||
// newInvoiceIds.push(latestInvoice.id);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const firstSub = stripeSubs?.[0];
|
|
||||||
// const firstItemSet = itemSets?.[0];
|
|
||||||
|
|
||||||
// await addSubItemsToRemove({
|
|
||||||
// sub: firstSub,
|
|
||||||
// cusProduct: curCusProduct,
|
|
||||||
// itemSet: firstItemSet,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// throw new Error("Stop");
|
|
||||||
@@ -40,6 +40,12 @@ export const isUsagePrice = ({
|
|||||||
return isUsage;
|
return isUsage;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isArrearPrice = ({ price }: { price?: Price }) => {
|
||||||
|
if (!price) return false;
|
||||||
|
let billingType = getBillingType(price.config);
|
||||||
|
return billingType == BillingType.UsageInArrear;
|
||||||
|
};
|
||||||
|
|
||||||
export const isPrepaidPrice = ({ price }: { price: Price }) => {
|
export const isPrepaidPrice = ({ price }: { price: Price }) => {
|
||||||
let billingType = getBillingType(price.config);
|
let billingType = getBillingType(price.config);
|
||||||
return billingType == BillingType.UsageInAdvance;
|
return billingType == BillingType.UsageInAdvance;
|
||||||
|
|||||||
@@ -1,13 +1,30 @@
|
|||||||
import { BillingInterval } from "@autumn/shared";
|
import { BillingInterval, Price } from "@autumn/shared";
|
||||||
|
import Stripe from "stripe";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const ItemSetSchema = z.object({
|
// export const ItemSetSchema = z.object({
|
||||||
items: z.array(z.any()),
|
// items: z.array(z.any()),
|
||||||
prices: z.array(z.any()),
|
// prices: z.array(z.any()),
|
||||||
interval: z.nativeEnum(BillingInterval),
|
// interval: z.nativeEnum(BillingInterval),
|
||||||
intervalCount: z.number(),
|
// intervalCount: z.number(),
|
||||||
subMeta: z.record(z.string(), z.any()),
|
// subMeta: z.record(z.string(), z.any()),
|
||||||
usageFeatures: z.array(z.string()),
|
// usageFeatures: z.array(z.string()),
|
||||||
});
|
// });
|
||||||
|
|
||||||
export type ItemSet = z.infer<typeof ItemSetSchema>;
|
// export type ItemSet = z.infer<typeof ItemSetSchema>;
|
||||||
|
|
||||||
|
export type ItemSet = {
|
||||||
|
subItems: (Stripe.SubscriptionUpdateParams.Item & {
|
||||||
|
autumnPrice?: Price;
|
||||||
|
})[];
|
||||||
|
invoiceItems: Stripe.SubscriptionUpdateParams.AddInvoiceItem[];
|
||||||
|
usageFeatures: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// export const ItemSetSchema2 = z.object({
|
||||||
|
// subItems: z.array(z.any()),
|
||||||
|
// invoiceItems: z.array(z.any()),
|
||||||
|
// usageFeatures: z.array(z.any()),
|
||||||
|
// });
|
||||||
|
|
||||||
|
// export type ItemSet2 = z.infer<typeof ItemSetSchema2>;
|
||||||
|
|||||||
22
server/tests/merged/mergeUtils.test.ts
Normal file
22
server/tests/merged/mergeUtils.test.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { cusProductToPrices } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
|
||||||
|
import { isFixedPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
|
||||||
|
import { FullCusProduct } from "@autumn/shared";
|
||||||
|
|
||||||
|
export const cusProductToSubIds = ({
|
||||||
|
cusProducts,
|
||||||
|
}: {
|
||||||
|
cusProducts: FullCusProduct[];
|
||||||
|
}) => {
|
||||||
|
return [...new Set(cusProducts.flatMap((cp) => cp.subscription_ids || []))];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cpToPrice = ({
|
||||||
|
cp,
|
||||||
|
type,
|
||||||
|
}: {
|
||||||
|
cp: FullCusProduct;
|
||||||
|
type: "base" | "arrear" | "cont" | "prepaid";
|
||||||
|
}) => {
|
||||||
|
const prices = cusProductToPrices({ cusProduct: cp });
|
||||||
|
return prices.find((p) => isFixedPrice({ price: p }));
|
||||||
|
};
|
||||||
185
server/tests/merged/mergedAdd1.ts
Normal file
185
server/tests/merged/mergedAdd1.ts
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import chalk from "chalk";
|
||||||
|
import { setupBefore } from "tests/before.js";
|
||||||
|
import { Stripe } from "stripe";
|
||||||
|
import { createProducts } from "tests/utils/productUtils.js";
|
||||||
|
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||||
|
import { TestFeature } from "tests/setup/v2Features.js";
|
||||||
|
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||||
|
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||||
|
import { APIVersion, AppEnv, entities, Organization } from "@autumn/shared";
|
||||||
|
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
||||||
|
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||||
|
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
|
||||||
|
import { CusService } from "@/internal/customers/CusService.js";
|
||||||
|
import { expect } from "chai";
|
||||||
|
import { cpToPrice, cusProductToSubIds } from "./mergeUtils.test.js";
|
||||||
|
import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
|
||||||
|
|
||||||
|
// UNCOMMENT FROM HERE
|
||||||
|
let pro = constructProduct({
|
||||||
|
id: "pro",
|
||||||
|
items: [constructArrearItem({ featureId: TestFeature.Words })],
|
||||||
|
type: "pro",
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`${chalk.yellowBright("mergedAdd1: Testing merged subs, with track")}`, () => {
|
||||||
|
let customerId = "mergedAdd1";
|
||||||
|
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
|
||||||
|
|
||||||
|
let stripeCli: Stripe;
|
||||||
|
let testClockId: string;
|
||||||
|
let curUnix: number;
|
||||||
|
let db: DrizzleCli;
|
||||||
|
let org: Organization;
|
||||||
|
let env: AppEnv;
|
||||||
|
|
||||||
|
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: customerId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await createProducts({
|
||||||
|
autumn: autumnJs,
|
||||||
|
products: [pro],
|
||||||
|
db,
|
||||||
|
orgId: org.id,
|
||||||
|
env,
|
||||||
|
customerId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { testClockId: testClockId1 } = await initCustomer({
|
||||||
|
autumn: autumnJs,
|
||||||
|
customerId,
|
||||||
|
db,
|
||||||
|
org,
|
||||||
|
env,
|
||||||
|
attachPm: "success",
|
||||||
|
});
|
||||||
|
|
||||||
|
testClockId = testClockId1!;
|
||||||
|
});
|
||||||
|
|
||||||
|
const entities = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
name: "Entity 1",
|
||||||
|
feature_id: TestFeature.Users,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
name: "Entity 2",
|
||||||
|
feature_id: TestFeature.Users,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
it("should attach pro product", async function () {
|
||||||
|
await autumn.entities.create(customerId, entities);
|
||||||
|
|
||||||
|
await autumn.attach({
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: pro.id,
|
||||||
|
entity_id: "1",
|
||||||
|
});
|
||||||
|
|
||||||
|
await autumn.attach({
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: pro.id,
|
||||||
|
entity_id: "2",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1. Should have one sub
|
||||||
|
const fullCus = await CusService.getFull({
|
||||||
|
db,
|
||||||
|
idOrInternalId: customerId,
|
||||||
|
orgId: org.id,
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
|
||||||
|
const cusProducts = fullCus.customer_products;
|
||||||
|
expect(cusProducts.length).to.equal(3);
|
||||||
|
|
||||||
|
const subIds = cusProductToSubIds({ cusProducts });
|
||||||
|
expect(subIds.length).to.equal(1);
|
||||||
|
|
||||||
|
// Get sub
|
||||||
|
const sub = await stripeCli.subscriptions.retrieve(subIds[0]);
|
||||||
|
|
||||||
|
// 1. Should have quantity 2 for base price
|
||||||
|
const basePrice = cpToPrice({ cp: cusProducts[0], type: "base" });
|
||||||
|
const arrearPrice = cpToPrice({ cp: cusProducts[0], type: "arrear" });
|
||||||
|
|
||||||
|
const baseItem = findStripeItemForPrice({
|
||||||
|
price: basePrice!,
|
||||||
|
stripeItems: sub.items.data,
|
||||||
|
})!;
|
||||||
|
|
||||||
|
const arrearItem = findStripeItemForPrice({
|
||||||
|
price: arrearPrice!,
|
||||||
|
stripeItems: sub.items.data,
|
||||||
|
})!;
|
||||||
|
|
||||||
|
expect(baseItem.quantity).to.equal(2);
|
||||||
|
expect(arrearItem.quantity).to.equal(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// it("should attach premium product", async function () {
|
||||||
|
// const wordsUsage = 100000;
|
||||||
|
// await autumn.track({
|
||||||
|
// customer_id: customerId,
|
||||||
|
// feature_id: TestFeature.Words,
|
||||||
|
// value: wordsUsage,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// curUnix = await advanceTestClock({
|
||||||
|
// stripeCli,
|
||||||
|
// testClockId,
|
||||||
|
// advanceTo: addWeeks(new Date(), 2).getTime(),
|
||||||
|
// waitForSeconds: 10,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// await attachAndExpectCorrect({
|
||||||
|
// autumn,
|
||||||
|
// customerId,
|
||||||
|
// product: premium,
|
||||||
|
// stripeCli,
|
||||||
|
// db,
|
||||||
|
// org,
|
||||||
|
// env,
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// it("should attach growth product", async function () {
|
||||||
|
// const wordsUsage = 200000;
|
||||||
|
// await autumn.track({
|
||||||
|
// customer_id: customerId,
|
||||||
|
// feature_id: TestFeature.Words,
|
||||||
|
// value: wordsUsage,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// curUnix = await advanceTestClock({
|
||||||
|
// stripeCli,
|
||||||
|
// testClockId,
|
||||||
|
// advanceTo: addWeeks(curUnix, 1).getTime(),
|
||||||
|
// waitForSeconds: 10,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// await attachAndExpectCorrect({
|
||||||
|
// autumn,
|
||||||
|
// customerId,
|
||||||
|
// product: growth,
|
||||||
|
// stripeCli,
|
||||||
|
// db,
|
||||||
|
// org,
|
||||||
|
// env,
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user