Merge branch 'staging' into feat/stainless
This commit is contained in:
80
conductor-setup.sh
Normal file
80
conductor-setup.sh
Normal 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
6
conductor.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"scripts": {
|
||||
"setup": "zsh conductor-setup.sh",
|
||||
"run": "bun run dev:bun"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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' \
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
91
server/src/external/stripe/stripeInvoiceUtils.ts
vendored
91
server/src/external/stripe/stripeInvoiceUtils.ts
vendored
@@ -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 = () => {
|
||||
|
||||
23
server/src/external/stripe/stripePriceUtils.ts
vendored
23
server/src/external/stripe/stripePriceUtils.ts
vendored
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -204,5 +204,4 @@ export const handleCreateCheckout = async ({
|
||||
checkout_url: checkout.url,
|
||||
});
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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...
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
98
shared/utils/productUtils/priceUtils/convertAmountUtils.ts
Normal file
98
shared/utils/productUtils/priceUtils/convertAmountUtils.ts
Normal 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;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user