diff --git a/server/shell/config.sh b/server/shell/config.sh index 6bd726b32..384ab7bfd 100644 --- a/server/shell/config.sh +++ b/server/shell/config.sh @@ -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" \ No newline at end of file +MOCHA_CMD="bunx mocha --parallel -j 6 --timeout 10000000 --ignore tests/00_setup.ts" \ No newline at end of file diff --git a/server/shell/g1.sh b/server/shell/g1.sh index fcebec0ca..6d0569571 100755 --- a/server/shell/g1.sh +++ b/server/shell/g1.sh @@ -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' \ diff --git a/server/src/external/stripe/createStripePrice/createStripeArrearProrated.ts b/server/src/external/stripe/createStripePrice/createStripeArrearProrated.ts index f51e4bca6..49f63ef07 100644 --- a/server/src/external/stripe/createStripePrice/createStripeArrearProrated.ts +++ b/server/src/external/stripe/createStripePrice/createStripeArrearProrated.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, diff --git a/server/src/external/stripe/createStripePrice/createStripeFixedPrice.ts b/server/src/external/stripe/createStripePrice/createStripeFixedPrice.ts index cfcdb84e6..41906182f 100644 --- a/server/src/external/stripe/createStripePrice/createStripeFixedPrice.ts +++ b/server/src/external/stripe/createStripePrice/createStripeFixedPrice.ts @@ -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, diff --git a/server/src/external/stripe/createStripePrice/createStripeInArrear.ts b/server/src/external/stripe/createStripePrice/createStripeInArrear.ts index 0f47d0ba1..7af4c0e33 100644 --- a/server/src/external/stripe/createStripePrice/createStripeInArrear.ts +++ b/server/src/external/stripe/createStripePrice/createStripeInArrear.ts @@ -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, }; diff --git a/server/src/external/stripe/createStripePrice/createStripeOneOffTiered.ts b/server/src/external/stripe/createStripePrice/createStripeOneOffTiered.ts index c859f5314..21284adab 100644 --- a/server/src/external/stripe/createStripePrice/createStripeOneOffTiered.ts +++ b/server/src/external/stripe/createStripePrice/createStripeOneOffTiered.ts @@ -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, }); diff --git a/server/src/external/stripe/createStripePrice/createStripePrepaid.ts b/server/src/external/stripe/createStripePrice/createStripePrepaid.ts index 1610d92e1..ce71b2672 100644 --- a/server/src/external/stripe/createStripePrice/createStripePrepaid.ts +++ b/server/src/external/stripe/createStripePrice/createStripePrepaid.ts @@ -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 diff --git a/server/src/external/stripe/createStripePrice/createStripePrice.ts b/server/src/external/stripe/createStripePrice/createStripePrice.ts index d3bb06b28..ec3870ce5 100644 --- a/server/src/external/stripe/createStripePrice/createStripePrice.ts +++ b/server/src/external/stripe/createStripePrice/createStripePrice.ts @@ -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, diff --git a/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts b/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts index 1a6e8e6a6..9e7c9de50 100644 --- a/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts +++ b/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts @@ -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, diff --git a/server/src/external/stripe/stripeInvoiceUtils.ts b/server/src/external/stripe/stripeInvoiceUtils.ts index f54576571..58b6b0ae4 100644 --- a/server/src/external/stripe/stripeInvoiceUtils.ts +++ b/server/src/external/stripe/stripeInvoiceUtils.ts @@ -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 = () => { diff --git a/server/src/external/stripe/stripePriceUtils.ts b/server/src/external/stripe/stripePriceUtils.ts index 4cde57493..e52a8ff46 100644 --- a/server/src/external/stripe/stripePriceUtils.ts +++ b/server/src/external/stripe/stripePriceUtils.ts @@ -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, diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts index ff9eff5e9..dbae9addf 100644 --- a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts +++ b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts @@ -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; } diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleSetupCheckout.ts b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleSetupCheckout.ts index fad3e8bcb..c5884da13 100644 --- a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleSetupCheckout.ts +++ b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleSetupCheckout.ts @@ -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, diff --git a/server/src/internal/customers/add-product/handleCreateCheckout.ts b/server/src/internal/customers/add-product/handleCreateCheckout.ts index b202fe235..ccd6f93c0 100644 --- a/server/src/internal/customers/add-product/handleCreateCheckout.ts +++ b/server/src/internal/customers/add-product/handleCreateCheckout.ts @@ -204,5 +204,4 @@ export const handleCreateCheckout = async ({ checkout_url: checkout.url, }); } - return; }; diff --git a/server/src/internal/customers/attach/attachPreviewUtils/priceToNewPreviewItem.ts b/server/src/internal/customers/attach/attachPreviewUtils/priceToNewPreviewItem.ts index 8871b5afa..590472994 100644 --- a/server/src/internal/customers/attach/attachPreviewUtils/priceToNewPreviewItem.ts +++ b/server/src/internal/customers/attach/attachPreviewUtils/priceToNewPreviewItem.ts @@ -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({ diff --git a/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts b/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts index 1bbae4859..d2c155c6a 100644 --- a/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts +++ b/server/src/internal/customers/attach/attachUtils/getAttachConfig.ts @@ -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... diff --git a/server/src/internal/customers/attach/attachUtils/getAttachFunction.ts b/server/src/internal/customers/attach/attachUtils/getAttachFunction.ts index 5c43fdbbe..7898618ef 100644 --- a/server/src/internal/customers/attach/attachUtils/getAttachFunction.ts +++ b/server/src/internal/customers/attach/attachUtils/getAttachFunction.ts @@ -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; } diff --git a/server/src/internal/customers/attach/attachUtils/getContUseItems/createContUseInvoiceItems.ts b/server/src/internal/customers/attach/attachUtils/getContUseItems/createContUseInvoiceItems.ts index 46faaa628..219effa70 100644 --- a/server/src/internal/customers/attach/attachUtils/getContUseItems/createContUseInvoiceItems.ts +++ b/server/src/internal/customers/attach/attachUtils/getContUseItems/createContUseInvoiceItems.ts @@ -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); diff --git a/server/src/internal/customers/cusUtils/cusResponseUtils/getCusUpcomingInvoice.ts b/server/src/internal/customers/cusUtils/cusResponseUtils/getCusUpcomingInvoice.ts index 0f72fd212..426d68684 100644 --- a/server/src/internal/customers/cusUtils/cusResponseUtils/getCusUpcomingInvoice.ts +++ b/server/src/internal/customers/cusUtils/cusResponseUtils/getCusUpcomingInvoice.ts @@ -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, }; diff --git a/server/src/internal/customers/cusUtils/cusResponseUtils/stripeDiscountToResponse.ts b/server/src/internal/customers/cusUtils/cusResponseUtils/stripeDiscountToResponse.ts index c58f2a5e3..98964fc6e 100644 --- a/server/src/internal/customers/cusUtils/cusResponseUtils/stripeDiscountToResponse.ts +++ b/server/src/internal/customers/cusUtils/cusResponseUtils/stripeDiscountToResponse.ts @@ -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, }; }; diff --git a/server/src/internal/features/featureUtils.ts b/server/src/internal/features/featureUtils.ts index 2d3f6a43a..b7ab63d61 100644 --- a/server/src/internal/features/featureUtils.ts +++ b/server/src/internal/features/featureUtils.ts @@ -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; diff --git a/server/src/internal/invoices/InvoiceService.ts b/server/src/internal/invoices/InvoiceService.ts index 5153c47c2..bc37166e1 100644 --- a/server/src/internal/invoices/InvoiceService.ts +++ b/server/src/internal/invoices/InvoiceService.ts @@ -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, }, diff --git a/server/src/internal/invoices/previewItemUtils/getCurContUseItems.ts b/server/src/internal/invoices/previewItemUtils/getCurContUseItems.ts index 4c60323cd..e43adb081 100644 --- a/server/src/internal/invoices/previewItemUtils/getCurContUseItems.ts +++ b/server/src/internal/invoices/previewItemUtils/getCurContUseItems.ts @@ -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({ diff --git a/server/src/internal/products/prices/priceUtils.ts b/server/src/internal/products/prices/priceUtils.ts index 5c81b615f..1c20329af 100644 --- a/server/src/internal/products/prices/priceUtils.ts +++ b/server/src/internal/products/prices/priceUtils.ts @@ -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) { diff --git a/server/src/internal/products/prices/priceUtils/constructPriceUtils.ts b/server/src/internal/products/prices/priceUtils/constructPriceUtils.ts index a8aa9fdd6..63b28053e 100644 --- a/server/src/internal/products/prices/priceUtils/constructPriceUtils.ts +++ b/server/src/internal/products/prices/priceUtils/constructPriceUtils.ts @@ -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, diff --git a/server/src/internal/products/prices/priceUtils/convertPrice.ts b/server/src/internal/products/prices/priceUtils/convertPrice.ts index 5ae41dd93..633ef683e 100644 --- a/server/src/internal/products/prices/priceUtils/convertPrice.ts +++ b/server/src/internal/products/prices/priceUtils/convertPrice.ts @@ -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; }; diff --git a/server/src/internal/rewards/rewardUtils.ts b/server/src/internal/rewards/rewardUtils.ts index bbf8f9770..3ab5ff45d 100644 --- a/server/src/internal/rewards/rewardUtils.ts +++ b/server/src/internal/rewards/rewardUtils.ts @@ -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(); } } diff --git a/server/src/utils/scriptUtils/logUtils/logSubItems.ts b/server/src/utils/scriptUtils/logUtils/logSubItems.ts index fa6636541..dbbb1cc9f 100644 --- a/server/src/utils/scriptUtils/logUtils/logSubItems.ts +++ b/server/src/utils/scriptUtils/logUtils/logSubItems.ts @@ -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}`, ); diff --git a/server/tests/attach/basic/basic10.ts b/server/tests/attach/basic/basic10.ts index 4e6249a47..da86b3a3e 100644 --- a/server/tests/attach/basic/basic10.ts +++ b/server/tests/attach/basic/basic10.ts @@ -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, ); diff --git a/server/tests/attach/basic/basic2.ts b/server/tests/attach/basic/basic2.ts index caffd492c..143a0b526 100644 --- a/server/tests/attach/basic/basic2.ts +++ b/server/tests/attach/basic/basic2.ts @@ -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)) { diff --git a/server/tests/attach/basic/basic3.ts b/server/tests/attach/basic/basic3.ts index 831bd9c85..78661c861 100644 --- a/server/tests/attach/basic/basic3.ts +++ b/server/tests/attach/basic/basic3.ts @@ -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; diff --git a/server/tests/attach/downgrade/downgrade1.ts b/server/tests/attach/downgrade/downgrade1.ts index ce0f6f43e..9e6038838 100644 --- a/server/tests/attach/downgrade/downgrade1.ts +++ b/server/tests/attach/downgrade/downgrade1.ts @@ -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, diff --git a/server/tests/attach/downgrade/downgrade5.ts b/server/tests/attach/downgrade/downgrade5.ts index 15e9b8b24..8a6ac61f1 100644 --- a/server/tests/attach/downgrade/downgrade5.ts +++ b/server/tests/attach/downgrade/downgrade5.ts @@ -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, diff --git a/server/tests/attach/prepaid/prepaid1.ts b/server/tests/attach/prepaid/prepaid1.ts index 4568f7399..f233f00f8 100644 --- a/server/tests/attach/prepaid/prepaid1.ts +++ b/server/tests/attach/prepaid/prepaid1.ts @@ -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, diff --git a/server/tests/utils/expectUtils/expectContUseUtils.ts b/server/tests/utils/expectUtils/expectContUseUtils.ts index f448beb9a..83143c708 100644 --- a/server/tests/utils/expectUtils/expectContUseUtils.ts +++ b/server/tests/utils/expectUtils/expectContUseUtils.ts @@ -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, diff --git a/shared/utils/index.ts b/shared/utils/index.ts index 5153d093e..fde08dd38 100644 --- a/shared/utils/index.ts +++ b/shared/utils/index.ts @@ -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"; diff --git a/shared/utils/productUtils/convertUtils.ts b/shared/utils/productUtils/convertUtils.ts index 5cc9b04aa..ca154859a 100644 --- a/shared/utils/productUtils/convertUtils.ts +++ b/shared/utils/productUtils/convertUtils.ts @@ -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) => diff --git a/shared/utils/productUtils/priceUtils/convertAmountUtils.ts b/shared/utils/productUtils/priceUtils/convertAmountUtils.ts new file mode 100644 index 000000000..ad67a7808 --- /dev/null +++ b/shared/utils/productUtils/priceUtils/convertAmountUtils.ts @@ -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; +}; diff --git a/vite/src/views/customers/customer/components/customer-sidebar/customer-rewards.tsx b/vite/src/views/customers/customer/components/customer-sidebar/customer-rewards.tsx index c69d537b5..7e07d0d3c 100644 --- a/vite/src/views/customers/customer/components/customer-sidebar/customer-rewards.tsx +++ b/vite/src/views/customers/customer/components/customer-sidebar/customer-rewards.tsx @@ -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 (

{`${coupon.name} `} - (${coupon.amount_off / 100} {coupon.currency.toUpperCase()}) + (${atmnAmountOff} {coupon.currency.toUpperCase()})

);