fix: billing interval
This commit is contained in:
@@ -1,14 +1,18 @@
|
||||
import { mapToProductItems } from "@/internal/products/productV2Utils.js";
|
||||
import {
|
||||
type AttachBranch,
|
||||
type AttachConfig,
|
||||
UsageModel,
|
||||
} from "@autumn/shared";
|
||||
import { getLatestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
|
||||
import { getOptions } from "@/internal/api/entitled/checkUtils.js";
|
||||
import { getItemsForNewProduct } from "@/internal/invoices/previewItemUtils/getItemsForNewProduct.js";
|
||||
import { AttachParams } from "../../cusProducts/AttachParams.js";
|
||||
import { mapToProductItems } from "@/internal/products/productV2Utils.js";
|
||||
import type { AttachParams } from "../../cusProducts/AttachParams.js";
|
||||
import {
|
||||
attachParamsToProduct,
|
||||
attachParamToCusProducts,
|
||||
paramsToCurSub,
|
||||
} from "../attachUtils/convertAttachParams.js";
|
||||
import { getOptions } from "@/internal/api/entitled/checkUtils.js";
|
||||
import { AttachBranch, AttachConfig, UsageModel } from "@autumn/shared";
|
||||
import { getLatestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
|
||||
|
||||
export const getDowngradeProductPreview = async ({
|
||||
attachParams,
|
||||
@@ -31,13 +35,12 @@ export const getDowngradeProductPreview = async ({
|
||||
let items = await getItemsForNewProduct({
|
||||
newProduct,
|
||||
attachParams,
|
||||
now,
|
||||
logger,
|
||||
});
|
||||
|
||||
items = items.filter((item) => item.usage_model !== UsageModel.Prepaid);
|
||||
|
||||
let options = getOptions({
|
||||
const options = getOptions({
|
||||
prodItems: mapToProductItems({
|
||||
prices: newProduct.prices,
|
||||
entitlements: newProduct.entitlements,
|
||||
@@ -48,7 +51,7 @@ export const getDowngradeProductPreview = async ({
|
||||
});
|
||||
|
||||
const latestPeriodEnd = sub ? getLatestPeriodEnd({ sub }) * 1000 : undefined;
|
||||
let nextCycleAt = curCusProduct?.trial_ends_at
|
||||
const nextCycleAt = curCusProduct?.trial_ends_at
|
||||
? curCusProduct.trial_ends_at
|
||||
: latestPeriodEnd;
|
||||
|
||||
|
||||
@@ -66,7 +66,6 @@ const getNextCycleItems = async ({
|
||||
const items = await getItemsForNewProduct({
|
||||
newProduct,
|
||||
attachParams,
|
||||
now: attachParams.now,
|
||||
logger,
|
||||
withPrepaid,
|
||||
// anchor,
|
||||
@@ -123,7 +122,6 @@ export const getNewProductPreview = async ({
|
||||
const items = await getItemsForNewProduct({
|
||||
newProduct,
|
||||
attachParams,
|
||||
now: attachParams.now,
|
||||
freeTrial: attachParams.freeTrial,
|
||||
anchor,
|
||||
logger,
|
||||
|
||||
@@ -179,10 +179,10 @@ export const getUpgradeProductPreview = async ({
|
||||
freeTrial = curCusProduct.free_trial;
|
||||
}
|
||||
|
||||
// console.log("Anchor")
|
||||
const newPreviewItems = await getItemsForNewProduct({
|
||||
newProduct,
|
||||
attachParams,
|
||||
now,
|
||||
freeTrial,
|
||||
sub: sub!,
|
||||
logger,
|
||||
|
||||
@@ -122,7 +122,6 @@ export const getProration = ({
|
||||
export const getItemsForNewProduct = async ({
|
||||
newProduct,
|
||||
attachParams,
|
||||
now,
|
||||
proration,
|
||||
anchor,
|
||||
freeTrial,
|
||||
@@ -133,7 +132,6 @@ export const getItemsForNewProduct = async ({
|
||||
}: {
|
||||
newProduct: FullProduct;
|
||||
attachParams: AttachParams;
|
||||
now?: number;
|
||||
proration?: {
|
||||
start: number;
|
||||
end: number;
|
||||
@@ -146,7 +144,7 @@ export const getItemsForNewProduct = async ({
|
||||
skipOneOff?: boolean;
|
||||
}) => {
|
||||
const { org, features } = attachParams;
|
||||
now = now || Date.now();
|
||||
const now = attachParams.now || Date.now();
|
||||
|
||||
const items: PreviewLineItem[] = [];
|
||||
|
||||
@@ -163,6 +161,7 @@ export const getItemsForNewProduct = async ({
|
||||
if (printLogs) {
|
||||
console.log("price", formatPrice({ price }));
|
||||
console.log("now:", formatUnixToDate(now));
|
||||
console.log("anchor", formatUnixToDate(anchor));
|
||||
}
|
||||
|
||||
const finalProration = getProration({
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { BillingInterval, IntervalConfig } from "@autumn/shared";
|
||||
import { BillingInterval, type IntervalConfig } from "@autumn/shared";
|
||||
import { UTCDate } from "@date-fns/utc";
|
||||
import {
|
||||
addMinutes,
|
||||
addMonths,
|
||||
addSeconds,
|
||||
addWeeks,
|
||||
addYears,
|
||||
differenceInSeconds,
|
||||
@@ -20,7 +19,6 @@ import {
|
||||
subWeeks,
|
||||
subYears,
|
||||
} from "date-fns";
|
||||
import { UTCDate } from "@date-fns/utc";
|
||||
import { formatUnixToDateTime } from "@/utils/genUtils.js";
|
||||
|
||||
export const subtractBillingIntervalUnix = ({
|
||||
@@ -192,13 +190,13 @@ export const getAlignedIntervalUnix = ({
|
||||
console.log("--------------------------------");
|
||||
}
|
||||
|
||||
let anchorAndNaturalDiff = differenceInSeconds(
|
||||
const anchorAndNaturalDiff = differenceInSeconds(
|
||||
naturalBillingDate,
|
||||
nextCycleAnchorUnix,
|
||||
);
|
||||
|
||||
// For insurance, also means you can't set billing cycle anchor to a minute in the future...
|
||||
let anchorAndNowDiff = Math.abs(
|
||||
const anchorAndNowDiff = Math.abs(
|
||||
differenceInSeconds(now, nextCycleAnchorUnix),
|
||||
);
|
||||
|
||||
@@ -254,8 +252,9 @@ export const subtractFromUnixTillAligned = ({
|
||||
return getTime(alignedDate);
|
||||
};
|
||||
|
||||
// Subtracts an interval from a period end, preserving end-of-month anchoring
|
||||
// e.g. 30 Sep -> 31 Aug (not 30 Aug)
|
||||
// Subtracts an interval from a period end, preserving anchor-based end-of-month behavior
|
||||
// Uses the anchor date (unixTimestamp) to determine if end-of-month logic should apply
|
||||
// e.g. Sep 30 anchor -> Aug 30, Jul 30, Feb 28/29 (follows Stripe behavior)
|
||||
export const subtractIntervalForProration = ({
|
||||
unixTimestamp,
|
||||
interval,
|
||||
@@ -265,68 +264,64 @@ export const subtractIntervalForProration = ({
|
||||
interval: BillingInterval;
|
||||
intervalCount?: number;
|
||||
}) => {
|
||||
const endDate = new UTCDate(unixTimestamp);
|
||||
|
||||
const isEndOfMonth = () => {
|
||||
const lastDay = new UTCDate(
|
||||
endDate.getFullYear(),
|
||||
endDate.getMonth() + 1,
|
||||
0,
|
||||
).getDate();
|
||||
return getDate(endDate) === lastDay;
|
||||
};
|
||||
const anchorDate = new UTCDate(unixTimestamp);
|
||||
const anchorDay = getDate(anchorDate);
|
||||
|
||||
const preserveTime = (d: UTCDate) => {
|
||||
let preserved = new UTCDate(d.getTime());
|
||||
preserved = new UTCDate(setHours(preserved, getHours(endDate)).getTime());
|
||||
preserved = new UTCDate(
|
||||
setMinutes(preserved, getMinutes(endDate)).getTime(),
|
||||
setHours(preserved, getHours(anchorDate)).getTime(),
|
||||
);
|
||||
preserved = new UTCDate(
|
||||
setSeconds(preserved, getSeconds(endDate)).getTime(),
|
||||
setMinutes(preserved, getMinutes(anchorDate)).getTime(),
|
||||
);
|
||||
preserved = new UTCDate(
|
||||
setSeconds(preserved, getSeconds(anchorDate)).getTime(),
|
||||
);
|
||||
return preserved;
|
||||
};
|
||||
|
||||
const setToLastDayOfMonth = (d: UTCDate) => {
|
||||
const last = new UTCDate(d.getFullYear(), d.getMonth() + 1, 0).getDate();
|
||||
return new UTCDate(setDate(d, last).getTime());
|
||||
const setToAnchorDayOrEndOfMonth = (d: UTCDate) => {
|
||||
const lastDayOfTargetMonth = new UTCDate(
|
||||
d.getFullYear(),
|
||||
d.getMonth() + 1,
|
||||
0,
|
||||
).getDate();
|
||||
|
||||
// If the anchor day doesn't exist in the target month, go to the last day of that month
|
||||
// This follows Stripe's behavior: Sep 30 -> Feb 28, Jul 31 -> Feb 28, etc.
|
||||
const targetDay = Math.min(anchorDay, lastDayOfTargetMonth);
|
||||
return new UTCDate(setDate(d, targetDay).getTime());
|
||||
};
|
||||
|
||||
switch (interval) {
|
||||
case BillingInterval.Week: {
|
||||
const sub = new UTCDate(subWeeks(endDate, 1 * intervalCount).getTime());
|
||||
const sub = new UTCDate(
|
||||
subWeeks(anchorDate, 1 * intervalCount).getTime(),
|
||||
);
|
||||
return getTime(sub);
|
||||
}
|
||||
case BillingInterval.Month: {
|
||||
let sub = new UTCDate(subMonths(endDate, 1 * intervalCount).getTime());
|
||||
if (isEndOfMonth()) {
|
||||
sub = setToLastDayOfMonth(sub);
|
||||
}
|
||||
let sub = new UTCDate(subMonths(anchorDate, 1 * intervalCount).getTime());
|
||||
sub = setToAnchorDayOrEndOfMonth(sub);
|
||||
sub = preserveTime(sub);
|
||||
return getTime(sub);
|
||||
}
|
||||
case BillingInterval.Quarter: {
|
||||
let sub = new UTCDate(subMonths(endDate, 3 * intervalCount).getTime());
|
||||
if (isEndOfMonth()) {
|
||||
sub = setToLastDayOfMonth(sub);
|
||||
}
|
||||
let sub = new UTCDate(subMonths(anchorDate, 3 * intervalCount).getTime());
|
||||
sub = setToAnchorDayOrEndOfMonth(sub);
|
||||
sub = preserveTime(sub);
|
||||
return getTime(sub);
|
||||
}
|
||||
case BillingInterval.SemiAnnual: {
|
||||
let sub = new UTCDate(subMonths(endDate, 6 * intervalCount).getTime());
|
||||
if (isEndOfMonth()) {
|
||||
sub = setToLastDayOfMonth(sub);
|
||||
}
|
||||
let sub = new UTCDate(subMonths(anchorDate, 6 * intervalCount).getTime());
|
||||
sub = setToAnchorDayOrEndOfMonth(sub);
|
||||
sub = preserveTime(sub);
|
||||
return getTime(sub);
|
||||
}
|
||||
case BillingInterval.Year: {
|
||||
let sub = new UTCDate(subYears(endDate, 1 * intervalCount).getTime());
|
||||
if (isEndOfMonth()) {
|
||||
sub = setToLastDayOfMonth(sub);
|
||||
}
|
||||
let sub = new UTCDate(subYears(anchorDate, 1 * intervalCount).getTime());
|
||||
sub = setToAnchorDayOrEndOfMonth(sub);
|
||||
sub = preserveTime(sub);
|
||||
return getTime(sub);
|
||||
}
|
||||
@@ -335,8 +330,9 @@ export const subtractIntervalForProration = ({
|
||||
}
|
||||
};
|
||||
|
||||
// Adds an interval to a period start, preserving end-of-month anchoring
|
||||
// e.g. 30 Sep -> 31 Oct (not 30 Oct)
|
||||
// Adds an interval to a period start, preserving anchor-based end-of-month behavior
|
||||
// Uses the anchor date (unixTimestamp) to determine if end-of-month logic should apply
|
||||
// e.g. Sep 30 anchor -> Oct 30, Nov 30, Feb 28/29 (follows Stripe behavior)
|
||||
export const addIntervalForProration = ({
|
||||
unixTimestamp,
|
||||
intervalConfig,
|
||||
@@ -347,68 +343,64 @@ export const addIntervalForProration = ({
|
||||
if (!intervalConfig) return unixTimestamp;
|
||||
let { interval, intervalCount } = intervalConfig;
|
||||
intervalCount = intervalCount ?? 1;
|
||||
const startDate = new UTCDate(unixTimestamp);
|
||||
|
||||
const isEndOfMonth = () => {
|
||||
const lastDay = new UTCDate(
|
||||
startDate.getFullYear(),
|
||||
startDate.getMonth() + 1,
|
||||
0,
|
||||
).getDate();
|
||||
return getDate(startDate) === lastDay;
|
||||
};
|
||||
const anchorDate = new UTCDate(unixTimestamp);
|
||||
const anchorDay = getDate(anchorDate);
|
||||
|
||||
const preserveTime = (d: UTCDate) => {
|
||||
let preserved = new UTCDate(d.getTime());
|
||||
preserved = new UTCDate(setHours(preserved, getHours(startDate)).getTime());
|
||||
preserved = new UTCDate(
|
||||
setMinutes(preserved, getMinutes(startDate)).getTime(),
|
||||
setHours(preserved, getHours(anchorDate)).getTime(),
|
||||
);
|
||||
preserved = new UTCDate(
|
||||
setSeconds(preserved, getSeconds(startDate)).getTime(),
|
||||
setMinutes(preserved, getMinutes(anchorDate)).getTime(),
|
||||
);
|
||||
preserved = new UTCDate(
|
||||
setSeconds(preserved, getSeconds(anchorDate)).getTime(),
|
||||
);
|
||||
return preserved;
|
||||
};
|
||||
|
||||
const setToLastDayOfMonth = (d: UTCDate) => {
|
||||
const last = new UTCDate(d.getFullYear(), d.getMonth() + 1, 0).getDate();
|
||||
return new UTCDate(setDate(d, last).getTime());
|
||||
const setToAnchorDayOrEndOfMonth = (d: UTCDate) => {
|
||||
const lastDayOfTargetMonth = new UTCDate(
|
||||
d.getFullYear(),
|
||||
d.getMonth() + 1,
|
||||
0,
|
||||
).getDate();
|
||||
|
||||
// If the anchor day doesn't exist in the target month, go to the last day of that month
|
||||
// This follows Stripe's behavior: Sep 30 -> Feb 28, Jul 31 -> Feb 28, etc.
|
||||
const targetDay = Math.min(anchorDay, lastDayOfTargetMonth);
|
||||
return new UTCDate(setDate(d, targetDay).getTime());
|
||||
};
|
||||
|
||||
switch (interval) {
|
||||
case BillingInterval.Week: {
|
||||
const add = new UTCDate(addWeeks(startDate, 1 * intervalCount).getTime());
|
||||
const add = new UTCDate(
|
||||
addWeeks(anchorDate, 1 * intervalCount).getTime(),
|
||||
);
|
||||
return getTime(add);
|
||||
}
|
||||
case BillingInterval.Month: {
|
||||
let add = new UTCDate(addMonths(startDate, 1 * intervalCount).getTime());
|
||||
if (isEndOfMonth()) {
|
||||
add = setToLastDayOfMonth(add);
|
||||
}
|
||||
let add = new UTCDate(addMonths(anchorDate, 1 * intervalCount).getTime());
|
||||
add = setToAnchorDayOrEndOfMonth(add);
|
||||
add = preserveTime(add);
|
||||
return getTime(add);
|
||||
}
|
||||
case BillingInterval.Quarter: {
|
||||
let add = new UTCDate(addMonths(startDate, 3 * intervalCount).getTime());
|
||||
if (isEndOfMonth()) {
|
||||
add = setToLastDayOfMonth(add);
|
||||
}
|
||||
let add = new UTCDate(addMonths(anchorDate, 3 * intervalCount).getTime());
|
||||
add = setToAnchorDayOrEndOfMonth(add);
|
||||
add = preserveTime(add);
|
||||
return getTime(add);
|
||||
}
|
||||
case BillingInterval.SemiAnnual: {
|
||||
let add = new UTCDate(addMonths(startDate, 6 * intervalCount).getTime());
|
||||
if (isEndOfMonth()) {
|
||||
add = setToLastDayOfMonth(add);
|
||||
}
|
||||
let add = new UTCDate(addMonths(anchorDate, 6 * intervalCount).getTime());
|
||||
add = setToAnchorDayOrEndOfMonth(add);
|
||||
add = preserveTime(add);
|
||||
return getTime(add);
|
||||
}
|
||||
case BillingInterval.Year: {
|
||||
let add = new UTCDate(addYears(startDate, 1 * intervalCount).getTime());
|
||||
if (isEndOfMonth()) {
|
||||
add = setToLastDayOfMonth(add);
|
||||
}
|
||||
let add = new UTCDate(addYears(anchorDate, 1 * intervalCount).getTime());
|
||||
add = setToAnchorDayOrEndOfMonth(add);
|
||||
add = preserveTime(add);
|
||||
return getTime(add);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { IntervalConfig } from "@autumn/shared";
|
||||
import type { IntervalConfig } from "@autumn/shared";
|
||||
import {
|
||||
addIntervalForProration,
|
||||
subtractIntervalForProration,
|
||||
} from "./billingIntervalUtils.js";
|
||||
import { formatUnixToDate } from "@/utils/genUtils.js";
|
||||
|
||||
export const addIntervalToAnchor = ({
|
||||
intervalConfig,
|
||||
@@ -45,7 +44,7 @@ export const subtractIntervalFromAnchor = ({
|
||||
now = now || Date.now();
|
||||
|
||||
for (let i = 0; i < 50; i++) {
|
||||
let newAnchor = subtractIntervalForProration({
|
||||
const newAnchor = subtractIntervalForProration({
|
||||
unixTimestamp: anchor,
|
||||
interval: intervalConfig.interval,
|
||||
intervalCount: intervalConfig.intervalCount ?? 1,
|
||||
|
||||
@@ -1,27 +1,25 @@
|
||||
import {
|
||||
Feature,
|
||||
FullCustomerPrice,
|
||||
OnIncrease,
|
||||
Organization,
|
||||
Price,
|
||||
Product,
|
||||
UsagePriceConfig,
|
||||
type Feature,
|
||||
type FullCustomerPrice,
|
||||
getFeatureInvoiceDescription,
|
||||
type OnIncrease,
|
||||
type Organization,
|
||||
type Price,
|
||||
type Product,
|
||||
type UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
import { Decimal } from "decimal.js";
|
||||
import { getFeatureInvoiceDescription } from "@autumn/shared";
|
||||
import Stripe from "stripe";
|
||||
import type Stripe from "stripe";
|
||||
import { getCusPaymentMethod } from "@/external/stripe/stripeCusUtils.js";
|
||||
import { payForInvoice } from "@/external/stripe/stripeInvoiceUtils.js";
|
||||
import { constructStripeInvoiceItem } from "@/internal/invoices/invoiceItemUtils/invoiceItemUtils.js";
|
||||
import { createAndFinalizeInvoice } from "@/internal/invoices/invoiceUtils/createAndFinalizeInvoice.js";
|
||||
import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js";
|
||||
import {
|
||||
shouldProrate,
|
||||
shouldBillNow,
|
||||
shouldProrate,
|
||||
} from "@/internal/products/prices/priceUtils/prorationConfigUtils.js";
|
||||
import { formatUnixToDate } from "@/utils/genUtils.js";
|
||||
import { getStripeNow } from "@/utils/scriptUtils/testClockUtils.js";
|
||||
import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
|
||||
|
||||
export const getUpgradeProrationInvoiceItem = ({
|
||||
prevPrice,
|
||||
@@ -68,12 +66,12 @@ export const getUpgradeProrationInvoiceItem = ({
|
||||
amount: invoiceAmount,
|
||||
});
|
||||
|
||||
let start = formatUnixToDate(now);
|
||||
let end = formatUnixToDate(subItem.current_period_end * 1000);
|
||||
const start = formatUnixToDate(now);
|
||||
const end = formatUnixToDate(subItem.current_period_end * 1000);
|
||||
invoiceDescription = `${invoiceDescription} (from ${start} to ${end})`;
|
||||
}
|
||||
|
||||
let invoiceItem = constructStripeInvoiceItem({
|
||||
const invoiceItem = constructStripeInvoiceItem({
|
||||
product,
|
||||
amount: invoiceAmount,
|
||||
org,
|
||||
@@ -117,14 +115,14 @@ export const createUpgradeProrationInvoice = async ({
|
||||
onIncrease: OnIncrease;
|
||||
logger: any;
|
||||
}) => {
|
||||
let now = await getStripeNow({ stripeCli, stripeSub: sub });
|
||||
const now = await getStripeNow({ stripeCli, stripeSub: sub });
|
||||
|
||||
const paymentMethod = await getCusPaymentMethod({
|
||||
stripeCli,
|
||||
stripeId: sub.customer as string,
|
||||
});
|
||||
|
||||
let invoiceItem = getUpgradeProrationInvoiceItem({
|
||||
const invoiceItem = getUpgradeProrationInvoiceItem({
|
||||
prevPrice,
|
||||
newPrice,
|
||||
now,
|
||||
@@ -138,12 +136,12 @@ export const createUpgradeProrationInvoice = async ({
|
||||
subItem,
|
||||
});
|
||||
|
||||
let invoiceAmount =
|
||||
const invoiceAmount =
|
||||
invoiceItem?.amount || invoiceItem?.price_data?.unit_amount || 0;
|
||||
|
||||
let invoiceDescription = invoiceItem?.description || "";
|
||||
const invoiceDescription = invoiceItem?.description || "";
|
||||
|
||||
if (invoiceAmount == 0) return;
|
||||
if (invoiceAmount === 0) return;
|
||||
|
||||
logger.info(
|
||||
`🚀 Creating invoice item: ${invoiceDescription} - ${invoiceAmount.toFixed(2)}`,
|
||||
|
||||
@@ -1,34 +1,31 @@
|
||||
import {
|
||||
FullCusEntWithFullCusProduct,
|
||||
FullCustomerPrice,
|
||||
InsertReplaceable,
|
||||
type Feature,
|
||||
type FullCusEntWithFullCusProduct,
|
||||
type FullCustomerPrice,
|
||||
getFeatureInvoiceDescription,
|
||||
type InsertReplaceable,
|
||||
OnDecrease,
|
||||
} from "@autumn/shared";
|
||||
import Stripe from "stripe";
|
||||
|
||||
import {
|
||||
Feature,
|
||||
Organization,
|
||||
Product,
|
||||
UsagePriceConfig,
|
||||
type Organization,
|
||||
type Product,
|
||||
type UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
import { Decimal } from "decimal.js";
|
||||
import { getFeatureInvoiceDescription } from "@autumn/shared";
|
||||
import type Stripe from "stripe";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { RepService } from "@/internal/customers/cusProducts/cusEnts/RepService.js";
|
||||
import { constructStripeInvoiceItem } from "@/internal/invoices/invoiceItemUtils/invoiceItemUtils.js";
|
||||
import { createAndFinalizeInvoice } from "@/internal/invoices/invoiceUtils/createAndFinalizeInvoice.js";
|
||||
import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js";
|
||||
import { formatUnixToDate } from "@/utils/genUtils.js";
|
||||
import { getStripeNow } from "@/utils/scriptUtils/testClockUtils.js";
|
||||
import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { getUsageFromBalance } from "../adjustAllowance.js";
|
||||
import { roundUsage } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
|
||||
import { getReplaceables } from "@/internal/products/prices/priceUtils/arrearProratedUtils/getContUsageDowngradeItem.js";
|
||||
import { RepService } from "@/internal/customers/cusProducts/cusEnts/RepService.js";
|
||||
import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js";
|
||||
import {
|
||||
shouldBillNow,
|
||||
shouldProrate,
|
||||
} from "@/internal/products/prices/priceUtils/prorationConfigUtils.js";
|
||||
import { roundUsage } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
|
||||
import { formatUnixToDate } from "@/utils/genUtils.js";
|
||||
import { getStripeNow } from "@/utils/scriptUtils/testClockUtils.js";
|
||||
import { getUsageFromBalance } from "../adjustAllowance.js";
|
||||
|
||||
export const createDowngradeProrationInvoice = async ({
|
||||
org,
|
||||
@@ -59,7 +56,7 @@ export const createDowngradeProrationInvoice = async ({
|
||||
}) => {
|
||||
const config = cusPrice.price.config as UsagePriceConfig;
|
||||
|
||||
let now = await getStripeNow({ stripeCli, stripeSub: sub });
|
||||
const now = await getStripeNow({ stripeCli, stripeSub: sub });
|
||||
let invoiceAmount = new Decimal(newPrice).minus(prevPrice).toNumber();
|
||||
|
||||
logger.info(`Prev price: ${prevPrice}, New price: ${newPrice}`);
|
||||
@@ -80,11 +77,11 @@ export const createDowngradeProrationInvoice = async ({
|
||||
allowNegative: true,
|
||||
});
|
||||
|
||||
let start = formatUnixToDate(now);
|
||||
let end = formatUnixToDate(subItem.current_period_end * 1000);
|
||||
const start = formatUnixToDate(now);
|
||||
const end = formatUnixToDate(subItem.current_period_end * 1000);
|
||||
invoiceDescription = `${invoiceDescription} (from ${start} to ${end})`;
|
||||
|
||||
if (invoiceAmount == 0) return;
|
||||
if (invoiceAmount === 0) return;
|
||||
|
||||
logger.info(
|
||||
`🚀 Creating invoice item: ${invoiceDescription} - ${invoiceAmount.toFixed(2)}`,
|
||||
@@ -157,7 +154,7 @@ export const handleProratedDowngrade = async ({
|
||||
balance: newBalance,
|
||||
});
|
||||
|
||||
let onDecrease =
|
||||
const onDecrease =
|
||||
cusPrice.price.proration_config?.on_decrease ||
|
||||
OnDecrease.ProrateImmediately;
|
||||
|
||||
@@ -167,9 +164,9 @@ export const handleProratedDowngrade = async ({
|
||||
let invoice = null;
|
||||
let newReplaceables: InsertReplaceable[] = [];
|
||||
|
||||
if (onDecrease == OnDecrease.NoProrations) {
|
||||
if (onDecrease === OnDecrease.NoProrations || sub.status === "trialing") {
|
||||
} else if (shouldProrate(onDecrease)) {
|
||||
let prevPrice = priceToInvoiceAmount({
|
||||
const prevPrice = priceToInvoiceAmount({
|
||||
price: cusPrice.price,
|
||||
overage: roundUsage({
|
||||
usage: prevOverage,
|
||||
@@ -177,7 +174,7 @@ export const handleProratedDowngrade = async ({
|
||||
}),
|
||||
});
|
||||
|
||||
let newPrice = priceToInvoiceAmount({
|
||||
const newPrice = priceToInvoiceAmount({
|
||||
price: cusPrice.price,
|
||||
overage: roundUsage({
|
||||
usage: newOverage,
|
||||
@@ -217,10 +214,10 @@ export const handleProratedDowngrade = async ({
|
||||
}
|
||||
}
|
||||
|
||||
let numDeletedReplaceables = cusEnt.replaceables.filter(
|
||||
const numDeletedReplaceables = cusEnt.replaceables.filter(
|
||||
(r) => r.delete_next_cycle,
|
||||
).length;
|
||||
let newQuantity = newUsage - numDeletedReplaceables;
|
||||
const newQuantity = newUsage - numDeletedReplaceables;
|
||||
|
||||
await stripeCli.subscriptionItems.update(subItem.id, {
|
||||
quantity: roundUsage({
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
import {
|
||||
type Entitlement,
|
||||
type FullCusEntWithFullCusProduct,
|
||||
type FullCustomerPrice,
|
||||
OnIncrease,
|
||||
type Organization,
|
||||
type Price,
|
||||
type UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
import type Stripe from "stripe";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { RepService } from "@/internal/customers/cusProducts/cusEnts/RepService.js";
|
||||
import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js";
|
||||
import { shouldCreateInvoiceItem } from "@/internal/products/prices/priceUtils/prorationConfigUtils.js";
|
||||
|
||||
import {
|
||||
Entitlement,
|
||||
FullCusEntWithFullCusProduct,
|
||||
FullCusEntWithProduct,
|
||||
FullCustomerPrice,
|
||||
OnIncrease,
|
||||
Organization,
|
||||
Price,
|
||||
UsagePriceConfig,
|
||||
} from "@autumn/shared";
|
||||
|
||||
import Stripe from "stripe";
|
||||
import { createUpgradeProrationInvoice } from "./createUpgradeProrationInvoice.js";
|
||||
import { getUsageFromBalance } from "../adjustAllowance.js";
|
||||
import { RepService } from "@/internal/customers/cusProducts/cusEnts/RepService.js";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { roundUsage } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
|
||||
import { getUsageFromBalance } from "../adjustAllowance.js";
|
||||
import { createUpgradeProrationInvoice } from "./createUpgradeProrationInvoice.js";
|
||||
|
||||
interface UsageValues {
|
||||
prevRoundedUsage: number;
|
||||
@@ -53,7 +50,7 @@ export const getPrevAndNewPriceForUpgrade = ({
|
||||
balance: newBalance,
|
||||
});
|
||||
|
||||
let prevPrice = priceToInvoiceAmount({
|
||||
const prevPrice = priceToInvoiceAmount({
|
||||
price,
|
||||
overage: roundUsage({
|
||||
// usage: prevUsage,
|
||||
@@ -62,7 +59,7 @@ export const getPrevAndNewPriceForUpgrade = ({
|
||||
}),
|
||||
});
|
||||
|
||||
let newPrice = priceToInvoiceAmount({
|
||||
const newPrice = priceToInvoiceAmount({
|
||||
price,
|
||||
overage: roundUsage({
|
||||
// usage: newUsage,
|
||||
@@ -90,8 +87,8 @@ export function getReps({
|
||||
prevBalance: number;
|
||||
newBalance: number;
|
||||
}) {
|
||||
let usageDiff = prevBalance - newBalance;
|
||||
let reps = cusEnt.replaceables.slice(0, usageDiff);
|
||||
const usageDiff = prevBalance - newBalance;
|
||||
const reps = cusEnt.replaceables.slice(0, usageDiff);
|
||||
return reps;
|
||||
}
|
||||
|
||||
@@ -121,14 +118,14 @@ export const handleProratedUpgrade = async ({
|
||||
logger.info(`Handling quantity increase`);
|
||||
|
||||
// 1. Get num reps to use
|
||||
let reps = getReps({
|
||||
const reps = getReps({
|
||||
cusEnt,
|
||||
prevBalance,
|
||||
newBalance,
|
||||
});
|
||||
newBalance = newBalance + reps.length; // Increase new balance by number of reps
|
||||
|
||||
let { prevPrice, newPrice, newUsage } = getPrevAndNewPriceForUpgrade({
|
||||
const { prevPrice, newPrice, newUsage } = getPrevAndNewPriceForUpgrade({
|
||||
ent: cusEnt.entitlement,
|
||||
price: cusPrice.price,
|
||||
newBalance,
|
||||
@@ -140,7 +137,7 @@ export const handleProratedUpgrade = async ({
|
||||
const product = cusEnt.customer_product.product;
|
||||
const feature = cusEnt.entitlement.feature;
|
||||
|
||||
let onIncrease =
|
||||
const onIncrease =
|
||||
cusPrice.price.proration_config?.on_increase ||
|
||||
OnIncrease.ProrateImmediately;
|
||||
|
||||
@@ -150,7 +147,7 @@ export const handleProratedUpgrade = async ({
|
||||
});
|
||||
|
||||
let invoice = null;
|
||||
if (shouldCreateInvoiceItem(onIncrease)) {
|
||||
if (shouldCreateInvoiceItem(onIncrease) && sub.status !== "trialing") {
|
||||
invoice = await createUpgradeProrationInvoice({
|
||||
org,
|
||||
cusPrice,
|
||||
@@ -168,7 +165,7 @@ export const handleProratedUpgrade = async ({
|
||||
});
|
||||
}
|
||||
|
||||
let deleted = await RepService.deleteInIds({
|
||||
const deleted = await RepService.deleteInIds({
|
||||
db,
|
||||
ids: reps.map((r) => r.id),
|
||||
});
|
||||
|
||||
@@ -1,39 +1,39 @@
|
||||
import { APIVersion, type AppEnv, type Organization } from "@autumn/shared";
|
||||
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, Organization } from "@autumn/shared";
|
||||
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { advanceTestClock } from "tests/utils/stripeUtils.js";
|
||||
import { addWeeks } from "date-fns";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import type { Stripe } from "stripe";
|
||||
import { setupBefore } from "tests/before.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
|
||||
import { createProducts } from "tests/utils/productUtils.js";
|
||||
import { advanceTestClock } from "tests/utils/stripeUtils.js";
|
||||
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||
|
||||
// UNCOMMENT FROM HERE
|
||||
let pro = constructProduct({
|
||||
const pro = constructProduct({
|
||||
id: "pro",
|
||||
items: [constructArrearItem({ featureId: TestFeature.Words })],
|
||||
type: "pro",
|
||||
});
|
||||
let premium = constructProduct({
|
||||
const premium = constructProduct({
|
||||
id: "premium",
|
||||
items: [constructArrearItem({ featureId: TestFeature.Words })],
|
||||
type: "premium",
|
||||
});
|
||||
let growth = constructProduct({
|
||||
const growth = constructProduct({
|
||||
id: "growth",
|
||||
items: [constructArrearItem({ featureId: TestFeature.Words })],
|
||||
type: "growth",
|
||||
});
|
||||
|
||||
describe(`${chalk.yellowBright("upgrade1: Testing usage upgrades")}`, () => {
|
||||
let customerId = "upgrade1";
|
||||
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
|
||||
const customerId = "upgrade1";
|
||||
const autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
|
||||
|
||||
let stripeCli: Stripe;
|
||||
let testClockId: string;
|
||||
@@ -77,7 +77,7 @@ describe(`${chalk.yellowBright("upgrade1: Testing usage upgrades")}`, () => {
|
||||
testClockId = testClockId1!;
|
||||
});
|
||||
|
||||
it("should attach pro product", async function () {
|
||||
it("should attach pro product", async () => {
|
||||
await attachAndExpectCorrect({
|
||||
autumn,
|
||||
customerId,
|
||||
@@ -89,7 +89,7 @@ describe(`${chalk.yellowBright("upgrade1: Testing usage upgrades")}`, () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should attach premium product", async function () {
|
||||
it("should attach premium product", async () => {
|
||||
const wordsUsage = 100000;
|
||||
await autumn.track({
|
||||
customer_id: customerId,
|
||||
@@ -115,7 +115,7 @@ describe(`${chalk.yellowBright("upgrade1: Testing usage upgrades")}`, () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should attach growth product", async function () {
|
||||
it("should attach growth product", async () => {
|
||||
const wordsUsage = 200000;
|
||||
await autumn.track({
|
||||
customer_id: customerId,
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||
import {
|
||||
APIVersion,
|
||||
AppEnv,
|
||||
type AppEnv,
|
||||
OnDecrease,
|
||||
OnIncrease,
|
||||
Organization,
|
||||
type Organization,
|
||||
} from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import Stripe from "stripe";
|
||||
import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { addWeeks } from "date-fns";
|
||||
import type Stripe from "stripe";
|
||||
import { setupBefore } from "tests/before.js";
|
||||
import { createProducts } from "tests/utils/productUtils.js";
|
||||
import { addPrefixToProducts } from "../../attach/utils.js";
|
||||
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { TestFeature } from "tests/setup/v2Features.js";
|
||||
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
|
||||
import {
|
||||
calcProrationAndExpectInvoice,
|
||||
expectSubQuantityCorrect,
|
||||
} from "tests/utils/expectUtils/expectContUseUtils.js";
|
||||
import { addWeeks } from "date-fns";
|
||||
import { createProducts } from "tests/utils/productUtils.js";
|
||||
import { advanceTestClock } from "tests/utils/stripeUtils.js";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { timeout } from "@/utils/genUtils.js";
|
||||
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||
import { addPrefixToProducts } from "../../attach/utils.js";
|
||||
|
||||
let userItem = constructArrearProratedItem({
|
||||
const userItem = constructArrearProratedItem({
|
||||
featureId: TestFeature.Users,
|
||||
pricePerUnit: 50,
|
||||
includedUsage: 1,
|
||||
@@ -35,7 +35,7 @@ let userItem = constructArrearProratedItem({
|
||||
},
|
||||
});
|
||||
|
||||
export let pro = constructProduct({
|
||||
const pro = constructProduct({
|
||||
items: [userItem],
|
||||
type: "pro",
|
||||
});
|
||||
@@ -43,8 +43,8 @@ export let pro = constructProduct({
|
||||
const testCase = "entity2";
|
||||
|
||||
describe(`${chalk.yellowBright(`contUse/${testCase}: Testing entities, prorate now`)}`, () => {
|
||||
let customerId = testCase;
|
||||
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
|
||||
const customerId = testCase;
|
||||
const autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
|
||||
let testClockId: string;
|
||||
let db: DrizzleCli, org: Organization, env: AppEnv;
|
||||
let stripeCli: Stripe;
|
||||
@@ -86,7 +86,7 @@ describe(`${chalk.yellowBright(`contUse/${testCase}: Testing entities, prorate n
|
||||
});
|
||||
|
||||
let usage = 0;
|
||||
let firstEntities = [
|
||||
const firstEntities = [
|
||||
{
|
||||
id: "1",
|
||||
name: "test",
|
||||
@@ -94,7 +94,7 @@ describe(`${chalk.yellowBright(`contUse/${testCase}: Testing entities, prorate n
|
||||
},
|
||||
];
|
||||
|
||||
it("should create entity, then attach pro", async function () {
|
||||
it("should create entity, then attach pro", async () => {
|
||||
await autumn.entities.create(customerId, firstEntities);
|
||||
usage += 1;
|
||||
|
||||
@@ -128,7 +128,7 @@ describe(`${chalk.yellowBright(`contUse/${testCase}: Testing entities, prorate n
|
||||
},
|
||||
];
|
||||
|
||||
it("should create 2 entities and have correct invoice", async function () {
|
||||
it("should create 2 entities and have correct invoice", async () => {
|
||||
curUnix = await advanceTestClock({
|
||||
stripeCli,
|
||||
testClockId,
|
||||
@@ -150,6 +150,8 @@ describe(`${chalk.yellowBright(`contUse/${testCase}: Testing entities, prorate n
|
||||
itemQuantity: usage,
|
||||
});
|
||||
|
||||
await timeout(5000);
|
||||
|
||||
await calcProrationAndExpectInvoice({
|
||||
autumn,
|
||||
stripeSubs,
|
||||
@@ -161,7 +163,7 @@ describe(`${chalk.yellowBright(`contUse/${testCase}: Testing entities, prorate n
|
||||
});
|
||||
});
|
||||
|
||||
it("should delete 1 entity and have correct invoice amount", async function () {
|
||||
it("should delete 1 entity and have correct invoice amount", async () => {
|
||||
curUnix = await advanceTestClock({
|
||||
stripeCli,
|
||||
testClockId,
|
||||
|
||||
410
server/tests/interval/intervalUtils1.test.ts
Normal file
410
server/tests/interval/intervalUtils1.test.ts
Normal file
@@ -0,0 +1,410 @@
|
||||
import { BillingInterval } from "@autumn/shared";
|
||||
import { UTCDate } from "@date-fns/utc";
|
||||
import { expect } from "chai";
|
||||
import { describe, it } from "mocha";
|
||||
import {
|
||||
addIntervalForProration,
|
||||
subtractIntervalForProration,
|
||||
} from "@/internal/products/prices/billingIntervalUtils.js";
|
||||
|
||||
describe("Billing Interval Utils - Stripe-like behavior", () => {
|
||||
describe("addIntervalForProration", () => {
|
||||
describe("Sep 30 anchor", () => {
|
||||
const sep30_2024 = new UTCDate("2024-09-30T12:00:00Z").getTime();
|
||||
|
||||
it("Sep 30 -> Oct 30", () => {
|
||||
const result = addIntervalForProration({
|
||||
unixTimestamp: sep30_2024,
|
||||
intervalConfig: { interval: BillingInterval.Month, intervalCount: 1 },
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2024-10-30",
|
||||
);
|
||||
});
|
||||
|
||||
it("Sep 30 -> Nov 30", () => {
|
||||
const result = addIntervalForProration({
|
||||
unixTimestamp: sep30_2024,
|
||||
intervalConfig: { interval: BillingInterval.Month, intervalCount: 2 },
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2024-11-30",
|
||||
);
|
||||
});
|
||||
|
||||
it("Sep 30 -> Dec 30", () => {
|
||||
const result = addIntervalForProration({
|
||||
unixTimestamp: sep30_2024,
|
||||
intervalConfig: { interval: BillingInterval.Month, intervalCount: 3 },
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2024-12-30",
|
||||
);
|
||||
});
|
||||
|
||||
it("Sep 30 -> Jan 30 (next year)", () => {
|
||||
const result = addIntervalForProration({
|
||||
unixTimestamp: sep30_2024,
|
||||
intervalConfig: { interval: BillingInterval.Month, intervalCount: 4 },
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2025-01-30",
|
||||
);
|
||||
});
|
||||
|
||||
it("Sep 30 -> Feb 28 (next year, non-leap)", () => {
|
||||
const result = addIntervalForProration({
|
||||
unixTimestamp: sep30_2024,
|
||||
intervalConfig: { interval: BillingInterval.Month, intervalCount: 5 },
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2025-02-28",
|
||||
);
|
||||
});
|
||||
|
||||
it("Sep 30 -> Feb 29 (leap year)", () => {
|
||||
const sep30_2023 = new UTCDate("2023-09-30T12:00:00Z").getTime();
|
||||
const result = addIntervalForProration({
|
||||
unixTimestamp: sep30_2023,
|
||||
intervalConfig: { interval: BillingInterval.Month, intervalCount: 5 },
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2024-02-29",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Aug 31 anchor", () => {
|
||||
const aug31_2024 = new UTCDate("2024-08-31T12:00:00Z").getTime();
|
||||
|
||||
it("Aug 31 -> Sep 30", () => {
|
||||
const result = addIntervalForProration({
|
||||
unixTimestamp: aug31_2024,
|
||||
intervalConfig: { interval: BillingInterval.Month, intervalCount: 1 },
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2024-09-30",
|
||||
);
|
||||
});
|
||||
|
||||
it("Aug 31 -> Oct 31", () => {
|
||||
const result = addIntervalForProration({
|
||||
unixTimestamp: aug31_2024,
|
||||
intervalConfig: { interval: BillingInterval.Month, intervalCount: 2 },
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2024-10-31",
|
||||
);
|
||||
});
|
||||
|
||||
it("Aug 31 -> Nov 30", () => {
|
||||
const result = addIntervalForProration({
|
||||
unixTimestamp: aug31_2024,
|
||||
intervalConfig: { interval: BillingInterval.Month, intervalCount: 3 },
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2024-11-30",
|
||||
);
|
||||
});
|
||||
|
||||
it("Aug 31 -> Dec 31", () => {
|
||||
const result = addIntervalForProration({
|
||||
unixTimestamp: aug31_2024,
|
||||
intervalConfig: { interval: BillingInterval.Month, intervalCount: 4 },
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2024-12-31",
|
||||
);
|
||||
});
|
||||
|
||||
it("Aug 31 -> Jan 31 (next year)", () => {
|
||||
const result = addIntervalForProration({
|
||||
unixTimestamp: aug31_2024,
|
||||
intervalConfig: { interval: BillingInterval.Month, intervalCount: 5 },
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2025-01-31",
|
||||
);
|
||||
});
|
||||
|
||||
it("Aug 31 -> Feb 28 (next year, non-leap)", () => {
|
||||
const result = addIntervalForProration({
|
||||
unixTimestamp: aug31_2024,
|
||||
intervalConfig: { interval: BillingInterval.Month, intervalCount: 6 },
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2025-02-28",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Time preservation", () => {
|
||||
it("preserves time components", () => {
|
||||
const sep30_1430 = new UTCDate("2024-09-30T14:30:45Z").getTime();
|
||||
const result = addIntervalForProration({
|
||||
unixTimestamp: sep30_1430,
|
||||
intervalConfig: { interval: BillingInterval.Month, intervalCount: 1 },
|
||||
});
|
||||
const resultDate = new UTCDate(result);
|
||||
expect(resultDate.toISOString()).to.equal("2024-10-30T14:30:45.000Z");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("subtractIntervalForProration", () => {
|
||||
describe("Oct 30 anchor (going backwards to Sep 30)", () => {
|
||||
const oct30_2024 = new UTCDate("2024-10-30T12:00:00Z").getTime();
|
||||
|
||||
it("Oct 30 -> Sep 30", () => {
|
||||
const result = subtractIntervalForProration({
|
||||
unixTimestamp: oct30_2024,
|
||||
interval: BillingInterval.Month,
|
||||
intervalCount: 1,
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2024-09-30",
|
||||
);
|
||||
});
|
||||
|
||||
it("Oct 30 -> Aug 30", () => {
|
||||
const result = subtractIntervalForProration({
|
||||
unixTimestamp: oct30_2024,
|
||||
interval: BillingInterval.Month,
|
||||
intervalCount: 2,
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2024-08-30",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Feb 28 anchor (going backwards)", () => {
|
||||
const feb28_2025 = new UTCDate("2025-02-28T12:00:00Z").getTime();
|
||||
|
||||
it("Feb 28 -> Jan 28", () => {
|
||||
const result = subtractIntervalForProration({
|
||||
unixTimestamp: feb28_2025,
|
||||
interval: BillingInterval.Month,
|
||||
intervalCount: 1,
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2025-01-28",
|
||||
);
|
||||
});
|
||||
|
||||
it("Feb 28 -> Dec 28 (previous year)", () => {
|
||||
const result = subtractIntervalForProration({
|
||||
unixTimestamp: feb28_2025,
|
||||
interval: BillingInterval.Month,
|
||||
intervalCount: 2,
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2024-12-28",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sep 30 anchor (going backwards from Sep 30)", () => {
|
||||
const sep30_2024 = new UTCDate("2024-09-30T12:00:00Z").getTime();
|
||||
|
||||
it("Sep 30 -> Aug 30", () => {
|
||||
const result = subtractIntervalForProration({
|
||||
unixTimestamp: sep30_2024,
|
||||
interval: BillingInterval.Month,
|
||||
intervalCount: 1,
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2024-08-30",
|
||||
);
|
||||
});
|
||||
|
||||
it("Sep 30 -> Jul 30", () => {
|
||||
const result = subtractIntervalForProration({
|
||||
unixTimestamp: sep30_2024,
|
||||
interval: BillingInterval.Month,
|
||||
intervalCount: 2,
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2024-07-30",
|
||||
);
|
||||
});
|
||||
|
||||
it("Sep 30 -> Jun 30", () => {
|
||||
const result = subtractIntervalForProration({
|
||||
unixTimestamp: sep30_2024,
|
||||
interval: BillingInterval.Month,
|
||||
intervalCount: 3,
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2024-06-30",
|
||||
);
|
||||
});
|
||||
|
||||
it("Sep 30 -> May 30", () => {
|
||||
const result = subtractIntervalForProration({
|
||||
unixTimestamp: sep30_2024,
|
||||
interval: BillingInterval.Month,
|
||||
intervalCount: 4,
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2024-05-30",
|
||||
);
|
||||
});
|
||||
|
||||
it("Sep 30 -> Apr 30", () => {
|
||||
const result = subtractIntervalForProration({
|
||||
unixTimestamp: sep30_2024,
|
||||
interval: BillingInterval.Month,
|
||||
intervalCount: 5,
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2024-04-30",
|
||||
);
|
||||
});
|
||||
|
||||
it("Sep 30 -> Mar 30", () => {
|
||||
const result = subtractIntervalForProration({
|
||||
unixTimestamp: sep30_2024,
|
||||
interval: BillingInterval.Month,
|
||||
intervalCount: 6,
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2024-03-30",
|
||||
);
|
||||
});
|
||||
|
||||
it("Sep 30 -> Feb 29 (leap year)", () => {
|
||||
const result = subtractIntervalForProration({
|
||||
unixTimestamp: sep30_2024,
|
||||
interval: BillingInterval.Month,
|
||||
intervalCount: 7,
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2024-02-29",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Jan 31 anchor (going backwards from Jan 31)", () => {
|
||||
const jan31_2025 = new UTCDate("2025-01-31T12:00:00Z").getTime();
|
||||
|
||||
it("Jan 31 -> Dec 31 (previous year)", () => {
|
||||
const result = subtractIntervalForProration({
|
||||
unixTimestamp: jan31_2025,
|
||||
interval: BillingInterval.Month,
|
||||
intervalCount: 1,
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2024-12-31",
|
||||
);
|
||||
});
|
||||
|
||||
it("Jan 31 -> Nov 30 (previous year)", () => {
|
||||
const result = subtractIntervalForProration({
|
||||
unixTimestamp: jan31_2025,
|
||||
interval: BillingInterval.Month,
|
||||
intervalCount: 2,
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2024-11-30",
|
||||
);
|
||||
});
|
||||
|
||||
it("Jan 31 -> Oct 31 (previous year)", () => {
|
||||
const result = subtractIntervalForProration({
|
||||
unixTimestamp: jan31_2025,
|
||||
interval: BillingInterval.Month,
|
||||
intervalCount: 3,
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2024-10-31",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Time preservation", () => {
|
||||
it("preserves time components", () => {
|
||||
const oct30_1430 = new UTCDate("2024-10-30T14:30:45Z").getTime();
|
||||
const result = subtractIntervalForProration({
|
||||
unixTimestamp: oct30_1430,
|
||||
interval: BillingInterval.Month,
|
||||
intervalCount: 1,
|
||||
});
|
||||
const resultDate = new UTCDate(result);
|
||||
expect(resultDate.toISOString()).to.equal("2024-09-30T14:30:45.000Z");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge cases", () => {
|
||||
describe("Leap year handling", () => {
|
||||
it("Feb 29 -> Mar 29 (leap year)", () => {
|
||||
const feb29_2024 = new UTCDate("2024-02-29T12:00:00Z").getTime();
|
||||
const result = addIntervalForProration({
|
||||
unixTimestamp: feb29_2024,
|
||||
intervalConfig: { interval: BillingInterval.Month, intervalCount: 1 },
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2024-03-29",
|
||||
);
|
||||
});
|
||||
|
||||
it("Feb 29 -> Feb 28 (next year, non-leap)", () => {
|
||||
const feb29_2024 = new UTCDate("2024-02-29T12:00:00Z").getTime();
|
||||
const result = addIntervalForProration({
|
||||
unixTimestamp: feb29_2024,
|
||||
intervalConfig: {
|
||||
interval: BillingInterval.Month,
|
||||
intervalCount: 12,
|
||||
},
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2025-02-28",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Quarterly intervals", () => {
|
||||
it("Jan 31 -> Apr 30 (quarterly)", () => {
|
||||
const jan31_2024 = new UTCDate("2024-01-31T12:00:00Z").getTime();
|
||||
const result = addIntervalForProration({
|
||||
unixTimestamp: jan31_2024,
|
||||
intervalConfig: {
|
||||
interval: BillingInterval.Quarter,
|
||||
intervalCount: 1,
|
||||
},
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2024-04-30",
|
||||
);
|
||||
});
|
||||
|
||||
it("Oct 31 -> Jan 31 (quarterly)", () => {
|
||||
const oct31_2024 = new UTCDate("2024-10-31T12:00:00Z").getTime();
|
||||
const result = addIntervalForProration({
|
||||
unixTimestamp: oct31_2024,
|
||||
intervalConfig: {
|
||||
interval: BillingInterval.Quarter,
|
||||
intervalCount: 1,
|
||||
},
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2025-01-31",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Yearly intervals", () => {
|
||||
it("Feb 29 -> Feb 28 (yearly, non-leap to leap)", () => {
|
||||
const feb29_2024 = new UTCDate("2024-02-29T12:00:00Z").getTime();
|
||||
const result = addIntervalForProration({
|
||||
unixTimestamp: feb29_2024,
|
||||
intervalConfig: { interval: BillingInterval.Year, intervalCount: 1 },
|
||||
});
|
||||
expect(new UTCDate(result).toISOString().slice(0, 10)).to.equal(
|
||||
"2025-02-28",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user