Merge branch 'staging' into feat/stainless

This commit is contained in:
John Yeo
2025-10-01 11:50:39 +01:00
41 changed files with 943 additions and 690 deletions

80
conductor-setup.sh Normal file
View File

@@ -0,0 +1,80 @@
#!/bin/zsh
set -e
echo "🚀 Starting Conductor workspace setup for Autumn..."
# Check for Bun
if ! command -v bun &> /dev/null; then
echo "❌ Error: Bun is not installed."
echo "Please install Bun from https://bun.sh"
exit 1
fi
echo "✅ Bun found: $(bun --version)"
# Determine root path - use CONDUCTOR_ROOT_PATH if set, otherwise use git root
if [ -n "$CONDUCTOR_ROOT_PATH" ]; then
ROOT_PATH="$CONDUCTOR_ROOT_PATH"
else
# Fallback for manual testing - go up two directories from .conductor/workspace
ROOT_PATH="$(cd "$(dirname "$0")/../.." && pwd)"
fi
echo "📂 Root path: $ROOT_PATH"
# Install dependencies
echo "📦 Installing dependencies..."
bun install
# Copy .env files from root repo
echo "📋 Copying .env files from root repository..."
if [ -f "$ROOT_PATH/server/.env" ]; then
mkdir -p server
cp "$ROOT_PATH/server/.env" server/.env
echo "✅ Copied server/.env"
else
echo "⚠️ Warning: $ROOT_PATH/server/.env not found"
fi
if [ -f "$ROOT_PATH/vite/.env" ]; then
mkdir -p vite
cp "$ROOT_PATH/vite/.env" vite/.env
echo "✅ Copied vite/.env"
else
echo "⚠️ Warning: $ROOT_PATH/vite/.env not found"
fi
if [ -f "$ROOT_PATH/shared/.env" ]; then
mkdir -p shared
cp "$ROOT_PATH/shared/.env" shared/.env
echo "✅ Copied shared/.env"
else
echo "⚠️ Warning: $ROOT_PATH/shared/.env not found"
fi
# Copy drizzle migration files
if [ -d "$ROOT_PATH/shared/drizzle" ]; then
echo "📋 Copying database migration files..."
mkdir -p shared/drizzle
cp -r "$ROOT_PATH/shared/drizzle/"* shared/drizzle/
echo "✅ Copied migration files"
fi
# Build shared workspace (required for other workspaces)
echo "🔨 Building shared workspace..."
bun -F @autumn/shared build
# Run database migrations if DATABASE_URL exists
if grep -q "DATABASE_URL=" server/.env 2>/dev/null; then
echo "🗄️ Running database migrations..."
bun db:migrate
echo "✅ Database migrations complete"
else
echo "⚠️ Skipping database migrations (no DATABASE_URL found)"
fi
echo "🎉 Workspace setup complete!"
echo ""
echo "Next: Click the 'Run' button to start the development server"

6
conductor.json Normal file
View File

@@ -0,0 +1,6 @@
{
"scripts": {
"setup": "zsh conductor-setup.sh",
"run": "bun run dev:bun"
}
}

View File

@@ -1,4 +1,4 @@
#!/bin/bash
MOCHA_SETUP="bunx mocha tests/00_setup.ts"
MOCHA_CMD="bunx mocha --parallel --timeout 10000000 --ignore tests/00_setup.ts"
MOCHA_CMD="bunx mocha --parallel -j 6 --timeout 10000000 --ignore tests/00_setup.ts"

View File

@@ -12,12 +12,12 @@ fi
$MOCHA_CMD \
'tests/attach/basic/*.ts' \
'tests/attach/upgrade/*.ts' \
'tests/attach/downgrade/*.ts' \
'tests/attach/addOn/*.ts'
'tests/attach/downgrade/*.ts'
$MOCHA_CMD \
'tests/attach/checkout/*.ts' \
'tests/attach/entities/*.ts' \
'tests/attach/free/*.ts'\
'tests/attach/addOn/*.ts' \

View File

@@ -1,23 +1,23 @@
import {
BillingInterval,
BillingType,
type EntitlementWithFeature,
type Organization,
type Price,
type Product,
TierInfinite,
type UsagePriceConfig,
} from "@autumn/shared";
import { Decimal } from "decimal.js";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { PriceService } from "@/internal/products/prices/PriceService.js";
import {
getBillingType,
getPriceEntitlement,
} from "@/internal/products/prices/priceUtils.js";
import {
Price,
UsagePriceConfig,
TierInfinite,
EntitlementWithFeature,
Organization,
Product,
BillingInterval,
BillingType,
} from "@autumn/shared";
import Stripe from "stripe";
import { billingIntervalToStripe } from "../stripePriceUtils.js";
import { priceToInArrearTiers } from "./createStripeInArrear.js";
import { PriceService } from "@/internal/products/prices/PriceService.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { Decimal } from "decimal.js";
export interface StripeMeteredPriceParams {
db: DrizzleCli;
@@ -54,16 +54,20 @@ export const createStripeMeteredPrice = async ({
limit: 100,
status: "active",
});
meter = meters.data.find((m) => m.event_name == price.id!);
meter = meters.data.find((m) => m.event_name === price.id);
if (!meter) {
throw error;
}
}
const tiers = priceToInArrearTiers(price, ent);
const tiers = priceToInArrearTiers({
price,
entitlement: ent,
org,
});
let priceAmountData = {};
if (ent.allowance == 0 && tiers.length == 1) {
if (ent.allowance === 0 && tiers.length === 1) {
priceAmountData = {
unit_amount_decimal: tiers[0].unit_amount_decimal,
};
@@ -110,7 +114,7 @@ export const arrearProratedToStripeTiers = (
price: Price,
entitlement: EntitlementWithFeature,
) => {
let usageConfig = structuredClone(price.config) as UsagePriceConfig;
const usageConfig = structuredClone(price.config) as UsagePriceConfig;
const billingUnits = usageConfig.billing_units;
const numFree = entitlement.allowance
@@ -127,10 +131,9 @@ export const arrearProratedToStripeTiers = (
}
for (let i = 0; i < usageConfig.usage_tiers.length; i++) {
const tier = usageConfig.usage_tiers[i];
// const amount = tier.amount * 100;
const amount = new Decimal(tier.amount).mul(100).toNumber();
const upTo =
tier.to == -1 || tier.to == TierInfinite
tier.to === -1 || tier.to === TierInfinite
? "inf"
: Math.round((tier.to - numFree) / billingUnits!) + numFree;
@@ -162,8 +165,8 @@ export const createStripeArrearProrated = async ({
}) => {
const relatedEnt = getPriceEntitlement(price, entitlements);
let recurringData = undefined;
if (price.config!.interval != BillingInterval.OneOff) {
let recurringData;
if (price.config!.interval !== BillingInterval.OneOff) {
recurringData = billingIntervalToStripe({
interval: price.config!.interval,
intervalCount: price.config!.interval_count,
@@ -173,11 +176,11 @@ export const createStripeArrearProrated = async ({
const config = price.config as UsagePriceConfig;
// 1. Product name
let productName = `${product.name} - ${
config.billing_units == 1 ? "" : `${config.billing_units} `
const productName = `${product.name} - ${
config.billing_units === 1 ? "" : `${config.billing_units} `
}${relatedEnt.feature.name}`;
let productData = curStripeProd
const productData = curStripeProd
? { product: curStripeProd.id }
: {
product_data: {
@@ -186,10 +189,14 @@ export const createStripeArrearProrated = async ({
};
// let tiers = arrearProratedToStripeTiers(price, relatedEnt);
let tiers = priceToInArrearTiers(price, relatedEnt);
const tiers = priceToInArrearTiers({
price,
entitlement: relatedEnt,
org,
});
let priceAmountData = {};
if (tiers.length == 1) {
if (tiers.length === 1) {
priceAmountData = {
unit_amount_decimal: tiers[0].unit_amount_decimal,
};
@@ -201,7 +208,7 @@ export const createStripeArrearProrated = async ({
};
}
let stripePrice = await stripeCli.prices.create({
const stripePrice = await stripeCli.prices.create({
...productData,
currency: org.default_currency || "usd",
...priceAmountData,
@@ -213,11 +220,11 @@ export const createStripeArrearProrated = async ({
config.stripe_price_id = stripePrice.id;
config.stripe_product_id = stripePrice.product as string;
let billingType = getBillingType(price.config!);
const billingType = getBillingType(price.config);
// CREATE PLACEHOLDER PRICE FOR INARREAR PRORATED PRICING
if (billingType == BillingType.InArrearProrated) {
let placeholderPrice = await createStripeMeteredPrice({
if (billingType === BillingType.InArrearProrated) {
const placeholderPrice = await createStripeMeteredPrice({
db,
stripeCli,
price,

View File

@@ -1,10 +1,14 @@
import Stripe from "stripe";
import type {
FixedPriceConfig,
Organization,
Price,
Product,
} from "@autumn/shared";
import { atmnToStripeAmount } from "@autumn/shared";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { PriceService } from "@/internal/products/prices/PriceService.js";
import { Price, Product, Organization, FixedPriceConfig } from "@autumn/shared";
import { SupabaseClient } from "@supabase/supabase-js";
import { billingIntervalToStripe } from "../stripePriceUtils.js";
import { Decimal } from "decimal.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
export const createStripeFixedPrice = async ({
db,
@@ -20,13 +24,17 @@ export const createStripeFixedPrice = async ({
org: Organization;
}) => {
const config = price.config as FixedPriceConfig;
const currency = org.default_currency || "usd";
let amount = new Decimal(config.amount).mul(100).toNumber();
const amount = atmnToStripeAmount({
amount: config.amount,
currency,
});
const stripePrice = await stripeCli.prices.create({
product: product.processor!.id,
unit_amount: amount,
currency: org.default_currency!,
currency,
recurring: {
...(billingIntervalToStripe({
interval: config.interval,

View File

@@ -1,23 +1,23 @@
import { PriceService } from "@/internal/products/prices/PriceService.js";
import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js";
import {
Product,
Price,
Organization,
EntitlementWithFeature,
UsagePriceConfig,
Feature,
TierInfinite,
Entitlement,
atmnToStripeAmountDecimal,
type Entitlement,
type EntitlementWithFeature,
ErrCode,
type Feature,
type Organization,
type Price,
type Product,
priceToEnt,
TierInfinite,
type UsagePriceConfig,
} from "@autumn/shared";
import { SupabaseClient } from "@supabase/supabase-js";
import Stripe from "stripe";
import { billingIntervalToStripe } from "../stripePriceUtils.js";
import { Decimal } from "decimal.js";
import RecaseError from "@/utils/errorUtils.js";
import { StatusCodes } from "http-status-codes";
import { DrizzleCli } from "@/db/initDrizzle.js";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { PriceService } from "@/internal/products/prices/PriceService.js";
import RecaseError from "@/utils/errorUtils.js";
import { billingIntervalToStripe } from "../stripePriceUtils.js";
export const searchStripeMeter = async ({
stripeCli,
@@ -30,7 +30,7 @@ export const searchStripeMeter = async ({
meterId?: string;
logger: any;
}) => {
let allStripeMeters = [];
const allStripeMeters = [];
let hasMore = true;
let startingAfter;
@@ -52,8 +52,8 @@ export const searchStripeMeter = async ({
const end = performance.now();
logger.info(`Stripe meter list took ${end - start}ms`);
let stripeMeter = allStripeMeters.find(
(m) => m.event_name == eventName || m.id == meterId,
const stripeMeter = allStripeMeters.find(
(m) => m.event_name === eventName || m.id === meterId,
);
return stripeMeter;
@@ -72,29 +72,25 @@ export const getStripeMeter = async ({
price: Price;
logger: any;
}) => {
let config = price.config as UsagePriceConfig;
const config = price.config as UsagePriceConfig;
let createNew = false;
try {
let stripeMeter = await searchStripeMeter({
const stripeMeter = await searchStripeMeter({
stripeCli,
eventName: price.id!,
meterId: config.stripe_meter_id!,
logger,
});
if (!stripeMeter) {
createNew = true;
} else {
if (stripeMeter) {
logger.info(
`✅ Found existing meter for ${product.name} - ${feature!.name}`,
);
return stripeMeter;
}
} catch (error) {
createNew = true;
}
let meter = await stripeCli.billing.meters.create({
} catch (_error) {}
const meter = await stripeCli.billing.meters.create({
display_name: `${product.name} - ${feature!.name}`,
event_name: price.id!,
default_aggregation: {
@@ -105,11 +101,16 @@ export const getStripeMeter = async ({
};
// IN ARREAR
export const priceToInArrearTiers = (
price: Price,
entitlement: Entitlement,
) => {
let usageConfig = structuredClone(price.config) as UsagePriceConfig;
export const priceToInArrearTiers = ({
price,
entitlement,
org,
}: {
price: Price;
entitlement: Entitlement;
org: Organization;
}) => {
const usageConfig = structuredClone(price.config) as UsagePriceConfig;
const tiers: any[] = [];
if (entitlement.allowance) {
tiers.push({
@@ -118,8 +119,8 @@ export const priceToInArrearTiers = (
});
for (let i = 0; i < usageConfig.usage_tiers.length; i++) {
let tier = usageConfig.usage_tiers[i];
if (tier.to != -1 && tier.to != TierInfinite) {
const tier = usageConfig.usage_tiers[i];
if (tier.to !== -1 && tier.to !== TierInfinite) {
usageConfig.usage_tiers[i].to = (tier.to || 0) + entitlement.allowance;
}
}
@@ -127,15 +128,18 @@ export const priceToInArrearTiers = (
for (let i = 0; i < usageConfig.usage_tiers.length; i++) {
const tier = usageConfig.usage_tiers[i];
let amount = new Decimal(tier.amount)
.div(usageConfig.billing_units ?? 1)
.mul(100)
.toDecimalPlaces(10)
.toString();
const atmnUnitAmount = new Decimal(tier.amount).div(
usageConfig.billing_units ?? 1,
);
const stripeUnitAmountDecimal = atmnToStripeAmountDecimal({
amount: atmnUnitAmount,
currency: org.default_currency || undefined,
});
tiers.push({
unit_amount_decimal: amount,
up_to: tier.to == -1 ? "inf" : tier.to,
unit_amount_decimal: stripeUnitAmountDecimal,
up_to: tier.to === -1 ? "inf" : tier.to,
});
}
@@ -167,19 +171,22 @@ export const createStripeInArrearPrice = async ({
internalEntityId?: string;
useCheckout?: boolean;
}) => {
let config = price.config as UsagePriceConfig;
const config = price.config as UsagePriceConfig;
// 1. Create meter
let relatedEnt = getPriceEntitlement(price, entitlements);
let feature = relatedEnt?.feature;
const relatedEnt = priceToEnt({
price,
entitlements,
});
const feature = relatedEnt?.feature;
// 1. If internal entity ID and not curStripe product, create product
if (internalEntityId && !useCheckout) {
if (!curStripeProduct) {
logger.info(
`Creating stripe in arrear product for ${relatedEnt.feature.name} (internal entity ID exists!)`,
`Creating stripe in arrear product for ${relatedEnt?.feature.name} (internal entity ID exists!)`,
);
let stripeProduct = await stripeCli.products.create({
const stripeProduct = await stripeCli.products.create({
name: `${product.name} - ${feature!.name}`,
});
config.stripe_product_id = stripeProduct.id;
@@ -199,7 +206,7 @@ export const createStripeInArrearPrice = async ({
}
logger.info(
`Creating stripe in arrear price for ${relatedEnt.feature.name} (no internal entity ID)`,
`Creating stripe in arrear price for ${relatedEnt?.feature.name} (no internal entity ID)`,
);
if (!feature) {
@@ -211,7 +218,7 @@ export const createStripeInArrearPrice = async ({
}
// 1. Get meter by event_name
let meter = await getStripeMeter({
const meter = await getStripeMeter({
product,
feature,
stripeCli,
@@ -221,13 +228,14 @@ export const createStripeInArrearPrice = async ({
config.stripe_meter_id = meter.id;
const tiers = priceToInArrearTiers(
const tiers = priceToInArrearTiers({
price,
getPriceEntitlement(price, entitlements),
);
entitlement: relatedEnt,
org,
});
let priceAmountData = {};
if (tiers.length == 1) {
if (tiers.length === 1) {
priceAmountData = {
unit_amount_decimal: tiers[0].unit_amount_decimal,
};

View File

@@ -1,14 +1,13 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import { PriceService } from "@/internal/products/prices/PriceService.js";
import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js";
import {
import type {
EntitlementWithFeature,
Price,
Product,
UsagePriceConfig,
} from "@autumn/shared";
import { SupabaseClient } from "@supabase/supabase-js";
import Stripe from "stripe";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { PriceService } from "@/internal/products/prices/PriceService.js";
import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js";
export const createStripeOneOffTieredProduct = async ({
db,
@@ -23,13 +22,13 @@ export const createStripeOneOffTieredProduct = async ({
entitlements: EntitlementWithFeature[];
product: Product;
}) => {
let config = price.config as UsagePriceConfig;
let relatedEnt = getPriceEntitlement(price, entitlements);
let productName = `${product.name} - ${
config.billing_units == 1 ? "" : `${config.billing_units} `
const config = price.config as UsagePriceConfig;
const relatedEnt = getPriceEntitlement(price, entitlements);
const productName = `${product.name} - ${
config.billing_units === 1 ? "" : `${config.billing_units} `
}${relatedEnt.feature.name}`;
let stripeProduct = await stripeCli.products.create({
const stripeProduct = await stripeCli.products.create({
name: productName,
});

View File

@@ -1,31 +1,27 @@
import {
atmnToStripeAmountDecimal,
BillingInterval,
BillingType,
Entitlement,
EntitlementWithFeature,
Organization,
Price,
Product,
type EntitlementWithFeature,
type Organization,
type Price,
type Product,
TierInfinite,
UsagePriceConfig,
type UsagePriceConfig,
} from "@autumn/shared";
import { SupabaseClient } from "@supabase/supabase-js";
import Stripe from "stripe";
import { billingIntervalToStripe } from "../stripePriceUtils.js";
import {
formatPrice,
getBillingType,
getPriceEntitlement,
} from "@/internal/products/prices/priceUtils.js";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { PriceService } from "@/internal/products/prices/PriceService.js";
import { Decimal } from "decimal.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js";
import { billingIntervalToStripe } from "../stripePriceUtils.js";
export const prepaidToStripeTiers = (
price: Price,
entitlement: EntitlementWithFeature,
) => {
let usageConfig = structuredClone(price.config) as UsagePriceConfig;
export const prepaidToStripeTiers = ({
price,
org,
}: {
price: Price;
org: Organization;
}) => {
const usageConfig = structuredClone(price.config) as UsagePriceConfig;
const billingUnits = usageConfig.billing_units;
// const numFree = entitlement.allowance
@@ -36,9 +32,12 @@ export const prepaidToStripeTiers = (
for (let i = 0; i < usageConfig.usage_tiers.length; i++) {
const tier = usageConfig.usage_tiers[i];
const amount = new Decimal(tier.amount).mul(100).toNumber();
const amount = atmnToStripeAmountDecimal({
amount: tier.amount,
currency: org.default_currency || undefined,
});
const upTo =
tier.to == -1 || tier.to == TierInfinite
tier.to === -1 || tier.to === TierInfinite
? "inf"
: Math.round(tier.to / billingUnits!);
@@ -70,8 +69,8 @@ export const createStripePrepaid = async ({
}) => {
const relatedEnt = getPriceEntitlement(price, entitlements);
let recurringData = undefined;
if (price.config!.interval != BillingInterval.OneOff) {
let recurringData;
if (price.config!.interval !== BillingInterval.OneOff) {
recurringData = billingIntervalToStripe({
interval: price.config!.interval,
intervalCount: price.config!.interval_count,
@@ -81,11 +80,11 @@ export const createStripePrepaid = async ({
const config = price.config as UsagePriceConfig;
// 1. Product name
let productName = `${product.name} - ${
config.billing_units == 1 ? "" : `${config.billing_units} `
const productName = `${product.name} - ${
config.billing_units === 1 ? "" : `${config.billing_units} `
}${relatedEnt.feature.name}`;
let productData = curStripeProd
const productData = curStripeProd
? { product: curStripeProd.id }
: {
product_data: {
@@ -95,13 +94,13 @@ export const createStripePrepaid = async ({
// 2. If billing interval is one off
let stripePrice = null;
if (price.config!.interval == BillingInterval.OneOff) {
if (price.config!.interval === BillingInterval.OneOff) {
const amount = config.usage_tiers[0].amount;
let unitAmountDecimalStr = new Decimal(amount)
.mul(100)
.toDecimalPlaces(10)
.toString();
const unitAmountDecimalStr = atmnToStripeAmountDecimal({
amount,
currency: org.default_currency || undefined,
});
stripePrice = await stripeCli.prices.create({
...productData,
@@ -112,10 +111,10 @@ export const createStripePrepaid = async ({
config.stripe_product_id = stripePrice.product as string;
config.stripe_price_id = stripePrice.id;
} else {
let tiers = prepaidToStripeTiers(price, relatedEnt);
const tiers = prepaidToStripeTiers({ price, org });
let priceAmountData = {};
if (tiers.length == 1) {
if (tiers.length === 1) {
priceAmountData = {
unit_amount_decimal: tiers[0].unit_amount_decimal,
};
@@ -139,7 +138,6 @@ export const createStripePrepaid = async ({
config.stripe_price_id = stripePrice.id;
config.stripe_product_id = stripePrice.product as string;
let billingType = getBillingType(price.config!);
}
// New config

View File

@@ -1,29 +1,28 @@
import {
BillingType,
type EntitlementWithFeature,
type Organization,
type Price,
type Product,
type UsagePriceConfig,
} from "@autumn/shared";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { PriceService } from "@/internal/products/prices/PriceService.js";
import {
getBillingType,
getPriceEntitlement,
priceIsOneOffAndTiered,
} from "@/internal/products/prices/priceUtils.js";
import {
Price,
EntitlementWithFeature,
Product,
Organization,
UsagePriceConfig,
BillingType,
} from "@autumn/shared";
import Stripe from "stripe";
import { createStripeFixedPrice } from "./createStripeFixedPrice.js";
import { createStripePrepaid } from "./createStripePrepaid.js";
import { createStripeOneOffTieredProduct } from "./createStripeOneOffTiered.js";
import { createStripeInArrearPrice } from "./createStripeInArrear.js";
import { billingIntervalToStripe } from "../stripePriceUtils.js";
import {
createStripeArrearProrated,
createStripeMeteredPrice,
} from "./createStripeArrearProrated.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { PriceService } from "@/internal/products/prices/PriceService.js";
import { billingIntervalToStripe } from "../stripePriceUtils.js";
import { createStripeFixedPrice } from "./createStripeFixedPrice.js";
import { createStripeInArrearPrice } from "./createStripeInArrear.js";
import { createStripeOneOffTieredProduct } from "./createStripeOneOffTiered.js";
import { createStripePrepaid } from "./createStripePrepaid.js";
export const checkCurStripePrice = async ({
price,
@@ -34,7 +33,7 @@ export const checkCurStripePrice = async ({
stripeCli: Stripe;
currency: string;
}) => {
let config = price.config! as UsagePriceConfig;
const config = price.config! as UsagePriceConfig;
let stripePrice: Stripe.Price | null = null;
if (!config.stripe_price_id) {
@@ -57,7 +56,7 @@ export const checkCurStripePrice = async ({
) {
stripePrice = null;
}
} catch (error) {
} catch (_error) {
stripePrice = null;
}
}
@@ -72,7 +71,7 @@ export const checkCurStripePrice = async ({
if (!stripeProd.active) {
stripeProd = null;
}
} catch (error) {
} catch (_error) {
stripeProd = null;
}
}
@@ -108,23 +107,23 @@ export const createStripePriceIFNotExist = async ({
const billingType = getBillingType(price.config!);
let { stripePrice, stripeProd } = await checkCurStripePrice({
const { stripePrice, stripeProd } = await checkCurStripePrice({
price,
stripeCli,
currency: org.default_currency || "usd",
});
let config = price.config! as UsagePriceConfig;
const config = price.config! as UsagePriceConfig;
config.stripe_price_id = stripePrice?.id;
config.stripe_product_id = stripeProd?.id;
let relatedEnt = getPriceEntitlement(price, entitlements);
let isOneOffAndTiered = priceIsOneOffAndTiered(price, relatedEnt);
const relatedEnt = getPriceEntitlement(price, entitlements);
const isOneOffAndTiered = priceIsOneOffAndTiered(price, relatedEnt);
// 1. If fixed price, just create price
if (
billingType == BillingType.FixedCycle ||
billingType == BillingType.OneOff
billingType === BillingType.FixedCycle ||
billingType === BillingType.OneOff
) {
if (!stripePrice) {
await createStripeFixedPrice({
@@ -138,7 +137,7 @@ export const createStripePriceIFNotExist = async ({
}
// 2. If prepaid
if (billingType == BillingType.UsageInAdvance) {
if (billingType === BillingType.UsageInAdvance) {
if (isOneOffAndTiered && !stripeProd) {
logger.info(`Creating stripe one off tiered product`);
await createStripeOneOffTieredProduct({
@@ -164,7 +163,7 @@ export const createStripePriceIFNotExist = async ({
}
}
if (billingType == BillingType.InArrearProrated) {
if (billingType === BillingType.InArrearProrated) {
if (!stripePrice) {
logger.info(`Creating stripe in arrear prorated product`);
await createStripeArrearProrated({
@@ -178,7 +177,7 @@ export const createStripePriceIFNotExist = async ({
});
} else if (!config.stripe_placeholder_price_id) {
logger.info(`Creating stripe placeholder price`);
let placeholderPrice = await createStripeMeteredPrice({
const placeholderPrice = await createStripeMeteredPrice({
db,
stripeCli,
price,
@@ -195,7 +194,7 @@ export const createStripePriceIFNotExist = async ({
}
}
if (billingType == BillingType.UsageInArrear) {
if (billingType === BillingType.UsageInArrear) {
await createStripeInArrearPrice({
db,
stripeCli,

View File

@@ -171,10 +171,6 @@ export const createStripeCoupon = async ({
} catch (_) {}
}
console.log("Reward type:", reward.type);
console.log("stripeProdIds", stripeProdIds);
console.log("Apply to all", discountConfig!.apply_to_all);
const stripeCoupon = await stripeCli.coupons.create({
// id: reward.internal_id,
id: reward.id,

View File

@@ -1,8 +1,14 @@
import Stripe from "stripe";
import RecaseError from "@/utils/errorUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
ErrCode,
type InvoiceDiscount,
type InvoiceStatus,
notNullish,
stripeToAtmnAmount,
} from "@autumn/shared";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { InvoiceService } from "@/internal/invoices/InvoiceService.js";
import { ErrCode, InvoiceDiscount, InvoiceStatus } from "@autumn/shared";
import RecaseError from "@/utils/errorUtils.js";
// For API calls
export const getStripeExpandedInvoice = async ({
@@ -75,8 +81,8 @@ export const payForInvoice = async ({
}
}
let invoice = await stripeCli.invoices.retrieve(invoiceId);
if (invoice.status == "paid") {
const invoice = await stripeCli.invoices.retrieve(invoiceId);
if (invoice.status === "paid") {
logger.info(`Invoice ${invoiceId} is already paid`);
return {
paid: true,
@@ -102,7 +108,7 @@ export const payForInvoice = async ({
if (voidIfFailed) {
try {
await stripeCli.invoices.voidInvoice(invoiceId);
} catch (error) {
} catch (_error) {
logger.error(`Failed to void failed invoice: ${invoiceId}`);
}
}
@@ -119,25 +125,6 @@ export const payForInvoice = async ({
invoice: null,
};
}
// if (isStripeCardDeclined(error)) {
// return {
// paid: false,
// error: new RecaseError({
// message: `Payment declined: ${error.message}`,
// code: ErrCode.StripeCardDeclined,
// statusCode: 400,
// }),
// };
// }
// return {
// paid: false,
// error: new RecaseError({
// message: "Failed to pay invoice",
// code: ErrCode.PayInvoiceFailed,
// }),
// };
}
};
@@ -175,37 +162,51 @@ export const getInvoiceDiscounts = ({
}: {
expandedInvoice: Stripe.Invoice;
}) => {
try {
if (!expandedInvoice.discounts || expandedInvoice.discounts.length === 0) {
return [];
}
if (!expandedInvoice.discounts || expandedInvoice.discounts.length === 0) {
return [];
}
if (typeof expandedInvoice.discounts[0] == "string") {
return [];
}
if (typeof expandedInvoice.discounts[0] === "string") {
return [];
}
let totalDiscountAmounts = expandedInvoice.total_discount_amounts;
const totalDiscountAmounts = expandedInvoice.total_discount_amounts;
const autumnDiscounts = expandedInvoice.discounts
.map((discount) => {
if (typeof discount === "string") return null;
const amountOff = totalDiscountAmounts?.find(
(item) => item.discount === discount.id,
)?.amount;
if (!amountOff) return null;
let autumnDiscounts = expandedInvoice.discounts.map((discount: any) => {
const amountOff = discount.coupon.amount_off;
const amountUsed = totalDiscountAmounts?.find(
(item) => item.discount === discount.id,
)?.amount;
let autumnDiscount: InvoiceDiscount = {
const atmnAmountOff = stripeToAtmnAmount({
amount: amountOff,
currency: expandedInvoice.currency,
});
const atmnAmountUsed = stripeToAtmnAmount({
amount: amountUsed || 0,
currency: expandedInvoice.currency,
});
const autumnDiscount: InvoiceDiscount = {
stripe_coupon_id: discount.coupon?.id,
coupon_name: discount.coupon.name,
amount_off: amountOff / 100,
amount_used: (amountUsed || 0) / 100,
coupon_name: discount.coupon?.name || "",
amount_off: atmnAmountOff,
amount_used: atmnAmountUsed,
};
return autumnDiscount;
});
})
.filter(notNullish);
return autumnDiscounts;
} catch (error) {
throw error;
}
return autumnDiscounts;
};
export const getInvoiceExpansion = () => {

View File

@@ -1,18 +1,15 @@
import {
BillingInterval,
Price,
Feature,
Customer,
FullCusProduct,
UsagePriceConfig,
FullProduct,
Organization,
type Customer,
type Feature,
type FullCusProduct,
type FullProduct,
type Organization,
type Price,
type UsagePriceConfig,
} from "@autumn/shared";
import Stripe from "stripe";
import { getPriceForOverage } from "@/internal/products/prices/priceUtils.js";
import { getFeatureName } from "@/internal/features/utils/displayUtils.js";
import type Stripe from "stripe";
import { getCusPriceUsage } from "@/internal/customers/cusProducts/cusPrices/cusPriceUtils.js";
export const createSubMeta = ({ features }: { features: Feature[] }) => {
@@ -88,9 +85,9 @@ export const getInvoiceItemForUsage = ({
withProdPrefix: true,
});
let config = price.config! as UsagePriceConfig;
const config = price.config! as UsagePriceConfig;
let invoiceItem: Stripe.InvoiceItemCreateParams = {
const invoiceItem: Stripe.InvoiceItemCreateParams = {
invoice: stripeInvoiceId,
customer: customer.processor.id,
currency,

View File

@@ -18,15 +18,12 @@ import { JobName } from "@/queue/JobName.js";
import { addTaskToQueue } from "@/queue/queueUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import { getEarliestPeriodEnd } from "../stripeSubUtils/convertSubUtils.js";
import { createStripeCli } from "../utils.js";
import { getOptionsFromCheckoutSession } from "./handleCheckoutCompleted/getOptionsFromCheckout.js";
import { handleCheckoutSub } from "./handleCheckoutCompleted/handleCheckoutSub.js";
import { handleRemainingSets } from "./handleCheckoutCompleted/handleRemainingSets.js";
import { handleSetupCheckout } from "./handleCheckoutCompleted/handleSetupCheckout.js";
import { getOptionsFromCheckoutSession } from "./handleCheckoutCompleted/getOptionsFromCheckout.js";
import { getEarliestPeriodEnd } from "../stripeSubUtils/convertSubUtils.js";
export const handleCheckoutSessionCompleted = async ({
req,
@@ -59,12 +56,12 @@ export const handleCheckoutSessionCompleted = async ({
attachParams.req = req;
attachParams.stripeCli = stripeCli;
if (attachParams.org.id != org.id) {
if (attachParams.org.id !== org.id) {
console.log("checkout.completed: org doesn't match, skipping");
return;
}
if (attachParams.customer.env != env) {
if (attachParams.customer.env !== env) {
console.log("checkout.completed: environments don't match, skipping");
return;
}

View File

@@ -1,10 +1,12 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import { handleAddProduct } from "@/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.js";
import { getDefaultAttachConfig } from "@/internal/customers/attach/attachUtils/getAttachConfig.js";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { AttachBranch } from "@autumn/shared";
import Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { handleAddProduct } from "@/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.js";
import { handleOneOffFunction } from "@/internal/customers/attach/attachFunctions/addProductFlow/handleOneOffFunction.js";
import { getDefaultAttachConfig } from "@/internal/customers/attach/attachUtils/getAttachConfig.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { isOneOff } from "@/internal/products/productUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import { getCusPaymentMethod } from "../../stripeCusUtils.js";
import { createStripeCli } from "../../utils.js";
export const handleSetupCheckout = async ({
@@ -17,10 +19,28 @@ export const handleSetupCheckout = async ({
attachParams: AttachParams;
}) => {
const logger = req.logger;
const { org, customer } = attachParams;
const paymentMethod = await getCusPaymentMethod({
stripeCli: createStripeCli({ org, env: customer.env }),
stripeId: customer.processor?.id,
errorIfNone: false,
});
attachParams.paymentMethod = paymentMethod;
logger.info(`HANDLING SETUP CHECKOUT COMPLETED`);
if (isOneOff(attachParams.prices)) {
await handleOneOffFunction({
req,
attachParams,
config: getDefaultAttachConfig(),
res: undefined,
});
return;
}
// 1. Check attach prices...
await handleAddProduct({
req,

View File

@@ -204,5 +204,4 @@ export const handleCreateCheckout = async ({
checkout_url: checkout.url,
});
}
return;
};

View File

@@ -1,15 +1,21 @@
import {
type EntitlementWithFeature,
type FullProduct,
formatAmount,
type Organization,
type Price,
type Reward,
} from "@autumn/shared";
import type Stripe from "stripe";
import { newPriceToInvoiceDescription } from "@/internal/invoices/invoiceFormatUtils.js";
import { getProration } from "@/internal/invoices/previewItemUtils/getItemsForNewProduct.js";
import {
getPriceEntitlement,
getPriceForOverage,
} from "@/internal/products/prices/priceUtils.js";
import { priceToUsageModel } from "@/internal/products/prices/priceUtils/convertPrice.js";
import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js";
import {
isFixedPrice,
isOneOffPrice,
} from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js";
import {
formatReward,
getAmountAfterReward,
@@ -17,16 +23,6 @@ import {
} from "@/internal/rewards/rewardUtils.js";
import { formatUnixToDate } from "@/utils/genUtils.js";
import {
EntitlementWithFeature,
formatAmount,
FullProduct,
Organization,
Price,
Reward,
} from "@autumn/shared";
import Stripe from "stripe";
export const priceToNewPreviewItem = ({
org,
price,
@@ -95,6 +91,7 @@ export const priceToNewPreviewItem = ({
amount,
reward,
subDiscounts: subDiscounts ?? [],
currency: org.default_currency || undefined,
});
}
@@ -103,6 +100,7 @@ export const priceToNewPreviewItem = ({
amount,
product,
stripeDiscounts: subDiscounts ?? [],
currency: org.default_currency || undefined,
});
let description = newPriceToInvoiceDescription({

View File

@@ -105,10 +105,6 @@ export const getAttachConfig = async ({
const sameIntervals = intervalsAreSame({ attachParams });
// let disableMerge =
// branch == AttachBranch.MainIsTrial ||
// org.config.merge_billing_cycles === false;
const invoiceAndEnable =
attachParams.invoiceOnly && attachBody.enable_product_immediately;
@@ -128,6 +124,7 @@ export const getAttachConfig = async ({
].includes(branch));
const onlyCheckout = !isFree && checkoutFlow && !freeTrialWithoutCardRequired;
const disableMerge = branch === AttachBranch.MainIsTrial || onlyCheckout;
// Require payment method...

View File

@@ -41,9 +41,6 @@ export const getAttachFunction = async ({
config: AttachConfig;
}) => {
const { onlyCheckout } = config;
const { curCusProduct } = attachParamToCusProducts({
attachParams,
});
// 1. Checkout function
const newScenario = [
@@ -58,11 +55,11 @@ export const getAttachFunction = async ({
if (newScenario && onlyCheckout) {
return AttachFunction.CreateCheckout;
} else if (branch == AttachBranch.OneOff) {
} else if (branch === AttachBranch.OneOff) {
return AttachFunction.OneOff;
} else if (
branch == AttachBranch.MultiAttach ||
branch == AttachBranch.MultiAttachUpdate
branch === AttachBranch.MultiAttach ||
branch === AttachBranch.MultiAttachUpdate
) {
return AttachFunction.MultiAttach;
} else if (newScenario) {
@@ -86,12 +83,12 @@ export const getAttachFunction = async ({
}
// 3. Downgrade scenarios
if (branch == AttachBranch.Downgrade) {
if (branch === AttachBranch.Downgrade) {
return AttachFunction.ScheduleProduct;
}
// 4. Prepaid scenarios
if (branch == AttachBranch.UpdatePrepaidQuantity) {
if (branch === AttachBranch.UpdatePrepaidQuantity) {
const curSameProduct = attachParamsToCurCusProduct({ attachParams });
if (curSameProduct?.free_trial) {
attachParams.freeTrial = curSameProduct.free_trial;
@@ -99,7 +96,7 @@ export const getAttachFunction = async ({
return AttachFunction.UpdatePrepaidQuantity;
}
if (branch == AttachBranch.Renew) {
if (branch === AttachBranch.Renew) {
return AttachFunction.Renew;
}

View File

@@ -1,20 +1,19 @@
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import {
BillingInterval,
type BillingInterval,
BillingType,
FullCusProduct,
FullProduct,
cusProductToPrices,
type FullCusProduct,
type FullProduct,
intervalsDifferent,
intervalsSame,
stripeToAtmnAmount,
} from "@autumn/shared";
import Stripe from "stripe";
import { getContUseInvoiceItems } from "./getContUseInvoiceItems.js";
import { findPriceInStripeItems } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
import { cusProductToPrices } from "@autumn/shared";
import { attachParamsToProduct } from "../convertAttachParams.js";
import { subToAutumnInterval } from "@/external/stripe/utils.js";
import { intervalsAreSame } from "../getAttachConfig.js";
import type Stripe from "stripe";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { findPriceInStripeItems } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
import { subToAutumnInterval } from "@/external/stripe/utils.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { attachParamsToProduct } from "../convertAttachParams.js";
import { getContUseInvoiceItems } from "./getContUseInvoiceItems.js";
export const filterContUsageProrations = async ({
sub,
@@ -32,7 +31,7 @@ export const filterContUsageProrations = async ({
const curPrices = cusProductToPrices({
cusProduct: curCusProduct,
});
let allPrices = [...curPrices, ...newProduct.prices];
const allPrices = [...curPrices, ...newProduct.prices];
// const upcomingLines = await stripeCli.invoices.listUpcomingLines({
// subscription: sub.id,
@@ -49,7 +48,7 @@ export const filterContUsageProrations = async ({
// console.log("LINE ITEM:", item);
if (!item.proration) continue;
let price = findPriceInStripeItems({
const price = findPriceInStripeItems({
prices: allPrices,
lineItem: item,
billingType: BillingType.InArrearProrated,
@@ -57,15 +56,16 @@ export const filterContUsageProrations = async ({
if (!price) continue;
const atmnAmount = stripeToAtmnAmount({
amount: item.amount,
currency: item.currency,
});
logger.info(
`Deleting ii: ${item.description} - ${item.amount / 100} (${intervalSet.interval}, ${intervalSet.intervalCount})`,
`Deleting ii: ${item.description} - ${atmnAmount} (${intervalSet.interval}, ${intervalSet.intervalCount})`,
);
await stripeCli.invoiceItems.del(
item.id,
// @ts-ignore -- Stripe types are not correct
// item.parent.subscription_item_details.invoice_item
);
await stripeCli.invoiceItems.del(item.id);
}
};
@@ -95,7 +95,7 @@ export const createAndFilterContUseItems = async ({
// return { newItems: [], oldItems: [], replaceables: [] };
// }
let { newItems, oldItems, replaceables } = await getContUseInvoiceItems({
const { newItems, oldItems, replaceables } = await getContUseInvoiceItems({
attachParams,
cusProduct: curMainProduct!,
sub,
@@ -120,7 +120,7 @@ export const createAndFilterContUseItems = async ({
continue;
}
let price =
const price =
product.prices.find((p) => p.id === item.price_id) ||
curPrices.find((p) => p.id === item.price_id);

View File

@@ -3,8 +3,8 @@ import {
CusExpand,
type FullCustomer,
type Organization,
stripeToAtmnAmount,
} from "@autumn/shared";
import { Decimal } from "decimal.js";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { getEarliestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
@@ -58,10 +58,16 @@ export const getCusUpcomingInvoice = async ({
const cusProd = fullCus.customer_products.find((cp) =>
lineItemInCusProduct({ cusProduct: cp, lineItem: line }),
);
const atmnLineAmount = stripeToAtmnAmount({
amount: line.amount,
currency: line.currency,
});
lines.push({
product_id: cusProd?.product.id || null,
description: line.description,
amount: new Decimal(line.amount).div(100).toDecimalPlaces(2).toNumber(),
amount: atmnLineAmount,
});
}
@@ -79,14 +85,21 @@ export const getCusUpcomingInvoice = async ({
}),
);
// console.log("lines: ", lines);
// console.log("discounts: ", discounts);
const atmnSubtotal = stripeToAtmnAmount({
amount: upcomingInvoice.subtotal,
currency: upcomingInvoice.currency,
});
const atmnTotal = stripeToAtmnAmount({
amount: upcomingInvoice.total,
currency: upcomingInvoice.currency,
});
const res = {
lines,
discounts,
subtotal: upcomingInvoice.subtotal / 100,
total: upcomingInvoice.total / 100,
subtotal: atmnSubtotal,
total: atmnTotal,
currency: upcomingInvoice.currency,
};

View File

@@ -1,4 +1,8 @@
import { CouponDurationType, RewardType } from "@autumn/shared";
import {
CouponDurationType,
RewardType,
stripeToAtmnAmount,
} from "@autumn/shared";
import type Stripe from "stripe";
const parseStripeCouponDuration = (coupon: Stripe.Coupon) => {
@@ -37,6 +41,11 @@ export const stripeDiscountToResponse = ({
(t) => t.discount === d.id,
);
const totalAtmnDiscountAmount = stripeToAtmnAmount({
amount: totalDiscountAmount?.amount || 0,
currency: d.coupon?.currency ?? undefined,
});
return {
id: d.coupon?.id,
name: d.coupon?.name ?? "",
@@ -52,7 +61,7 @@ export const stripeDiscountToResponse = ({
duration_value,
total_discount_amount: totalDiscountAmount?.amount
? totalDiscountAmount.amount / 100
? totalAtmnDiscountAmount
: null,
};
};

View File

@@ -1,26 +1,24 @@
import RecaseError from "@/utils/errorUtils.js";
import {
MeteredConfig,
ErrCode,
AggregateType,
CreditSystemConfig,
Feature,
FeatureUsageType,
Organization,
ProductItemFeatureType,
type CreditSystemConfig,
cusProductsToCusPrices,
ErrCode,
type Feature,
FeatureType,
FullCustomer,
UsagePriceConfig,
FeatureUsageType,
type FullCustomer,
type MeteredConfig,
ProductItemFeatureType,
type UsagePriceConfig,
} from "@autumn/shared";
import { FeatureService } from "./FeatureService.js";
import { StatusCodes } from "http-status-codes";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { generateFeatureDisplay } from "@/external/llm/llmUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { ACTIVE_STATUSES } from "../customers/cusProducts/CusProductService.js";
import { ProductService } from "../products/ProductService.js";
import { getCreditSystemsFromFeature } from "./creditSystemUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { cusProductsToCusPrices, cusProductToPrices } from "@autumn/shared";
import { priceToFeature } from "../products/prices/priceUtils/convertPrice.js";
import { ACTIVE_STATUSES } from "../customers/cusProducts/CusProductService.js";
import { FeatureService } from "./FeatureService.js";
export const validateFeatureId = (featureId: string) => {
if (!featureId.match(/^[a-zA-Z0-9_-]+$/)) {
@@ -35,7 +33,7 @@ export const validateFeatureId = (featureId: string) => {
};
export const validateMeteredConfig = (config: MeteredConfig) => {
let newConfig = { ...config };
const newConfig = { ...config };
if (!config.usage_type) {
throw new RecaseError({
@@ -45,7 +43,7 @@ export const validateMeteredConfig = (config: MeteredConfig) => {
});
}
if (config.aggregate?.type == AggregateType.Count) {
if (config.aggregate?.type === AggregateType.Count) {
newConfig.aggregate = {
type: AggregateType.Count,
property: null,
@@ -57,7 +55,7 @@ export const validateMeteredConfig = (config: MeteredConfig) => {
};
}
if (newConfig.filters.length == 0) {
if (newConfig?.filters?.length === 0 || !newConfig?.filters) {
newConfig.filters = [
{
property: "",
@@ -71,8 +69,8 @@ export const validateMeteredConfig = (config: MeteredConfig) => {
};
export const validateCreditSystem = (config: CreditSystemConfig) => {
let schema = config.schema;
if (!schema || schema.length == 0) {
const schema = config.schema;
if (!schema || schema.length === 0) {
throw new RecaseError({
message: `At least one metered feature is required for credit system`,
code: ErrCode.InvalidFeature,
@@ -94,11 +92,13 @@ export const validateCreditSystem = (config: CreditSystemConfig) => {
});
}
let newConfig = { ...config, usage_type: FeatureUsageType.Single };
const newConfig = { ...config, usage_type: FeatureUsageType.Single };
for (let i = 0; i < newConfig.schema.length; i++) {
newConfig.schema[i].feature_amount = 1;
let creditAmount = parseFloat(newConfig.schema[i].credit_amount.toString());
const creditAmount = parseFloat(
newConfig.schema[i].credit_amount.toString(),
);
if (isNaN(creditAmount)) {
throw new RecaseError({
message: `Credit amount should be a number`,
@@ -126,28 +126,29 @@ export const getObjectsUsingFeature = async ({
allFeatures: Feature[];
feature: Feature;
}) => {
let products = await ProductService.listFull({
const products = await ProductService.listFull({
db,
orgId,
env,
});
let allPrices = products.flatMap((p) => p.prices);
let allEnts = products.flatMap((p) => p.entitlements);
let creditSystems = getCreditSystemsFromFeature({
const allPrices = products.flatMap((p) => p.prices);
const allEnts = products.flatMap((p) => p.entitlements);
const creditSystems = getCreditSystemsFromFeature({
featureId: feature.id,
features: allFeatures,
});
let entitlements = allEnts.filter(
(entitlement) => entitlement.internal_feature_id == feature.internal_id,
const entitlements = allEnts.filter(
(entitlement) => entitlement.internal_feature_id === feature.internal_id,
);
let linkedEntitlements = allEnts.filter(
(entitlement) => entitlement.entity_feature_id == feature.id,
const linkedEntitlements = allEnts.filter(
(entitlement) => entitlement.entity_feature_id === feature.id,
);
let prices = allPrices.filter(
(price) => (price.config as any).internal_feature_id == feature.internal_id,
const prices = allPrices.filter(
(price) =>
(price.config as any).internal_feature_id === feature.internal_id,
);
return { entitlements, prices, creditSystems, linkedEntitlements };
@@ -193,10 +194,10 @@ export const runSaveFeatureDisplayTask = async ({
};
export const getCusFeatureType = ({ feature }: { feature: Feature }) => {
if (feature.type == FeatureType.Boolean) {
if (feature.type === FeatureType.Boolean) {
return ProductItemFeatureType.Static;
} else if (feature.type == FeatureType.Metered) {
if (feature.config.usage_type == FeatureUsageType.Single) {
} else if (feature.type === FeatureType.Metered) {
if (feature.config.usage_type === FeatureUsageType.Single) {
return ProductItemFeatureType.SingleUse;
} else {
return ProductItemFeatureType.ContinuousUse;
@@ -207,7 +208,7 @@ export const getCusFeatureType = ({ feature }: { feature: Feature }) => {
};
export const isCreditSystem = ({ feature }: { feature: Feature }) => {
return feature.type == FeatureType.CreditSystem;
return feature.type === FeatureType.CreditSystem;
};
export const isPaidContinuousUse = ({
@@ -217,22 +218,25 @@ export const isPaidContinuousUse = ({
feature: Feature;
fullCus: FullCustomer;
}) => {
let isContinuous = feature.config?.usage_type == FeatureUsageType.Continuous;
const isContinuous =
feature.config?.usage_type === FeatureUsageType.Continuous;
if (!isContinuous) {
return false;
}
let cusPrices = cusProductsToCusPrices({
const cusPrices = cusProductsToCusPrices({
cusProducts: fullCus.customer_products,
inStatuses: ACTIVE_STATUSES,
});
let hasPaid = cusPrices.some((cp) => {
let config = cp.price.config as UsagePriceConfig;
if (config.internal_feature_id == feature.internal_id) {
const hasPaid = cusPrices.some((cp) => {
const config = cp.price.config as UsagePriceConfig;
if (config.internal_feature_id === feature.internal_id) {
return true;
}
return false;
});
return hasPaid;

View File

@@ -1,22 +1,21 @@
import {
Customer,
Feature,
Invoice,
InvoiceItem,
type Customer,
type Feature,
type Invoice,
type InvoiceItem,
InvoiceItemResponseSchema,
InvoiceResponse,
InvoiceStatus,
LoggerAction,
Organization,
type InvoiceResponse,
type InvoiceStatus,
invoices,
type Organization,
stripeToAtmnAmount,
} from "@autumn/shared";
import Stripe from "stripe";
import { generateId } from "@/utils/genUtils.js";
import { getInvoiceDiscounts } from "@/external/stripe/stripeInvoiceUtils.js";
import { Autumn } from "autumn-js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { invoices } from "@autumn/shared";
import { and, desc, eq } from "drizzle-orm";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { getInvoiceDiscounts } from "@/external/stripe/stripeInvoiceUtils.js";
import { generateId } from "@/utils/genUtils.js";
export const processInvoice = ({
invoice,
@@ -38,7 +37,7 @@ export const processInvoice = ({
hosted_invoice_url: `${process.env.BETTER_AUTH_URL}/invoices/hosted_invoice_url/${invoice.id}`,
items: withItems
? (invoice.items || []).map((i) => {
let feature = features?.find(
const feature = features?.find(
(f) => f.internal_id === i.internal_feature_id,
);
@@ -135,11 +134,11 @@ export class InvoiceService {
// Convert product ids to unique product ids
const uniqueProductIds = [...new Set(productIds)];
const uniqueInternalProductIds = [...new Set(internalProductIds)];
let total = stripeInvoice.total / 100;
if (stripeInvoice.currency.toLowerCase() == "clp") {
total = stripeInvoice.total;
}
const atmnTotal = stripeToAtmnAmount({
amount: stripeInvoice.total,
currency: stripeInvoice.currency,
});
const invoice: Invoice = {
id: generateId("inv"),
@@ -153,7 +152,7 @@ export class InvoiceService {
internal_entity_id: internalEntityId || null,
// Stripe stuff
total,
total: atmnTotal,
currency: stripeInvoice.currency,
discounts: getInvoiceDiscounts({
expandedInvoice: stripeInvoice,
@@ -165,7 +164,7 @@ export class InvoiceService {
try {
await db.insert(invoices).values(invoice as any);
} catch (error: any) {
if (error.code == "23505") {
if (error.code === "23505") {
console.log(" 🧐 Invoice already exists");
return;
} else {
@@ -184,7 +183,7 @@ export class InvoiceService {
await autumn.track({
customer_id: org.id,
event_name: "revenue",
value: Math.round(stripeInvoice.total / 100),
value: atmnTotal,
customer_data: {
name: org.slug,
},

View File

@@ -1,27 +1,27 @@
import { getSubItemAmount } from "@/external/stripe/stripeSubUtils/getSubItemAmount.js";
import { findPriceInStripeItems } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import {
BillingType,
cusProductToEnts,
cusProductToPrices,
getFeatureInvoiceDescription,
PreviewLineItem,
UsagePriceConfig,
type PreviewLineItem,
stripeToAtmnAmount,
type UsagePriceConfig,
} from "@autumn/shared";
import { Decimal } from "decimal.js";
import Stripe from "stripe";
import { getProration } from "./getItemsForNewProduct.js";
import { calculateProrationAmount } from "../prorationUtils.js";
import { formatUnixToDate } from "@/utils/genUtils.js";
import { formatAmount } from "@/utils/formatUtils.js";
import type Stripe from "stripe";
import { getSubItemAmount } from "@/external/stripe/stripeSubUtils/getSubItemAmount.js";
import { findPriceInStripeItems } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
import { attachParamToCusProducts } from "@/internal/customers/attach/attachUtils/convertAttachParams.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { getExistingUsageFromCusProducts } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import {
priceToFeature,
priceToUsageModel,
} from "@/internal/products/prices/priceUtils/convertPrice.js";
import { attachParamToCusProducts } from "@/internal/customers/attach/attachUtils/convertAttachParams.js";
import { cusProductToEnts, cusProductToPrices } from "@autumn/shared";
import { getExistingUsageFromCusProducts } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import { getPriceEntitlement } from "@/internal/products/prices/priceUtils.js";
import { formatAmount } from "@/utils/formatUtils.js";
import { formatUnixToDate } from "@/utils/genUtils.js";
import { calculateProrationAmount } from "../prorationUtils.js";
import { getProration } from "./getItemsForNewProduct.js";
export const getCurContUseItems = async ({
sub,
@@ -38,8 +38,8 @@ export const getCurContUseItems = async ({
const curPrices = cusProductToPrices({ cusProduct: curCusProduct });
const curEnts = cusProductToEnts({ cusProduct: curCusProduct });
let items: PreviewLineItem[] = [];
let now = attachParams.now || Date.now();
const items: PreviewLineItem[] = [];
const now = attachParams.now || Date.now();
for (const item of sub.items.data) {
const price = findPriceInStripeItems({
@@ -52,7 +52,12 @@ export const getCurContUseItems = async ({
const periodEnd = item.current_period_end * 1000;
const totalAmountCents = getSubItemAmount({ subItem: item });
const totalAmount = new Decimal(totalAmountCents).div(100).toNumber();
const atmnTotalAmount = stripeToAtmnAmount({
amount: totalAmountCents,
currency: sub.currency,
});
const ent = getPriceEntitlement(price, curEnts);
if (now < periodEnd) {
@@ -71,7 +76,7 @@ export const getCurContUseItems = async ({
periodEnd: finalProration?.end,
periodStart: finalProration?.start,
now,
amount: totalAmount,
amount: atmnTotalAmount,
});
const existingUsage = getExistingUsageFromCusProducts({

View File

@@ -1,36 +1,27 @@
import { compareObjects, generateId, notNullish } from "@/utils/genUtils.js";
import {
BillWhen,
BillingInterval,
BillingType,
FixedPriceConfig,
Price,
PriceType,
UsagePriceConfig,
Entitlement,
EntitlementWithFeature,
FeatureOptions,
BillWhen,
type Entitlement,
type EntitlementWithFeature,
ErrCode,
FullProduct,
TierInfinite,
OnIncrease,
type FeatureOptions,
type FixedPriceConfig,
type FullProduct,
OnDecrease,
Product,
OnIncrease,
type Price,
PriceType,
type Product,
TierInfinite,
type UsagePriceConfig,
} from "@autumn/shared";
import RecaseError from "@/utils/errorUtils.js";
import { StatusCodes } from "http-status-codes";
import { Decimal } from "decimal.js";
import { StatusCodes } from "http-status-codes";
import RecaseError from "@/utils/errorUtils.js";
import { compareObjects, generateId, notNullish } from "@/utils/genUtils.js";
import { compareBillingIntervals } from "./priceUtils/priceIntervalUtils.js";
const BillingIntervalOrder = [
BillingInterval.Year,
BillingInterval.SemiAnnual,
BillingInterval.Quarter,
BillingInterval.Month,
BillingInterval.OneOff,
];
export const constructPrice = ({
internalProductId,
entitlementId,
@@ -54,7 +45,7 @@ export const constructPrice = ({
});
}
let newPrice: Price = {
const newPrice: Price = {
id: generateId("pr"),
org_id: orgId,
internal_product_id: internalProductId,
@@ -74,23 +65,23 @@ export const constructPrice = ({
export const getBillingType = (config: FixedPriceConfig | UsagePriceConfig) => {
// 1. Fixed cycle / one off
if (
config.type == PriceType.Fixed &&
config.interval == BillingInterval.OneOff
config.type === PriceType.Fixed &&
config.interval === BillingInterval.OneOff
) {
return BillingType.OneOff;
} else if (config.type == PriceType.Fixed) {
} else if (config.type === PriceType.Fixed) {
return BillingType.FixedCycle;
}
// 2. Prepaid
let usageConfig = config as UsagePriceConfig;
const usageConfig = config as UsagePriceConfig;
if (
usageConfig.bill_when == BillWhen.InAdvance ||
usageConfig.bill_when == BillWhen.StartOfPeriod
usageConfig.bill_when === BillWhen.InAdvance ||
usageConfig.bill_when === BillWhen.StartOfPeriod
) {
return BillingType.UsageInAdvance;
} else if (usageConfig.bill_when == BillWhen.EndOfPeriod) {
} else if (usageConfig.bill_when === BillWhen.EndOfPeriod) {
if (usageConfig.should_prorate) {
return BillingType.InArrearProrated;
}
@@ -140,7 +131,7 @@ export const getBillingInterval = (prices: Price[]) => {
// }))
// );
if (pricesCopy.length == 0) {
if (pricesCopy.length === 0) {
throw new RecaseError({
message: "No prices found, can't get billing interval",
code: ErrCode.InvalidRequest,
@@ -156,12 +147,12 @@ export const getBillingInterval = (prices: Price[]) => {
};
export const pricesOnlyOneOff = (prices: Price[]) => {
if (prices.length == 0) return false;
if (prices.length === 0) return false;
return prices.every((price) => {
let interval = price.config?.interval;
const interval = price.config?.interval;
if (!interval || interval != BillingInterval.OneOff) {
if (!interval || interval !== BillingInterval.OneOff) {
return false;
}
return true;
@@ -173,7 +164,7 @@ export const pricesContainRecurring = (prices: Price[]) => {
return prices.some((price) => {
const interval = price.config?.interval;
if (interval && interval != BillingInterval.OneOff) {
if (interval && interval !== BillingInterval.OneOff) {
return true;
}
@@ -201,17 +192,18 @@ export const getPriceEntitlement = (
entitlements: EntitlementWithFeature[],
allowFeatureMatch = false,
) => {
let config = price.config as UsagePriceConfig;
const config = price.config as UsagePriceConfig;
const entitlement = entitlements.find((ent) => {
let entIdMatch =
notNullish(price.entitlement_id) && price.entitlement_id == ent.id;
const entIdMatch =
notNullish(price.entitlement_id) && price.entitlement_id === ent.id;
let featureIdMatch =
const featureIdMatch =
notNullish(config.internal_feature_id) &&
config.internal_feature_id == ent.internal_feature_id;
config.internal_feature_id === ent.internal_feature_id;
let productIdMatch = ent.internal_product_id == price.internal_product_id;
const productIdMatch =
ent.internal_product_id === price.internal_product_id;
if (allowFeatureMatch) {
return (entIdMatch || featureIdMatch) && productIdMatch;
@@ -227,7 +219,7 @@ export const getPriceOptions = (
price: Price,
optionsList: FeatureOptions[],
) => {
let config = price.config as UsagePriceConfig;
const config = price.config as UsagePriceConfig;
const options = optionsList.find(
(options) => options.internal_feature_id === config.internal_feature_id,
@@ -241,7 +233,7 @@ export const pricesAreSame = (price1: Price, price2: Price) => {
const originalValue = (price1.config as any)[key];
const newValue = (price2.config as any)[key];
if (key == "usage_tiers") {
if (key === "usage_tiers") {
for (let i = 0; i < originalValue.length; i++) {
const originalTier = originalValue[i];
const newTier = newValue[i];
@@ -258,14 +250,14 @@ export const pricesAreSame = (price1: Price, price2: Price) => {
};
export const getUsageTier = (price: Price, quantity: number) => {
let usageConfig = price.config as UsagePriceConfig;
const usageConfig = price.config as UsagePriceConfig;
for (let i = 0; i < usageConfig.usage_tiers.length; i++) {
if (i == usageConfig.usage_tiers.length - 1) {
if (i === usageConfig.usage_tiers.length - 1) {
return usageConfig.usage_tiers[i];
}
let tier = usageConfig.usage_tiers[i];
if (tier.to == TierInfinite || tier.to >= quantity) {
const tier = usageConfig.usage_tiers[i];
if (tier.to === TierInfinite || tier.to >= quantity) {
return tier;
}
}
@@ -282,15 +274,15 @@ export const getPriceAmount = ({
relatedEnt?: EntitlementWithFeature;
quantity?: number;
}) => {
let billingType = getBillingType(price.config!);
if (billingType == BillingType.OneOff) {
let config = price.config as FixedPriceConfig;
const billingType = getBillingType(price.config!);
if (billingType === BillingType.OneOff) {
const config = price.config as FixedPriceConfig;
return Number(config.amount.toFixed(2));
} else if (billingType == BillingType.UsageInAdvance) {
let quantity = options?.quantity!;
let config = price.config as UsagePriceConfig;
} else if (billingType === BillingType.UsageInAdvance) {
const quantity = options?.quantity!;
const config = price.config as UsagePriceConfig;
let overage = new Decimal(quantity)
const overage = new Decimal(quantity)
.mul(config.billing_units || 1)
.toNumber();
@@ -301,19 +293,19 @@ export const getPriceAmount = ({
};
export const getPriceForOverage = (price: Price, overage?: number) => {
let usageConfig = price.config as UsagePriceConfig;
let billingType = getBillingType(usageConfig);
const usageConfig = price.config as UsagePriceConfig;
const billingType = getBillingType(usageConfig);
if (
billingType == BillingType.FixedCycle ||
billingType == BillingType.OneOff
billingType === BillingType.FixedCycle ||
billingType === BillingType.OneOff
) {
const config = price.config as FixedPriceConfig;
return config.amount;
}
let amount = 0;
let billingUnits = usageConfig.billing_units || 1;
const billingUnits = usageConfig.billing_units || 1;
let remainingUsage = new Decimal(
Math.ceil(new Decimal(overage!).div(billingUnits).toNumber()),
)
@@ -322,10 +314,10 @@ export const getPriceForOverage = (price: Price, overage?: number) => {
let lastTo: number = 0;
for (let i = 0; i < usageConfig.usage_tiers.length; i++) {
let tier = usageConfig.usage_tiers[i];
const tier = usageConfig.usage_tiers[i];
let amountUsed = 0;
if (tier.to == TierInfinite || tier.to == -1) {
if (tier.to === TierInfinite || tier.to === -1) {
amountUsed = remainingUsage;
} else {
amountUsed = Math.min(remainingUsage, tier.to - lastTo);
@@ -333,7 +325,7 @@ export const getPriceForOverage = (price: Price, overage?: number) => {
}
// Divide amount by billing units
let amountPerUnit = new Decimal(tier.amount)
const amountPerUnit = new Decimal(tier.amount)
.div(usageConfig.billing_units!)
.toNumber();
@@ -353,11 +345,11 @@ export const priceToEventName = (productName: string, featureName: string) => {
};
export const roundPriceAmounts = (price: Price) => {
if (price.config!.type == PriceType.Fixed) {
if (price.config!.type === PriceType.Fixed) {
const config = price.config as FixedPriceConfig;
config.amount = Number(config.amount.toFixed(10));
price.config = config;
} else if (price.config!.type == PriceType.Usage) {
} else if (price.config!.type === PriceType.Usage) {
const config = price.config as UsagePriceConfig;
for (let i = 0; i < config.usage_tiers.length; i++) {
config.usage_tiers[i].amount = Number(
@@ -373,13 +365,13 @@ export const priceIsOneOffAndTiered = (
price: Price,
relatedEnt: EntitlementWithFeature,
) => {
let config = price.config as UsagePriceConfig;
if (config.type == PriceType.Fixed) {
const config = price.config as UsagePriceConfig;
if (config.type === PriceType.Fixed) {
return false;
}
return (
config.interval == BillingInterval.OneOff && config.usage_tiers.length > 1
config.interval === BillingInterval.OneOff && config.usage_tiers.length > 1
);
};
@@ -391,13 +383,13 @@ export const getProductForPrice = (price: Price, products: FullProduct[]) => {
// Price to price / tiers
export const priceToAmountOrTiers = (price: Price) => {
if (price.config!.type == PriceType.Fixed) {
let config = price.config as FixedPriceConfig;
if (price.config!.type === PriceType.Fixed) {
const config = price.config as FixedPriceConfig;
return {
price: config.amount,
};
} else {
let config = price.config as UsagePriceConfig;
const config = price.config as UsagePriceConfig;
if (config.usage_tiers.length > 1) {
return {
tiers: config.usage_tiers,
@@ -417,7 +409,7 @@ export const roundUsage = ({
usage: number;
billingUnits: number;
}) => {
if (!billingUnits || billingUnits == 1) {
if (!billingUnits || billingUnits === 1) {
return usage;
}
@@ -435,24 +427,24 @@ export const formatPrice = ({
price: Price;
product?: Product;
}) => {
if (price.config.type == PriceType.Fixed) {
if (price.config.type === PriceType.Fixed) {
const config = price.config as FixedPriceConfig;
const formatted = `${config.amount}${config.interval == BillingInterval.OneOff ? "(one off)" : `/ ${config.interval}`}`;
const formatted = `${config.amount}${config.interval === BillingInterval.OneOff ? "(one off)" : `/ ${config.interval}`}`;
if (product) {
return `${product.name} - ${formatted}`;
}
return formatted;
} else {
const config = price.config as UsagePriceConfig;
let billingType = getBillingType(config);
let formatBillingType = {
const billingType = getBillingType(config);
const formatBillingType = {
[BillingType.UsageInAdvance]: "prepaid",
[BillingType.UsageInArrear]: "usage",
[BillingType.InArrearProrated]: "cont_use",
[BillingType.FixedCycle]: "cont_use",
};
let featureId = config.feature_id;
const featureId = config.feature_id;
const formatted = `${formatBillingType[billingType as keyof typeof formatBillingType]} price for feature ${featureId}: $${config.usage_tiers[0].amount}${config.billing_units ? ` ${config.billing_units}` : ""}`;
if (product) {

View File

@@ -1,7 +1,11 @@
import Stripe from "stripe";
import { constructPrice } from "../priceUtils.js";
import { FullProduct, PriceType } from "@autumn/shared";
import {
type FullProduct,
PriceType,
stripeToAtmnAmount,
} from "@autumn/shared";
import type Stripe from "stripe";
import { subItemToAutumnInterval } from "@/external/stripe/utils.js";
import { constructPrice } from "../priceUtils.js";
export const subItemToFixedPrice = ({
subItem,
@@ -15,13 +19,19 @@ export const subItemToFixedPrice = ({
const { price } = subItem;
const { interval, intervalCount } = subItemToAutumnInterval(subItem);
const atmnAmount = stripeToAtmnAmount({
amount: price.unit_amount || 0,
currency: price.currency,
});
return constructPrice({
internalProductId: product.internal_id,
isCustom: true,
orgId: product.org_id,
fixedConfig: {
type: PriceType.Fixed,
amount: basePrice || (price.unit_amount || 0) / 100,
amount: basePrice || atmnAmount,
interval,
interval_count: intervalCount,
stripe_price_id: price.id,

View File

@@ -1,18 +1,16 @@
import {
BillingType,
EntitlementWithFeature,
Feature,
UsageModel,
UsagePriceConfig,
BillingInterval,
CustomerPrice,
FullCustomerEntitlement,
FullCustomerPrice,
ProductOptions,
FullProduct,
BillingType,
type EntitlementWithFeature,
type Feature,
type FullCustomerEntitlement,
type FullCustomerPrice,
type FullProduct,
type Price,
type ProductOptions,
UsageModel,
type UsagePriceConfig,
} from "@autumn/shared";
import { Price } from "@autumn/shared";
import { getBillingType, getPriceEntitlement } from "../priceUtils.js";
import { isFixedPrice } from "./usagePriceUtils/classifyUsagePrice.js";
@@ -30,19 +28,19 @@ export const toIntervalKey = ({
interval: BillingInterval;
intervalCount: number;
}) => {
if (interval == BillingInterval.OneOff) {
if (interval === BillingInterval.OneOff) {
return BillingInterval.OneOff;
} else if (interval == BillingInterval.Quarter) {
let finalCount = (intervalCount ?? 1) * 3;
} else if (interval === BillingInterval.Quarter) {
const finalCount = (intervalCount ?? 1) * 3;
return `${BillingInterval.Month}-${finalCount}`;
} else if (interval == BillingInterval.SemiAnnual) {
let finalCount = (intervalCount ?? 1) * 6;
} else if (interval === BillingInterval.SemiAnnual) {
const finalCount = (intervalCount ?? 1) * 6;
return `${BillingInterval.Month}-${finalCount}`;
}
if (interval == BillingInterval.Week) {
if (interval === BillingInterval.Week) {
return `${BillingInterval.Week}-${intervalCount}`;
} else if (interval == BillingInterval.Year) {
} else if (interval === BillingInterval.Year) {
return `${BillingInterval.Year}-${intervalCount}`;
}
return `${interval}-${intervalCount}`;
@@ -72,7 +70,8 @@ export const priceToFeature = ({
if (features) {
return features.find(
(f) =>
f.internal_id == (price.config as UsagePriceConfig).internal_feature_id,
f.internal_id ===
(price.config as UsagePriceConfig).internal_feature_id,
);
}
@@ -81,11 +80,11 @@ export const priceToFeature = ({
};
export const priceToUsageModel = (price: Price) => {
let billingType = getBillingType(price.config);
const billingType = getBillingType(price.config);
if (isFixedPrice({ price })) {
return undefined;
}
if (billingType == BillingType.UsageInAdvance) {
if (billingType === BillingType.UsageInAdvance) {
return UsageModel.Prepaid;
}
return UsageModel.PayPerUse;
@@ -99,7 +98,7 @@ export const cusPriceToCusEnt = ({
cusEnts: FullCustomerEntitlement[];
}) => {
return cusEnts.find(
(ce) => ce.entitlement?.id == cusPrice.price.entitlement_id,
(ce) => ce.entitlement?.id === cusPrice.price.entitlement_id,
);
};
@@ -115,9 +114,9 @@ export const priceToProductOptions = ({
if (!options) return undefined;
const productId = products.find(
(p) => p.internal_id == price.internal_product_id,
(p) => p.internal_id === price.internal_product_id,
)?.id;
const productOptions = options.find((o) => o.product_id == productId);
const productOptions = options.find((o) => o.product_id === productId);
return productOptions;
};

View File

@@ -1,25 +1,25 @@
import {
type AppEnv,
type CreateReward,
DiscountConfigSchema,
ErrCode,
type Organization,
type Price,
type Product,
type Reward,
RewardCategory,
RewardType,
stripeToAtmnAmount,
} from "@autumn/shared";
import { Decimal } from "decimal.js";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import RecaseError from "@/utils/errorUtils.js";
import { generateId, getUnique, nullish } from "@/utils/genUtils.js";
import {
Reward,
CreateReward,
RewardType,
RewardCategory,
ErrCode,
DiscountConfigSchema,
Price,
Organization,
AppEnv,
Product,
} from "@autumn/shared";
import { ProductService } from "../products/ProductService.js";
import { initProductInStripe } from "../products/productUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import Stripe from "stripe";
import { Decimal } from "decimal.js";
import { isFixedPrice } from "../products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import { formatPrice } from "../products/prices/priceUtils.js";
import { initProductInStripe } from "../products/productUtils.js";
export const constructReward = ({
internalId,
@@ -50,7 +50,7 @@ export const constructReward = ({
DiscountConfigSchema.parse(reward.discount_config);
}
let promoCodes = reward.promo_codes.filter((promoCode) => {
const promoCodes = reward.promo_codes.filter((promoCode) => {
return promoCode.code.length > 0;
});
@@ -67,7 +67,7 @@ export const constructReward = ({
};
}
let newReward = {
const newReward = {
...reward,
...configData,
internal_id: internalId || generateId("rew"),
@@ -96,7 +96,7 @@ export enum CouponType {
export const getCouponType = (reward: Reward) => {
if (!reward) return null;
let config = reward.discount_config;
const config = reward.discount_config;
if (nullish(config)) {
return null;
}
@@ -131,7 +131,7 @@ export const initRewardStripePrices = async ({
env: AppEnv;
logger: any;
}) => {
let pricesToInit = prices.map((p: Price) =>
const pricesToInit = prices.map((p: Price) =>
nullish(p.config.stripe_price_id),
);
@@ -139,10 +139,10 @@ export const initRewardStripePrices = async ({
return;
}
let internalProductIds = getUnique(
const internalProductIds = getUnique(
prices.map((p: Price) => p.internal_product_id),
);
let products = await ProductService.listByInternalIds({
const products = await ProductService.listByInternalIds({
db,
internalIds: internalProductIds,
});
@@ -162,7 +162,7 @@ export const initRewardStripePrices = async ({
await Promise.all(batchInit);
for (const price of prices) {
let product = products.find(
const product = products.find(
(p) => p.internal_id === price.internal_product_id,
);
@@ -174,7 +174,7 @@ export const initRewardStripePrices = async ({
export const formatReward = ({ reward }: { reward: Reward }) => {
if (!reward) return "";
const discountString =
reward.type == RewardType.PercentageDiscount
reward.type === RewardType.PercentageDiscount
? `${reward.discount_config?.discount_value}%`
: `${reward.discount_config?.discount_value} off`;
@@ -191,10 +191,12 @@ export const getAmountAfterReward = ({
amount,
reward,
subDiscounts,
currency,
}: {
amount: number;
reward: Reward;
subDiscounts: Stripe.Discount[];
currency?: string;
}) => {
if (subDiscounts.find((d) => d.coupon?.id === reward.id)) {
return amount;
@@ -204,7 +206,14 @@ export const getAmountAfterReward = ({
const discountValue = new Decimal(
reward.discount_config?.discount_value ?? 0,
);
const discountRatio = new Decimal(1).minus(discountValue.div(100));
const atmnDiscountValue = stripeToAtmnAmount({
amount: discountValue.toNumber(),
currency,
});
const discountRatio = new Decimal(1).minus(atmnDiscountValue);
return new Decimal(amount).mul(discountRatio).toNumber();
} else if (reward.type === RewardType.FixedDiscount) {
const discountAmount = new Decimal(
@@ -267,11 +276,13 @@ export const getAmountAfterStripeDiscounts = ({
amount,
product,
stripeDiscounts,
currency,
}: {
price: Price;
product: Product;
amount: number;
stripeDiscounts: Stripe.Discount[];
currency?: string;
}) => {
let amountAfterDiscount = amount;
@@ -291,8 +302,13 @@ export const getAmountAfterStripeDiscounts = ({
.toNumber();
} else if (coupon.amount_off) {
// must do some ratio ting here...
const atmnDiscountAmount = stripeToAtmnAmount({
amount: coupon.amount_off,
currency: currency,
});
amountAfterDiscount = new Decimal(amountAfterDiscount)
.minus(new Decimal(coupon.amount_off).div(100))
.minus(atmnDiscountAmount)
.toNumber();
}
}

View File

@@ -1,5 +1,6 @@
import { stripeToAtmnAmount } from "@autumn/shared";
import type Stripe from "stripe";
import { subItemToAutumnInterval } from "@/external/stripe/utils.js";
import Stripe from "stripe";
export const logSubItems = ({
sub,
@@ -8,16 +9,20 @@ export const logSubItems = ({
sub?: Stripe.Subscription;
subItems?: Stripe.SubscriptionItem[];
}) => {
let finalSubItems = subItems || sub!.items.data;
const finalSubItems = subItems || sub!.items.data;
for (const item of finalSubItems) {
let isMetered = item.price.recurring?.usage_type === "metered";
let isTiered = item.price.billing_scheme === "tiered";
const isMetered = item.price.recurring?.usage_type === "metered";
const atmnPrice = stripeToAtmnAmount({
amount: item.price.unit_amount || 0,
currency: item.price.currency,
});
if (isMetered) {
console.log(`Usage price`);
} else {
let price = item.price.unit_amount! / 100;
let subInterval = subItemToAutumnInterval(item);
const price = atmnPrice;
const subInterval = subItemToAutumnInterval(item);
console.log(
`${price} ${item.price.currency}${item.quantity !== 1 ? ` x ${item.quantity}` : ""} / ${subInterval?.intervalCount} ${subInterval?.interval}`,
);

View File

@@ -1,6 +1,7 @@
import { products } from "tests/global.js";
import { expect } from "chai";
import chalk from "chalk";
import { Decimal } from "decimal.js";
import { AutumnCli } from "tests/cli/AutumnCli.js";
import { features, oneTimeProducts } from "tests/global.js";
import { compareMainProduct } from "tests/utils/compare.js";
@@ -11,15 +12,13 @@ import {
} from "tests/utils/genUtils.js";
import { initCustomer } from "tests/utils/init.js";
import { completeCheckoutForm } from "tests/utils/stripeUtils.js";
import { Decimal } from "decimal.js";
import { expect } from "chai";
const testCase = "basic10";
describe(`${chalk.yellowBright("basic10: Multi attach, all one off")}`, () => {
let customerId = testCase;
let quantity = 1000;
let options = [
const customerId = testCase;
const quantity = 1000;
const options = [
{
feature_id: features.metered2.id,
quantity,
@@ -34,7 +33,7 @@ describe(`${chalk.yellowBright("basic10: Multi attach, all one off")}`, () => {
});
});
it("should attach monthly with one time", async function () {
it("should attach monthly with one time", async () => {
const res = await AutumnCli.attach({
customerId,
productIds: [
@@ -48,7 +47,7 @@ describe(`${chalk.yellowBright("basic10: Multi attach, all one off")}`, () => {
await timeout(20000);
});
it("should have correct main product and entitlements", async function () {
it("should have correct main product and entitlements", async () => {
const cusRes = await AutumnCli.getCustomer(customerId);
compareMainProduct({
@@ -71,7 +70,7 @@ describe(`${chalk.yellowBright("basic10: Multi attach, all one off")}`, () => {
const metered2Amount = metered2Tiers[0].amount;
let numBillingUnits = new Decimal(options[0].quantity).div(
const numBillingUnits = new Decimal(options[0].quantity).div(
oneTimeProducts.oneTimeMetered2.prices[0].config.billing_units,
);

View File

@@ -1,19 +1,19 @@
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { expect } from "chai";
import chalk from "chalk";
import { setupBefore } from "tests/before.js";
import { AutumnCli } from "tests/cli/AutumnCli.js";
import { products } from "tests/global.js";
import { compareMainProduct } from "tests/utils/compare.js";
import { completeCheckoutForm } from "tests/utils/stripeUtils.js";
import { timeout } from "tests/utils/genUtils.js";
import { completeCheckoutForm } from "tests/utils/stripeUtils.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
// UNCOMMENT FROM HERE
const testCase = "basic2";
describe(`${chalk.yellowBright("basic2: Testing attach pro")}`, () => {
let customerId = testCase;
let autumn: AutumnInt = new AutumnInt();
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt();
let db, org, env;
before(async function () {
@@ -33,7 +33,7 @@ describe(`${chalk.yellowBright("basic2: Testing attach pro")}`, () => {
});
});
it("should attach pro through checkout", async function () {
it("should attach pro through checkout", async () => {
const { checkout_url } = await autumn.attach({
customer_id: customerId,
product_id: products.pro.id,
@@ -43,7 +43,7 @@ describe(`${chalk.yellowBright("basic2: Testing attach pro")}`, () => {
await timeout(12000);
});
it("should have correct product & entitlements", async function () {
it("should have correct product & entitlements", async () => {
const res = await AutumnCli.getCustomer(customerId);
compareMainProduct({
sent: products.pro,
@@ -52,7 +52,7 @@ describe(`${chalk.yellowBright("basic2: Testing attach pro")}`, () => {
expect(res.invoices.length).to.be.greaterThan(0);
});
it("should have correct result when calling /check", async function () {
it("should have correct result when calling /check", async () => {
const proEntitlements = products.pro.entitlements;
for (const entitlement of Object.values(proEntitlements)) {

View File

@@ -1,41 +1,41 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { expect } from "chai";
import chalk from "chalk";
import { setupBefore } from "tests/before.js";
import { AutumnCli } from "tests/cli/AutumnCli.js";
import { features, products } from "tests/global.js";
import { compareMainProduct } from "tests/utils/compare.js";
import { completeCheckoutForm } from "tests/utils/stripeUtils.js";
import { timeout } from "tests/utils/genUtils.js";
import { constructRawProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js";
import { createProducts } from "tests/utils/productUtils.js";
import { completeCheckoutForm } from "tests/utils/stripeUtils.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js";
import { constructRawProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
// const oneTimeQuantity = 2;
// const oneTimePurchaseCount = 2;
// const oneTimeOverrideQuantity = 4;
// const monthlyQuantity = 2;
let oneTimeItem = constructPrepaidItem({
const oneTimeItem = constructPrepaidItem({
featureId: features.metered1.id,
price: 9,
billingUnits: 250,
isOneOff: true,
});
let oneTime = constructRawProduct({
const oneTime = constructRawProduct({
id: "basic3_one_off",
items: [oneTimeItem],
isAddOn: true,
});
let monthlyItem = constructPrepaidItem({
const monthlyItem = constructPrepaidItem({
featureId: features.metered1.id,
price: 9,
billingUnits: 250,
});
let monthly = constructRawProduct({
const monthly = constructRawProduct({
id: "basic3_monthly",
items: [
constructPrepaidItem({
@@ -49,8 +49,8 @@ let monthly = constructRawProduct({
// UNCOMMENT FROM HERE
const testCase = "basic3";
describe(`${chalk.yellowBright("basic3: Testing attach one time / monthly add ons")}`, () => {
let customerId = testCase;
let autumn: AutumnInt = new AutumnInt();
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt();
let db, org, env;
before(async function () {
@@ -77,7 +77,7 @@ describe(`${chalk.yellowBright("basic3: Testing attach one time / monthly add on
});
});
it("should attach pro", async function () {
it("should attach pro", async () => {
await autumn.attach({
customer_id: customerId,
product_id: products.pro.id,
@@ -94,7 +94,7 @@ describe(`${chalk.yellowBright("basic3: Testing attach one time / monthly add on
const oneTimeBillingUnits = oneTimeItem.billing_units;
const oneTimePurchaseCount = 2;
it("should attach one time add on twice, force checkout", async function () {
it("should attach one time add on twice, force checkout", async () => {
for (let i = 0; i < 2; i++) {
const res = await autumn.attach({
customer_id: customerId,
@@ -110,13 +110,13 @@ describe(`${chalk.yellowBright("basic3: Testing attach one time / monthly add on
}
});
it("should have correct product & entitlements", async function () {
it("should have correct product & entitlements", async () => {
const cusRes = await AutumnCli.getCustomer(customerId);
const addOnBalance = cusRes.entitlements.find(
(e: any) =>
e.feature_id === features.metered1.id &&
e.interval ==
e.interval ===
products.oneTimeAddOnMetered1.entitlements.metered1.interval,
);
@@ -141,7 +141,7 @@ describe(`${chalk.yellowBright("basic3: Testing attach one time / monthly add on
);
});
it("should have correct /check result for metered1", async function () {
it("should have correct /check result for metered1", async () => {
const res: any = await AutumnCli.entitled(customerId, features.metered1.id);
expect(res!.allowed).to.be.true;

View File

@@ -1,42 +1,40 @@
import { APIVersion, type AppEnv, type Organization } from "@autumn/shared";
import chalk from "chalk";
import Stripe from "stripe";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { APIVersion, AppEnv, Organization } from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
import type Stripe from "stripe";
import { setupBefore } from "tests/before.js";
import { createProducts } from "tests/utils/productUtils.js";
import { addPrefixToProducts } from "../utils.js";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { expectDowngradeCorrect } from "tests/utils/expectUtils/expectScheduleUtils.js";
import { expectNextCycleCorrect } from "tests/utils/expectUtils/expectScheduleUtils.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import {
expectDowngradeCorrect,
expectNextCycleCorrect,
} from "tests/utils/expectUtils/expectScheduleUtils.js";
import { createProducts } from "tests/utils/productUtils.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";
import { addPrefixToProducts } from "../utils.js";
const testCase = "downgrade1";
let pro = constructProduct({
const pro = constructProduct({
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "pro",
});
let premium = constructProduct({
const premium = constructProduct({
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "premium",
});
describe(`${chalk.yellowBright(`${testCase}: Testing downgrade from premium -> pro`)}`, () => {
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;
let curUnix = new Date().getTime();
before(async function () {
await setupBefore(this);
const { autumnJs } = this;
@@ -72,7 +70,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing downgrade from premium -> p
testClockId = testClockId1!;
});
it("should attach premium product", async function () {
it("should attach premium product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
@@ -87,7 +85,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing downgrade from premium -> p
// let nextCycle = Date.now();
let preview = null;
it("should downgrade to pro", async function () {
it("should downgrade to pro", async () => {
const { preview: preview_ } = await expectDowngradeCorrect({
autumn,
customerId,
@@ -102,7 +100,7 @@ describe(`${chalk.yellowBright(`${testCase}: Testing downgrade from premium -> p
preview = preview_;
});
it("should have pro attached on next cycle", async function () {
it("should have pro attached on next cycle", async () => {
await expectNextCycleCorrect({
preview: preview!,
autumn,

View File

@@ -1,24 +1,21 @@
import chalk from "chalk";
import Stripe from "stripe";
import { CusProductStatus } from "@autumn/shared";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { expect } from "chai";
import chalk from "chalk";
import { addHours, addMonths } from "date-fns";
import type Stripe from "stripe";
import { setupBefore } from "tests/before.js";
import { AutumnCli } from "tests/cli/AutumnCli.js";
import { products } from "tests/global.js";
import { expect } from "chai";
import { compareMainProduct } from "tests/utils/compare.js";
import { addHours, addMonths } from "date-fns";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { setupBefore } from "tests/before.js";
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
const testCase = "downgrade5";
describe(`${chalk.yellowBright(`${testCase}: testing basic downgrade (paid to paid)`)}`, () => {
let customerId = testCase;
let autumn: AutumnInt = new AutumnInt();
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt();
let testClockId: string;
let stripeCli: Stripe;
@@ -40,21 +37,21 @@ describe(`${chalk.yellowBright(`${testCase}: testing basic downgrade (paid to pa
testClockId = testClockId_;
});
it("should attach premium", async function () {
it("should attach premium", async () => {
await AutumnCli.attach({
customerId: customerId,
productId: products.premium.id,
});
});
it("should attach pro", async function () {
it("should attach pro", async () => {
await AutumnCli.attach({
customerId: customerId,
productId: products.pro.id,
});
});
it("should have correct product and entitlements for scheduled pro", async function () {
it("should have correct product and entitlements for scheduled pro", async () => {
const res = await AutumnCli.getCustomer(customerId);
compareMainProduct({
@@ -72,7 +69,7 @@ describe(`${chalk.yellowBright(`${testCase}: testing basic downgrade (paid to pa
expect(resPro).to.exist;
});
it("should attach premium and remove scheduled pro", async function () {
it("should attach premium and remove scheduled pro", async () => {
await AutumnCli.attach({
customerId: customerId,
productId: products.premium.id,
@@ -93,7 +90,7 @@ describe(`${chalk.yellowBright(`${testCase}: testing basic downgrade (paid to pa
});
// Advance time 1 month
it("should attach pro, advance stripe clock and have pro is attached", async function () {
it("should attach pro, advance stripe clock and have pro is attached", async () => {
await AutumnCli.attach({
customerId: customerId,
productId: products.pro.id,

View File

@@ -1,33 +1,33 @@
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import {
APIVersion,
AppEnv,
Customer,
type AppEnv,
type Customer,
OnDecrease,
OnIncrease,
Organization,
type Organization,
} from "@autumn/shared";
import { expect } from "chai";
import chalk from "chalk";
import Stripe from "stripe";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { addHours, addMonths } from "date-fns";
import type Stripe from "stripe";
import { setupBefore } from "tests/before.js";
import { createProducts } from "tests/utils/productUtils.js";
import { addPrefixToProducts } from "../utils.js";
import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
import { addHours, addMonths } from "date-fns";
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
import { expect } from "chai";
import { createProducts } from "tests/utils/productUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { getMainCusProduct } from "@/internal/customers/cusProducts/cusProductUtils.js";
import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
import { addPrefixToProducts } from "../utils.js";
const testCase = "prepaid1";
export let pro = constructProduct({
export const pro = constructProduct({
items: [
constructPrepaidItem({
featureId: TestFeature.Messages,
@@ -44,13 +44,13 @@ export let pro = constructProduct({
});
describe(`${chalk.yellowBright(`attach/${testCase}: update quantity, no proration downgrade, single use`)}`, () => {
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;
let curUnix = new Date().getTime();
const curUnix = new Date().getTime();
let customer: Customer;
before(async function () {
@@ -95,7 +95,7 @@ describe(`${chalk.yellowBright(`attach/${testCase}: update quantity, no proratio
},
];
it("should attach pro product to customer", async function () {
it("should attach pro product to customer", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
@@ -107,14 +107,14 @@ describe(`${chalk.yellowBright(`attach/${testCase}: update quantity, no proratio
options,
});
let customer = await autumn.customers.get(customerId);
const customer = await autumn.customers.get(customerId);
expectProductAttached({
customer,
product: pro,
});
});
it("should reduce quantity to 200 and have correct sub item quantity + cus product quantity", async function () {
it("should reduce quantity to 200 and have correct sub item quantity + cus product quantity", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
@@ -132,7 +132,7 @@ describe(`${chalk.yellowBright(`attach/${testCase}: update quantity, no proratio
});
});
it("should increase quantity to 400 and have correct sub item quantity + invoice..", async function () {
it("should increase quantity to 400 and have correct sub item quantity + invoice..", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
@@ -152,7 +152,7 @@ describe(`${chalk.yellowBright(`attach/${testCase}: update quantity, no proratio
});
const newQuantity = 200;
it("should decrease quantity to 200, advance clock to next cycle and have correct balance", async function () {
it("should decrease quantity to 200, advance clock to next cycle and have correct balance", async () => {
await attachAndExpectCorrect({
autumn,
customerId,

View File

@@ -1,17 +1,21 @@
import Stripe from "stripe";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
import { cusProductToPrices } from "@autumn/shared";
import { CusService } from "@/internal/customers/CusService.js";
import { findContUsePrice } from "@/internal/products/prices/priceUtils/findPriceUtils.js";
import { AppEnv, FullCustomer, Organization } from "@autumn/shared";
import {
type AppEnv,
cusProductToPrices,
type FullCustomer,
type Organization,
} from "@autumn/shared";
import { expect } from "chai";
import type Stripe from "stripe";
import { TestFeature } from "tests/setup/v2Features.js";
import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { notNullish } from "@/utils/genUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import type { AutumnInt } from "@/external/autumn/autumnCli.js";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js";
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
import { CusService } from "@/internal/customers/CusService.js";
import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js";
import { findContUsePrice } from "@/internal/products/prices/priceUtils/findPriceUtils.js";
import { notNullish } from "@/utils/genUtils.js";
export const expectSubQuantityCorrect = async ({
stripeCli,
@@ -41,21 +45,21 @@ export const expectSubQuantityCorrect = async ({
idOrInternalId: customerId,
});
let cusProduct = fullCus.customer_products.find(
const cusProduct = fullCus.customer_products.find(
(cp) => cp.product_id === productId,
);
let stripeSubs = await getStripeSubs({
const stripeSubs = await getStripeSubs({
stripeCli,
subIds: cusProduct?.subscription_ids,
});
let subItems = stripeSubs.flatMap((sub) => sub.items.data);
let prices = cusProductToPrices({ cusProduct: cusProduct! });
const subItems = stripeSubs.flatMap((sub) => sub.items.data);
const prices = cusProductToPrices({ cusProduct: cusProduct! });
let contPrice = findContUsePrice({ prices });
const contPrice = findContUsePrice({ prices });
let subItem = findStripeItemForPrice({
const subItem = findStripeItemForPrice({
price: contPrice!,
stripeItems: subItems,
});
@@ -67,13 +71,13 @@ export const expectSubQuantityCorrect = async ({
);
// Check num replaceables correct
let cusEnts = cusProduct?.customer_entitlements;
let cusEnt = cusEnts?.find((ent) => ent.feature_id === TestFeature.Users);
const cusEnts = cusProduct?.customer_entitlements;
const cusEnt = cusEnts?.find((ent) => ent.feature_id === TestFeature.Users);
expect(cusEnt).to.exist;
expect(cusEnt?.replaceables.length).to.equal(numReplaceables);
let expectedBalance = cusEnt!.entitlement.allowance! - usage;
const expectedBalance = cusEnt!.entitlement.allowance! - usage;
expect(cusEnt!.balance).to.equal(expectedBalance);
return {
@@ -100,7 +104,7 @@ export const expectUpcomingItemsCorrect = async ({
expectedNumItems: number;
quantity: number;
}) => {
let sub = stripeSubs[0];
const sub = stripeSubs[0];
// let upcomingLines = await stripeCli.invoices.listUpcomingLines({
// subscription: sub.id,
// });
@@ -114,9 +118,9 @@ export const expectUpcomingItemsCorrect = async ({
const { start, end } = subToPeriodStartEnd({ sub });
let amount = quantity * unitPrice!;
const amount = quantity * unitPrice!;
let proratedAmount = calculateProrationAmount({
const proratedAmount = calculateProrationAmount({
amount,
periodStart: start * 1000,
periodEnd: end * 1000,
@@ -124,14 +128,6 @@ export const expectUpcomingItemsCorrect = async ({
allowNegative: true,
});
// console.group();
// console.group("Upcoming lines");
// for (const line of lines) {
// console.log(line.description, line.amount / 100);
// }
// console.groupEnd();
// console.groupEnd();
const firstItem = lineItems.data[0];
expect(firstItem.amount).to.equal(Math.round(proratedAmount * 100));
};
@@ -153,11 +149,11 @@ export const calcProrationAndExpectInvoice = async ({
curUnix: number;
numInvoices: number;
}) => {
let customer = await autumn.customers.get(customerId);
let invoices = customer.invoices;
const customer = await autumn.customers.get(customerId);
const invoices = customer.invoices;
let sub = stripeSubs[0];
let amount = quantity * unitPrice;
const sub = stripeSubs[0];
const amount = quantity * unitPrice;
const { start, end } = subToPeriodStartEnd({ sub });
let proratedAmount = calculateProrationAmount({
amount,

View File

@@ -1,23 +1,21 @@
// Cus ent utils
export * from "./cusEntUtils/sortCusEntsForDeduction.js";
// Cus product utils
export * from "./cusProductUtils/classifyCusProduct.js";
export * from "./cusProductUtils/convertCusProduct.js";
export * from "./cusProductUtils/productIdToCusProduct.js";
export * from "./cusProductUtils/cusProductConstants.js";
export * from "./cusProductUtils/cusProductUtils.js";
export * from "./cusProductUtils/formatCusProductUtils.js";
export * from "./utils.js";
// Cus ent utils
export * from "./cusEntUtils/balanceUtils.js";
export * from "./cusEntUtils/sortCusEntsForDeduction.js";
// Cus product utils
export * from "./cusProductUtils/classifyCusProduct.js";
export * from "./cusProductUtils/convertCusProduct.js";
export * from "./cusProductUtils/cusProductConstants.js";
export * from "./cusProductUtils/cusProductUtils.js";
export * from "./cusProductUtils/formatCusProductUtils.js";
export * from "./cusProductUtils/productIdToCusProduct.js";
// Product utils
export * from "./productUtils/convertUtils.js";
export * from "./productUtils/priceUtils/convertAmountUtils.js";
export * from "./productUtils/priceUtils.js";
export * from "./productV2Utils/mapToProductV2.js";
// Item utils
export * from "./productV2Utils/productItemUtils/mapToItem.js";
export * from "./productV2Utils/productItemUtils/productItemUtils.js";
export * from "./productV2Utils/mapToProductV2.js";
// Product utils
export * from "./productUtils/convertUtils.js";
export * from "./productUtils/priceUtils.js";
export * from "./utils.js";

View File

@@ -1,5 +1,8 @@
import { Entitlement } from "../../models/productModels/entModels/entModels.js";
import { Price } from "../../models/productModels/priceModels/priceModels.js";
import type {
Entitlement,
EntitlementWithFeature,
} from "../../models/productModels/entModels/entModels.js";
import type { Price } from "../../models/productModels/priceModels/priceModels.js";
// export const getEntRelatedPrice = (
// entitlement: Entitlement,
@@ -43,7 +46,7 @@ export const priceToEnt = ({
entitlements,
}: {
price: Price;
entitlements: Entitlement[];
entitlements: EntitlementWithFeature[];
}) => {
return entitlements.find(
(ent) =>

View File

@@ -0,0 +1,98 @@
import { Decimal } from "decimal.js";
/**
* Zero-decimal currencies that Stripe handles without decimal places.
* These currencies don't require multiplying/dividing by 100.
*/
const ZERO_DECIMAL_CURRENCIES = [
"BIF", // Burundian Franc
"CLP", // Chilean Peso
"DJF", // Djiboutian Franc
"GNF", // Guinean Franc
"JPY", // Japanese Yen
"KMF", // Comorian Franc
"KRW", // South Korean Won
"MGA", // Malagasy Ariary
"PYG", // Paraguayan Guaraní
"RWF", // Rwandan Franc
"UGX", // Ugandan Shilling
"VND", // Vietnamese Đồng
"VUV", // Vanuatu Vatu
"XAF", // Central African CFA Franc
"XOF", // West African CFA Franc
"XPF", // CFP Franc
];
/**
* Converts an Autumn amount to a Stripe amount.
* For most currencies, multiplies by 100 (e.g., $1.00 -> 100 cents).
* For zero-decimal currencies like JPY, returns the amount as-is.
*/
export const atmnToStripeAmount = ({
amount,
currency = "USD",
}: {
amount: number;
currency?: string;
}): number => {
if (ZERO_DECIMAL_CURRENCIES.includes(currency.toUpperCase())) {
return amount;
}
return new Decimal(amount).mul(100).round().toNumber();
};
/**
* Converts an Autumn amount to a Stripe decimal string.
* For most currencies, multiplies by 100 and returns as string with decimal places.
* For zero-decimal currencies like JPY, returns the amount as-is with decimal places.
* Used for Stripe API calls that require unit_amount_decimal as a string.
*/
export const atmnToStripeAmountDecimal = ({
amount,
currency = "USD",
decimalPlaces = 10,
}: {
amount: number | Decimal;
currency?: string;
decimalPlaces?: number;
}): string => {
const decimal = amount instanceof Decimal ? amount : new Decimal(amount);
if (ZERO_DECIMAL_CURRENCIES.includes(currency.toUpperCase())) {
return decimal.toDecimalPlaces(decimalPlaces).toString();
}
return decimal.mul(100).toDecimalPlaces(decimalPlaces).toString();
};
/**
* Converts a Stripe amount to an Autumn amount.
* For most currencies, divides by 100 (e.g., 100 cents -> $1.00).
* For zero-decimal currencies like JPY, returns the amount as-is.
*/
export const stripeToAtmnAmount = ({
amount,
currency = "usd",
decimalPlaces = 10,
round = true,
}: {
amount: number;
currency?: string;
decimalPlaces?: number;
round?: boolean;
}): number => {
let finalAmount = amount;
if (!ZERO_DECIMAL_CURRENCIES.includes(currency.toUpperCase())) {
finalAmount = new Decimal(amount).div(100).toNumber();
}
if (round) {
return new Decimal(finalAmount).toDecimalPlaces(decimalPlaces).toNumber();
}
if (decimalPlaces) {
return new Decimal(finalAmount).toDecimalPlaces(decimalPlaces).toNumber();
}
return finalAmount;
};

View File

@@ -1,24 +1,25 @@
import AddCouponDialogContent from "../../components/add-coupon/AddCouponDialogContent";
import { stripeToAtmnAmount } from "@autumn/shared";
import { ArrowUpRightFromSquare } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router";
import { SideAccordion } from "@/components/general/SideAccordion";
import { getRedirectUrl } from "@/utils/genUtils";
import { Button } from "@/components/ui/button";
import { Dialog } from "@/components/ui/dialog";
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Tooltip,
TooltipTrigger,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { ArrowUpRightFromSquare } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Link } from "react-router";
import { useEnv } from "@/utils/envUtils";
import { useCusReferralQuery } from "../../hooks/useCusReferralQuery";
import { useRewardsQuery } from "@/hooks/queries/useRewardsQuery";
import { useEnv } from "@/utils/envUtils";
import { getRedirectUrl } from "@/utils/genUtils";
import AddCouponDialogContent from "../../components/add-coupon/AddCouponDialogContent";
import { useCusReferralQuery } from "../../hooks/useCusReferralQuery";
export const CustomerRewards = () => {
// const { discount, env } = useCustomerContext();
@@ -32,12 +33,16 @@ export const CustomerRewards = () => {
const getDiscountText = (discount: any) => {
const coupon = discount.coupon;
const atmnAmountOff = stripeToAtmnAmount({
amount: coupon.amount_off,
currency: coupon.currency,
});
if (coupon.amount_off) {
return (
<p>
{`${coupon.name} `}
<span className="text-t3">
(${coupon.amount_off / 100} {coupon.currency.toUpperCase()})
(${atmnAmountOff} {coupon.currency.toUpperCase()})
</span>
</p>
);