fix: type errors
This commit is contained in:
@@ -271,12 +271,14 @@ export const createStripeInArrearPrice = async ({
|
|||||||
...productData,
|
...productData,
|
||||||
...priceAmountData,
|
...priceAmountData,
|
||||||
currency: org.default_currency || "usd",
|
currency: org.default_currency || "usd",
|
||||||
recurring: {
|
recurring: recurringData?.interval
|
||||||
interval: recurringData.interval,
|
? {
|
||||||
interval_count: recurringData.interval_count,
|
interval: recurringData.interval,
|
||||||
meter: meter.id,
|
interval_count: recurringData.interval_count,
|
||||||
usage_type: "metered",
|
meter: meter.id,
|
||||||
},
|
usage_type: "metered",
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
nickname: `Autumn Price (${relatedEnt.feature.name})`,
|
nickname: `Autumn Price (${relatedEnt.feature.name})`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
type Organization,
|
type Organization,
|
||||||
type Price,
|
type Price,
|
||||||
type Product,
|
type Product,
|
||||||
TierBehavior,
|
priceToStripeTiersMode,
|
||||||
TierInfinite,
|
TierInfinite,
|
||||||
type UsagePriceConfig,
|
type UsagePriceConfig,
|
||||||
} from "@autumn/shared";
|
} from "@autumn/shared";
|
||||||
@@ -71,12 +71,14 @@ export const createStripePrepaid = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
const relatedEnt = getPriceEntitlement(price, entitlements);
|
const relatedEnt = getPriceEntitlement(price, entitlements);
|
||||||
|
|
||||||
let recurringData;
|
let recurringData: Partial<Stripe.PriceCreateParams.Recurring> | undefined;
|
||||||
if (price.config!.interval !== BillingInterval.OneOff) {
|
if (price.config!.interval !== BillingInterval.OneOff) {
|
||||||
recurringData = billingIntervalToStripe({
|
recurringData = {
|
||||||
interval: price.config!.interval,
|
...billingIntervalToStripe({
|
||||||
intervalCount: price.config!.interval_count,
|
interval: price.config!.interval,
|
||||||
});
|
intervalCount: price.config!.interval_count,
|
||||||
|
}),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = price.config as UsagePriceConfig;
|
const config = price.config as UsagePriceConfig;
|
||||||
@@ -114,8 +116,7 @@ export const createStripePrepaid = async ({
|
|||||||
config.stripe_price_id = stripePrice.id;
|
config.stripe_price_id = stripePrice.id;
|
||||||
} else {
|
} else {
|
||||||
const tiers = prepaidToStripeTiers({ price, org });
|
const tiers = prepaidToStripeTiers({ price, org });
|
||||||
const tiersMode =
|
const tiersMode = priceToStripeTiersMode({ price });
|
||||||
price.tier_behavior === TierBehavior.VolumeBased ? "volume" : "graduated";
|
|
||||||
|
|
||||||
let priceAmountData = {};
|
let priceAmountData = {};
|
||||||
if (tiers.length === 1) {
|
if (tiers.length === 1) {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const billingIntervalToStripe = ({
|
|||||||
}: {
|
}: {
|
||||||
interval: BillingInterval;
|
interval: BillingInterval;
|
||||||
intervalCount?: number | null;
|
intervalCount?: number | null;
|
||||||
}): Stripe.PriceCreateParams.Recurring | Record<string, any> => {
|
}): Partial<Stripe.PriceCreateParams.Recurring> => {
|
||||||
const finalCount = intervalCount ?? 1;
|
const finalCount = intervalCount ?? 1;
|
||||||
switch (interval) {
|
switch (interval) {
|
||||||
case BillingInterval.Week:
|
case BillingInterval.Week:
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ export const constructArrearItem = ({
|
|||||||
featureId,
|
featureId,
|
||||||
includedUsage = 10000,
|
includedUsage = 10000,
|
||||||
price = 0.1,
|
price = 0.1,
|
||||||
|
tiers,
|
||||||
billingUnits = 1000,
|
billingUnits = 1000,
|
||||||
config = {
|
config = {
|
||||||
on_increase: OnIncrease.ProrateImmediately,
|
on_increase: OnIncrease.ProrateImmediately,
|
||||||
@@ -140,6 +141,7 @@ export const constructArrearItem = ({
|
|||||||
featureId: string;
|
featureId: string;
|
||||||
includedUsage?: number;
|
includedUsage?: number;
|
||||||
price?: number;
|
price?: number;
|
||||||
|
tiers?: { amount: number; to: number | "inf" }[];
|
||||||
billingUnits?: number;
|
billingUnits?: number;
|
||||||
config?: ProductItemConfig;
|
config?: ProductItemConfig;
|
||||||
rolloverConfig?: RolloverConfig;
|
rolloverConfig?: RolloverConfig;
|
||||||
@@ -152,7 +154,8 @@ export const constructArrearItem = ({
|
|||||||
feature_id: featureId,
|
feature_id: featureId,
|
||||||
usage_model: UsageModel.PayPerUse,
|
usage_model: UsageModel.PayPerUse,
|
||||||
included_usage: includedUsage,
|
included_usage: includedUsage,
|
||||||
price: price,
|
price: tiers ? undefined : price,
|
||||||
|
tiers: tiers,
|
||||||
billing_units: billingUnits,
|
billing_units: billingUnits,
|
||||||
interval: interval,
|
interval: interval,
|
||||||
interval_count: intervalCount,
|
interval_count: intervalCount,
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ test.concurrent(`${chalk.yellowBright("attach-prepaid-volume: immediate switch,
|
|||||||
product_id: premium.id,
|
product_id: premium.id,
|
||||||
options: [{ feature_id: TestFeature.Messages, quantity: newQuantity }],
|
options: [{ feature_id: TestFeature.Messages, quantity: newQuantity }],
|
||||||
});
|
});
|
||||||
expect(preview.total).toBeGreaterThan(0);
|
expect(preview.total).toEqual(30);
|
||||||
|
|
||||||
await autumnV1.billing.attach({
|
await autumnV1.billing.attach({
|
||||||
customer_id: customerId,
|
customer_id: customerId,
|
||||||
@@ -201,7 +201,7 @@ test.concurrent(`${chalk.yellowBright("attach-prepaid-volume: immediate switch,
|
|||||||
product_id: premium.id,
|
product_id: premium.id,
|
||||||
options: [{ feature_id: TestFeature.Messages, quantity: newQuantity }],
|
options: [{ feature_id: TestFeature.Messages, quantity: newQuantity }],
|
||||||
});
|
});
|
||||||
expect(preview.total).toBeGreaterThan(0);
|
expect(preview.total).toEqual(10 + 30);
|
||||||
|
|
||||||
await autumnV1.billing.attach({
|
await autumnV1.billing.attach({
|
||||||
customer_id: customerId,
|
customer_id: customerId,
|
||||||
|
|||||||
@@ -327,3 +327,98 @@ test.concurrent(`${chalk.yellowBright("attach-prepaid-volume: quantity 0 → no
|
|||||||
latestTotal: BASE_PRICE,
|
latestTotal: BASE_PRICE,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// TEST 5: 4 tiers with included usage — volume pricing at correct tier
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 4 tiers (billingUnits = 100):
|
||||||
|
* Tier 1: 0-200 units @ $15/pack
|
||||||
|
* Tier 2: 201-500 units @ $10/pack
|
||||||
|
* Tier 3: 501-1000 units @ $7/pack
|
||||||
|
* Tier 4: 1001+ units @ $5/pack
|
||||||
|
*
|
||||||
|
* With 100 included (free) and 900 total quantity:
|
||||||
|
* Paid packs: (900 - 100) / 100 = 8 packs
|
||||||
|
* Volume: all 8 packs at tier 3 rate ($7) = $56
|
||||||
|
* Total: $20 base + $56 = $76
|
||||||
|
*/
|
||||||
|
test.concurrent(`${chalk.yellowBright("attach-prepaid-volume: 4 tiers, 100 included, 900 total → $76")}`, async () => {
|
||||||
|
const customerId = "attach-prepaid-volume-4tier-incl";
|
||||||
|
const quantity = 900;
|
||||||
|
const includedUsage = 100;
|
||||||
|
|
||||||
|
// 4-tier pricing structure
|
||||||
|
const fourTiers = [
|
||||||
|
{ to: 200, amount: 15 },
|
||||||
|
{ to: 500, amount: 10 },
|
||||||
|
{ to: 1000, amount: 7 },
|
||||||
|
{ to: "inf" as const, amount: 5 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Paid packs after free: (900 - 100) / 100 = 8 packs
|
||||||
|
// 8 packs falls into tier 3 (800 paid units = 501-1000 range)
|
||||||
|
// Volume pricing: all 8 packs at $7 = $56
|
||||||
|
const expectedPrepaidCost = 8 * 7;
|
||||||
|
|
||||||
|
const volumeItem = items.volumePrepaidMessages({
|
||||||
|
includedUsage,
|
||||||
|
billingUnits: BILLING_UNITS,
|
||||||
|
tiers: fourTiers,
|
||||||
|
});
|
||||||
|
|
||||||
|
const pro = products.pro({
|
||||||
|
id: "pro-volume-4tier",
|
||||||
|
items: [volumeItem],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { autumnV1, ctx } = await initScenario({
|
||||||
|
customerId,
|
||||||
|
setup: [
|
||||||
|
s.customer({ paymentMethod: "success" }),
|
||||||
|
s.products({ list: [pro] }),
|
||||||
|
],
|
||||||
|
actions: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Preview must reflect volume pricing at tier 3
|
||||||
|
const preview = await autumnV1.billing.previewAttach({
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: pro.id,
|
||||||
|
options: [{ feature_id: TestFeature.Messages, quantity }],
|
||||||
|
});
|
||||||
|
expect(preview.total).toBe(BASE_PRICE + expectedPrepaidCost);
|
||||||
|
|
||||||
|
await autumnV1.billing.attach({
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: pro.id,
|
||||||
|
options: [{ feature_id: TestFeature.Messages, quantity }],
|
||||||
|
redirect_mode: "if_required",
|
||||||
|
});
|
||||||
|
|
||||||
|
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||||
|
|
||||||
|
await expectProductActive({ customer, productId: pro.id });
|
||||||
|
|
||||||
|
// Balance should equal total quantity (free + paid)
|
||||||
|
expectCustomerFeatureCorrect({
|
||||||
|
customer,
|
||||||
|
featureId: TestFeature.Messages,
|
||||||
|
balance: quantity,
|
||||||
|
usage: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expectCustomerInvoiceCorrect({
|
||||||
|
customer,
|
||||||
|
count: 1,
|
||||||
|
latestTotal: BASE_PRICE + expectedPrepaidCost,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expectSubToBeCorrect({
|
||||||
|
db: ctx.db,
|
||||||
|
customerId,
|
||||||
|
org: ctx.org,
|
||||||
|
env: ctx.env,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { expect, test } from "bun:test";
|
import { expect, test } from "bun:test";
|
||||||
import type { ApiCustomerV3 } from "@autumn/shared";
|
import { type ApiCustomerV3 } from "@autumn/shared";
|
||||||
import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect";
|
import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect";
|
||||||
import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect";
|
import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect";
|
||||||
import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect";
|
import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect";
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
import { expect, test } from "bun:test";
|
||||||
|
import type { ApiCustomerV3 } from "@autumn/shared";
|
||||||
|
import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect";
|
||||||
|
import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect";
|
||||||
|
import { calculateProration } from "@tests/integration/billing/utils/proration/calculateProration";
|
||||||
|
import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect";
|
||||||
|
import { TestFeature } from "@tests/setup/v2Features";
|
||||||
|
import { items } from "@tests/utils/fixtures/items";
|
||||||
|
import { products } from "@tests/utils/fixtures/products";
|
||||||
|
import { advanceTestClock } from "@tests/utils/stripeUtils";
|
||||||
|
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario";
|
||||||
|
import chalk from "chalk";
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
// PAID-TO-PAID: TIER BEHAVIOR TRANSITIONS
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const BILLING_UNITS = 100;
|
||||||
|
|
||||||
|
// Tiers for prepaid pricing tests (same for graduated and volume)
|
||||||
|
const PREPAID_TIERS = [
|
||||||
|
{ to: 500, amount: 10 },
|
||||||
|
{ to: "inf" as const, amount: 5 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 4.8 Graduated prepaid to volume prepaid (mid-cycle with proration)
|
||||||
|
test.concurrent(`${chalk.yellowBright("p2p: graduated prepaid to volume prepaid (mid-cycle proration)")}`, async () => {
|
||||||
|
const quantity = 800;
|
||||||
|
const basePrice = 20;
|
||||||
|
|
||||||
|
// Graduated calculation: 5 packs × $10 + 3 packs × $5 = $65
|
||||||
|
const graduatedCost = 5 * 10 + 3 * 5;
|
||||||
|
|
||||||
|
// Volume calculation: 8 packs × $5 (tier 2 rate for all) = $40
|
||||||
|
const volumeCost = 8 * 5;
|
||||||
|
|
||||||
|
const graduatedItem = items.tieredPrepaidMessages({
|
||||||
|
includedUsage: 0,
|
||||||
|
billingUnits: BILLING_UNITS,
|
||||||
|
tiers: PREPAID_TIERS,
|
||||||
|
});
|
||||||
|
const priceItem = items.monthlyPrice({ price: basePrice });
|
||||||
|
const pro = products.base({ id: "pro", items: [graduatedItem, priceItem] });
|
||||||
|
|
||||||
|
const { customerId, autumnV1, ctx, testClockId } = await initScenario({
|
||||||
|
customerId: "p2p-grad-to-vol-proration",
|
||||||
|
setup: [
|
||||||
|
s.customer({ paymentMethod: "success", testClock: true }),
|
||||||
|
s.products({ list: [pro] }),
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
s.attach({
|
||||||
|
productId: "pro",
|
||||||
|
options: [{ feature_id: TestFeature.Messages, quantity }],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify initial invoice (base price + graduated prepaid)
|
||||||
|
const initialCustomer =
|
||||||
|
await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||||
|
await expectCustomerInvoiceCorrect({
|
||||||
|
customer: initialCustomer,
|
||||||
|
count: 1,
|
||||||
|
latestTotal: basePrice + graduatedCost,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Advance 15 days (mid-cycle)
|
||||||
|
const advancedTo = await advanceTestClock({
|
||||||
|
stripeCli: ctx.stripeCli,
|
||||||
|
testClockId: testClockId!,
|
||||||
|
numberOfDays: 15,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Change to volume prepaid with same quantity
|
||||||
|
const volumeItem = items.volumePrepaidMessages({
|
||||||
|
includedUsage: 0,
|
||||||
|
billingUnits: BILLING_UNITS,
|
||||||
|
tiers: PREPAID_TIERS,
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateParams = {
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: pro.id,
|
||||||
|
items: [volumeItem, priceItem],
|
||||||
|
};
|
||||||
|
|
||||||
|
const preview = await autumnV1.subscriptions.previewUpdate(updateParams);
|
||||||
|
|
||||||
|
// Calculate prorated difference: credit old graduated, charge new volume
|
||||||
|
const proratedGraduated = await calculateProration({
|
||||||
|
customerId,
|
||||||
|
advancedTo,
|
||||||
|
amount: graduatedCost,
|
||||||
|
});
|
||||||
|
const proratedVolume = await calculateProration({
|
||||||
|
customerId,
|
||||||
|
advancedTo,
|
||||||
|
amount: volumeCost,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Volume is cheaper, so this should be a credit (negative)
|
||||||
|
const expectedAmount = proratedVolume - proratedGraduated;
|
||||||
|
|
||||||
|
expect(preview.total).toEqual(expectedAmount);
|
||||||
|
|
||||||
|
await autumnV1.subscriptions.update(updateParams);
|
||||||
|
|
||||||
|
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||||
|
|
||||||
|
// Balance should remain at 800 (quantity unchanged)
|
||||||
|
expectCustomerFeatureCorrect({
|
||||||
|
customer,
|
||||||
|
featureId: TestFeature.Messages,
|
||||||
|
balance: quantity,
|
||||||
|
usage: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expectCustomerInvoiceCorrect({
|
||||||
|
customer,
|
||||||
|
count: 2,
|
||||||
|
latestTotal: preview.total,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expectSubToBeCorrect({
|
||||||
|
db: ctx.db,
|
||||||
|
customerId,
|
||||||
|
org: ctx.org,
|
||||||
|
env: ctx.env,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tiers for consumable pricing tests
|
||||||
|
const CONSUMABLE_TIERS = [
|
||||||
|
{ to: 500, amount: 0.1 },
|
||||||
|
{ to: "inf" as const, amount: 0.05 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// 4.9 Volume prepaid to graduated tiered consumable
|
||||||
|
test.concurrent(`${chalk.yellowBright("p2p: volume prepaid to graduated tiered consumable")}`, async () => {
|
||||||
|
const quantity = 800;
|
||||||
|
const basePrice = 20;
|
||||||
|
|
||||||
|
// Volume prepaid: 8 packs × $5 = $40
|
||||||
|
const volumeCost = 8 * 5;
|
||||||
|
|
||||||
|
const volumeItem = items.volumePrepaidMessages({
|
||||||
|
includedUsage: 0,
|
||||||
|
billingUnits: BILLING_UNITS,
|
||||||
|
tiers: PREPAID_TIERS,
|
||||||
|
});
|
||||||
|
const priceItem = items.monthlyPrice({ price: basePrice });
|
||||||
|
const pro = products.base({ id: "pro", items: [volumeItem, priceItem] });
|
||||||
|
|
||||||
|
const { customerId, autumnV1, ctx } = await initScenario({
|
||||||
|
customerId: "p2p-vol-to-tiered-cons",
|
||||||
|
setup: [
|
||||||
|
s.customer({ paymentMethod: "success" }),
|
||||||
|
s.products({ list: [pro] }),
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
|
s.attach({
|
||||||
|
productId: "pro",
|
||||||
|
options: [{ feature_id: TestFeature.Messages, quantity }],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track some usage (600 of 800 = 200 remaining)
|
||||||
|
const messagesUsage = 600;
|
||||||
|
await autumnV1.track(
|
||||||
|
{
|
||||||
|
customer_id: customerId,
|
||||||
|
feature_id: TestFeature.Messages,
|
||||||
|
value: messagesUsage,
|
||||||
|
},
|
||||||
|
{ timeout: 2000 },
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify customer has usage tracked
|
||||||
|
const customerBefore =
|
||||||
|
await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||||
|
expect(customerBefore.features[TestFeature.Messages].usage).toBe(
|
||||||
|
messagesUsage,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Change to tiered consumable with 100 included
|
||||||
|
// tier_behavior should be undefined (defaults to graduated)
|
||||||
|
const tieredConsumableItem = items.tieredConsumableMessages({
|
||||||
|
includedUsage: 100,
|
||||||
|
billingUnits: 1,
|
||||||
|
tiers: CONSUMABLE_TIERS,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify the item has no tier_behavior set (defaults to graduated)
|
||||||
|
expect(tieredConsumableItem.tier_behavior).toBeUndefined();
|
||||||
|
|
||||||
|
const updateParams = {
|
||||||
|
customer_id: customerId,
|
||||||
|
product_id: pro.id,
|
||||||
|
items: [tieredConsumableItem, priceItem],
|
||||||
|
};
|
||||||
|
|
||||||
|
const preview = await autumnV1.subscriptions.previewUpdate(updateParams);
|
||||||
|
|
||||||
|
// Should refund full prepaid amount ($40)
|
||||||
|
expect(preview.total).toBe(-volumeCost);
|
||||||
|
|
||||||
|
await autumnV1.subscriptions.update(updateParams);
|
||||||
|
|
||||||
|
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||||
|
|
||||||
|
// Usage should be preserved (600 tracked)
|
||||||
|
// New included usage is 100, so balance = 100 - 600 = -500 (in overage)
|
||||||
|
expectCustomerFeatureCorrect({
|
||||||
|
customer,
|
||||||
|
featureId: TestFeature.Messages,
|
||||||
|
includedUsage: tieredConsumableItem.included_usage,
|
||||||
|
balance: tieredConsumableItem.included_usage - messagesUsage,
|
||||||
|
usage: messagesUsage,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expectCustomerInvoiceCorrect({
|
||||||
|
customer,
|
||||||
|
count: 2,
|
||||||
|
latestTotal: preview.total,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expectSubToBeCorrect({
|
||||||
|
db: ctx.db,
|
||||||
|
customerId,
|
||||||
|
org: ctx.org,
|
||||||
|
env: ctx.env,
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -579,6 +579,31 @@ const consumableWords = ({
|
|||||||
interval,
|
interval,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tiered consumable messages - graduated pricing with tiers (pay-per-use).
|
||||||
|
* Default tiers: 0-500 units at $0.10/unit, 501+ at $0.05/unit.
|
||||||
|
* IMPORTANT: Last tier MUST have `to: "inf"` - Stripe requires a catch-all tier.
|
||||||
|
* Note: tier_behavior is undefined, defaulting to graduated pricing.
|
||||||
|
*/
|
||||||
|
const tieredConsumableMessages = ({
|
||||||
|
includedUsage = 0,
|
||||||
|
billingUnits = 1,
|
||||||
|
tiers = [
|
||||||
|
{ to: 500, amount: 0.1 },
|
||||||
|
{ to: "inf", amount: 0.05 },
|
||||||
|
],
|
||||||
|
}: {
|
||||||
|
includedUsage?: number;
|
||||||
|
billingUnits?: number;
|
||||||
|
tiers?: { to: number | "inf"; amount: number }[];
|
||||||
|
} = {}): LimitedItem =>
|
||||||
|
constructArrearItem({
|
||||||
|
featureId: TestFeature.Messages,
|
||||||
|
tiers: tiers as { to: number; amount: number }[],
|
||||||
|
billingUnits,
|
||||||
|
includedUsage,
|
||||||
|
}) as LimitedItem;
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
// ALLOCATED / SEATS (prorated billing)
|
// ALLOCATED / SEATS (prorated billing)
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
@@ -701,6 +726,7 @@ export const items = {
|
|||||||
consumable,
|
consumable,
|
||||||
consumableMessages,
|
consumableMessages,
|
||||||
consumableWords,
|
consumableWords,
|
||||||
|
tieredConsumableMessages,
|
||||||
|
|
||||||
// Allocated
|
// Allocated
|
||||||
allocatedUsers,
|
allocatedUsers,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { UsageTier } from "@models/productModels/priceModels/priceConfig/us
|
|||||||
import { Infinite } from "@models/productModels/productEnums";
|
import { Infinite } from "@models/productModels/productEnums";
|
||||||
import { roundUsageToNearestBillingUnit } from "@utils/billingUtils/usageUtils/roundUsageToNearestBillingUnit";
|
import { roundUsageToNearestBillingUnit } from "@utils/billingUtils/usageUtils/roundUsageToNearestBillingUnit";
|
||||||
import { nullish } from "@utils/utils";
|
import { nullish } from "@utils/utils";
|
||||||
import Decimal from "decimal.js";
|
import { Decimal } from "decimal.js";
|
||||||
|
|
||||||
export const volumeTiersToLineAmount = ({
|
export const volumeTiersToLineAmount = ({
|
||||||
tiers,
|
tiers,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { Organization } from "@models/orgModels/orgTable";
|
import type { Organization } from "@models/orgModels/orgTable";
|
||||||
import { TierBehavior } from "@models/productModels/priceModels/priceConfig/usagePriceConfig";
|
|
||||||
import type { Price } from "@models/productModels/priceModels/priceModels";
|
import type { Price } from "@models/productModels/priceModels/priceModels";
|
||||||
import type { FullProduct } from "@models/productModels/productModels";
|
import type { FullProduct } from "@models/productModels/productModels";
|
||||||
import { orgToCurrency } from "@utils/orgUtils/convertOrgUtils";
|
import { orgToCurrency } from "@utils/orgUtils/convertOrgUtils";
|
||||||
@@ -8,6 +7,7 @@ import { priceToStripePrepaidV2Tiers } from "@utils/productUtils/priceUtils/conv
|
|||||||
import { priceToStripeProductName } from "@utils/productUtils/priceUtils/convertPrice/priceToStripeProductName";
|
import { priceToStripeProductName } from "@utils/productUtils/priceUtils/convertPrice/priceToStripeProductName";
|
||||||
import { priceToStripeRecurringParams } from "@utils/productUtils/priceUtils/convertPrice/priceToStripeRecurringParams";
|
import { priceToStripeRecurringParams } from "@utils/productUtils/priceUtils/convertPrice/priceToStripeRecurringParams";
|
||||||
import type Stripe from "stripe";
|
import type Stripe from "stripe";
|
||||||
|
import { priceToStripeTiersMode } from "./priceToStripeTiersMode";
|
||||||
|
|
||||||
export const priceToStripeCreatePriceParams = ({
|
export const priceToStripeCreatePriceParams = ({
|
||||||
price,
|
price,
|
||||||
@@ -41,9 +41,7 @@ export const priceToStripeCreatePriceParams = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const tiers = priceToStripePrepaidV2Tiers({ price, entitlement, org });
|
const tiers = priceToStripePrepaidV2Tiers({ price, entitlement, org });
|
||||||
|
const tiersMode = priceToStripeTiersMode({ price });
|
||||||
const tiersMode =
|
|
||||||
price.tier_behavior === TierBehavior.VolumeBased ? "volume" : "graduated";
|
|
||||||
|
|
||||||
let priceAmountData = {};
|
let priceAmountData = {};
|
||||||
if (tiers.length === 1) {
|
if (tiers.length === 1) {
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { type Price, TierBehavior } from "../../../..";
|
||||||
|
|
||||||
|
export const priceToStripeTiersMode = ({ price }: { price: Price }) => {
|
||||||
|
return price.tier_behavior === TierBehavior.VolumeBased
|
||||||
|
? "volume"
|
||||||
|
: "graduated";
|
||||||
|
};
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { priceIsTieredOneOff } from "@utils/productUtils/priceUtils/classifyPrice/priceIsTieredOneOff.js";
|
import { priceIsTieredOneOff } from "@utils/productUtils/priceUtils/classifyPrice/priceIsTieredOneOff.js";
|
||||||
import { priceToAllowanceInPacks } from "@utils/productUtils/priceUtils/convertPrice/priceToAllowanceInPacks.js";
|
import { priceToAllowanceInPacks } from "@utils/productUtils/priceUtils/convertPrice/priceToAllowanceInPacks.js";
|
||||||
import { priceToStripeCreatePriceParams } from "@utils/productUtils/priceUtils/convertPrice/priceToStripeCreatePriceParams.js";
|
import { priceToStripeCreatePriceParams } from "@utils/productUtils/priceUtils/convertPrice/priceToStripeCreatePriceParams.js";
|
||||||
|
import { priceToStripeTiersMode } from "./convertPrice/priceToStripeTiersMode.js";
|
||||||
|
|
||||||
export * from "./classifyPrice/priceIsTieredOneOff.js";
|
export * from "./classifyPrice/priceIsTieredOneOff.js";
|
||||||
export * from "./classifyPriceUtils.js";
|
export * from "./classifyPriceUtils.js";
|
||||||
export * from "./convertAmountUtils.js";
|
export * from "./convertAmountUtils.js";
|
||||||
|
export * from "./convertPrice/priceToStripeTiersMode.js";
|
||||||
export * from "./convertPriceUtils.js";
|
export * from "./convertPriceUtils.js";
|
||||||
export * from "./findPrice/findPriceByFeatureId.js";
|
export * from "./findPrice/findPriceByFeatureId.js";
|
||||||
export * from "./formatPriceUtils.js";
|
export * from "./formatPriceUtils.js";
|
||||||
@@ -14,6 +16,7 @@ export const priceUtils = {
|
|||||||
convert: {
|
convert: {
|
||||||
toAllowanceInPacks: priceToAllowanceInPacks,
|
toAllowanceInPacks: priceToAllowanceInPacks,
|
||||||
toStripeCreatePriceParams: priceToStripeCreatePriceParams,
|
toStripeCreatePriceParams: priceToStripeCreatePriceParams,
|
||||||
|
toStripeTiersMode: priceToStripeTiersMode,
|
||||||
},
|
},
|
||||||
|
|
||||||
isTieredOneOff: priceIsTieredOneOff,
|
isTieredOneOff: priceIsTieredOneOff,
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export const productItemToPlanItemParamsV1 = ({
|
|||||||
billing_units: planItemV1.price.billing_units,
|
billing_units: planItemV1.price.billing_units,
|
||||||
billing_method: planItemV1.price.billing_method,
|
billing_method: planItemV1.price.billing_method,
|
||||||
max_purchase: planItemV1.price.max_purchase ?? undefined,
|
max_purchase: planItemV1.price.max_purchase ?? undefined,
|
||||||
|
tier_behavior: planItemV1.price.tier_behavior ?? undefined,
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
proration: planItemV1.proration
|
proration: planItemV1.proration
|
||||||
|
|||||||
Reference in New Issue
Block a user