This commit is contained in:
John Yeo
2026-01-23 17:13:19 +00:00
parent 3f4247e453
commit cb05d169a2
454 changed files with 9436 additions and 9160 deletions

View File

@@ -1,15 +1,15 @@
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npx ultracite fix"
}
]
}
]
}
}
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "npx ultracite fix"
}
]
}
]
}
}

View File

@@ -26,6 +26,7 @@ Write integration tests for the Autumn billing system using the `initScenario` p
- Use `Decimal.js` for balance calculations in track tests
- Unique `customerId` per test
- Use generic types with `AutumnInt`: `autumnV1.customers.get<ApiCustomerV3>()`, `autumnV1.check<CheckResponseV1>()`
- **USE UTILITY FUNCTIONS WHENEVER POSSIBLE** - the shorter the code, the better. Check `server/tests/integration/billing/utils/` for existing utilities like `expectCustomerProducts`, `expectProductScheduled`, `expectCustomerInvoiceCorrect`, etc.
**DON'T:**
- Use `describe/beforeAll/test` (legacy pattern)
@@ -33,6 +34,7 @@ Write integration tests for the Autumn billing system using the `initScenario` p
- Share state between tests
- Use raw arithmetic for balance calculations (floating point errors)
- Use `as unknown as Type` casting - use generic types instead
- Write manual assertion loops when a utility function exists
## AutumnInt Response Types

View File

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

View File

@@ -1,9 +1,9 @@
#!/usr/bin/env node
import { randomBytes } from "crypto";
import { writeFileSync, copyFileSync, readFileSync } from "fs";
import inquirer from "inquirer";
import { spawnSync } from "child_process";
import chalk from "chalk";
import { spawnSync } from "child_process";
import { randomBytes } from "crypto";
import { copyFileSync, readFileSync, writeFileSync } from "fs";
import inquirer from "inquirer";
const genUrlSafeBase64 = (bytes) => {
return randomBytes(bytes)
@@ -253,7 +253,7 @@ async function main() {
};
let databaseUrl = "";
let stripeWebhookVars = [];
const stripeWebhookVars = [];
databaseUrl = await handleDatabaseSetup();
// stripeWebhookVars = await handleLocalRunSetup();

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env node
import { randomBytes } from "crypto";
import { writeFileSync, copyFileSync } from "fs";
import chalk from "chalk";
import { randomBytes } from "crypto";
import { copyFileSync, writeFileSync } from "fs";
const genUrlSafeBase64 = (bytes) => {
return randomBytes(bytes)
@@ -26,8 +26,8 @@ async function main() {
STRIPE_WEBHOOK_URL: process.env.STRIPE_WEBHOOK_URL,
};
let databaseUrl = process.env.DATABASE_URL;
let stripeWebhookVars = [];
const databaseUrl = process.env.DATABASE_URL;
const stripeWebhookVars = [];
// stripeWebhookVars = await handleLocalRunSetup();
// Step 11: Write to server/.env

View File

@@ -1,5 +1,5 @@
import inquirer from "inquirer";
import chalk from "chalk";
import inquirer from "inquirer";
/**
* Prompts user for Stripe test API key
@@ -111,7 +111,9 @@ export async function setupTunnelUrl(): Promise<string> {
),
);
console.log(
chalk.cyan("You can use tools like ngrok, localtunnel, or Cloudflare Tunnel."),
chalk.cyan(
"You can use tools like ngrok, localtunnel, or Cloudflare Tunnel.",
),
);
console.log(chalk.cyan("Example: https://your-subdomain.ngrok.io\n"));

View File

@@ -9,9 +9,15 @@ import postgres from "postgres";
export const client = postgres(process.env.DATABASE_URL!);
export const db = drizzle(client, { schema });
export const initDrizzle = (params?: { maxConnections?: number, replica?: boolean }) => {
export const initDrizzle = (params?: {
maxConnections?: number;
replica?: boolean;
}) => {
const maxConnections = params?.maxConnections || 10;
const dbUrl = (params?.replica ? process.env.DATABASE_REPLICA_URL : process.env.DATABASE_URL) ?? "";
const dbUrl =
(params?.replica
? process.env.DATABASE_REPLICA_URL
: process.env.DATABASE_URL) ?? "";
const client = postgres(dbUrl, {
max: maxConnections,
});

View File

@@ -1,6 +1,5 @@
import { Redis } from "@upstash/redis";
const upstash = new Redis({
url: process.env.CLOUD_UPSTASH_REDIS_REST_URL,
token: process.env.CLOUD_UPSTASH_REDIS_REST_TOKEN,

View File

@@ -124,7 +124,6 @@ export function createRoute<
middlewares.push(validator("json", opts.body));
}
// Same for query
if (opts.versionedQuery && opts.resource) {
middlewares.push(
@@ -143,7 +142,6 @@ export function createRoute<
middlewares.push(validator("param", opts.params));
}
// Add expand middleware after query validation
if (opts.query || opts.versionedQuery) {
middlewares.push(expandMiddleware());

View File

@@ -4,10 +4,10 @@ import { createRoute } from "../../honoMiddlewares/routeHandler";
/**
* GET /admin/org-member
*
*
* Query params:
* - org_id: The org ID to get a member for
*
*
* Returns the userId of the first member of the org.
*/
export const handleGetOrgMember = createRoute({
@@ -32,4 +32,3 @@ export const handleGetOrgMember = createRoute({
return c.json({ userId: orgMember.userId });
},
});

View File

@@ -1,10 +1,10 @@
import { isFreeProduct } from "@/internal/products/productUtils.js";
import {
AttachBranch,
AttachPreview,
type AttachPreview,
AttachScenario,
FullProduct,
type FullProduct,
} from "@autumn/shared";
import { isFreeProduct } from "@/internal/products/productUtils.js";
export const getAttachScenario = async ({
preview,
@@ -13,7 +13,7 @@ export const getAttachScenario = async ({
preview: AttachPreview;
product: FullProduct;
}) => {
let branch = preview.branch;
const branch = preview.branch;
if (
branch == AttachBranch.New ||

View File

@@ -1,8 +1,8 @@
import {
customerEntitlements,
customerProducts,
entitlements,
features,
customerEntitlements,
customerProducts,
entitlements,
features,
} from "@autumn/shared";
import { CustomerNotFoundError } from "@shared/index";
import { and, eq, isNull } from "drizzle-orm";
@@ -11,71 +11,71 @@ import { createRoute } from "@/honoMiddlewares/routeHandler";
import { CusService } from "@/internal/customers/CusService";
const ListBalancesSchema = z.object({
customer_id: z.string(),
customer_id: z.string(),
});
export const handleListBalances = createRoute({
query: ListBalancesSchema,
handler: async (c) => {
const ctx = c.get("ctx");
const { customer_id } = c.req.valid("query");
query: ListBalancesSchema,
handler: async (c) => {
const ctx = c.get("ctx");
const { customer_id } = c.req.valid("query");
const fullCus = await CusService.getFull({
db: ctx.db,
idOrInternalId: customer_id,
orgId: ctx.org.id,
env: ctx.env,
});
const fullCus = await CusService.getFull({
db: ctx.db,
idOrInternalId: customer_id,
orgId: ctx.org.id,
env: ctx.env,
});
if (!fullCus) {
throw new CustomerNotFoundError({ customerId: customer_id });
}
if (!fullCus) {
throw new CustomerNotFoundError({ customerId: customer_id });
}
// Get customer entitlements where the entitlement has no internal_product_id
const rawBalances = await ctx.db
.select({
customer_entitlement: customerEntitlements,
entitlement: entitlements,
feature: features,
customer_product: customerProducts,
})
.from(customerEntitlements)
.innerJoin(
entitlements,
eq(customerEntitlements.entitlement_id, entitlements.id),
)
.innerJoin(
features,
eq(entitlements.internal_feature_id, features.internal_id),
)
.leftJoin(
customerProducts,
eq(customerEntitlements.customer_product_id, customerProducts.id),
)
.where(
and(
eq(customerEntitlements.internal_customer_id, fullCus.internal_id),
isNull(entitlements.internal_product_id),
),
);
// Get customer entitlements where the entitlement has no internal_product_id
const rawBalances = await ctx.db
.select({
customer_entitlement: customerEntitlements,
entitlement: entitlements,
feature: features,
customer_product: customerProducts,
})
.from(customerEntitlements)
.innerJoin(
entitlements,
eq(customerEntitlements.entitlement_id, entitlements.id),
)
.innerJoin(
features,
eq(entitlements.internal_feature_id, features.internal_id),
)
.leftJoin(
customerProducts,
eq(customerEntitlements.customer_product_id, customerProducts.id),
)
.where(
and(
eq(customerEntitlements.internal_customer_id, fullCus.internal_id),
isNull(entitlements.internal_product_id),
),
);
const formattedBalances = rawBalances.map((row) => ({
...row.customer_entitlement,
entitlement: {
...row.entitlement,
feature: row.feature,
},
customer_product: row.customer_product
? {
...row.customer_product,
product: null,
customer_entitlements: [],
customer_prices: [],
free_trial: null,
}
: null,
}));
const formattedBalances = rawBalances.map((row) => ({
...row.customer_entitlement,
entitlement: {
...row.entitlement,
feature: row.feature,
},
customer_product: row.customer_product
? {
...row.customer_product,
product: null,
customer_entitlements: [],
customer_prices: [],
free_trial: null,
}
: null,
}));
return c.json({ balances: formattedBalances });
},
return c.json({ balances: formattedBalances });
},
});

View File

@@ -1,7 +1,7 @@
import {
fullCustomerToCustomerEntitlements,
type FullCustomer,
findCustomerEntitlementById,
fullCustomerToCustomerEntitlements,
} from "@autumn/shared";
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
import type { DeductionUpdate } from "../types/deductionUpdate.js";

View File

@@ -1,7 +1,7 @@
import {
cusEntToStartingBalance,
fullCustomerToCustomerEntitlements,
type FullCustomer,
fullCustomerToCustomerEntitlements,
getMaxOverage,
getRelevantFeatures,
isAllocatedCustomerEntitlement,
@@ -43,9 +43,9 @@ export const prepareFeatureDeduction = ({
const relevantFeatures = notNullish(targetBalance)
? [feature]
: getRelevantFeatures({
features: ctx.features,
featureId: feature.id,
});
features: ctx.features,
featureId: feature.id,
});
// Get customer entitlements for these features (includes both product and loose entitlements)
const cusEnts = fullCustomerToCustomerEntitlements({

View File

@@ -1,6 +1,6 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import { Customer, customers } from "@autumn/shared";
import { type Customer, customers } from "@autumn/shared";
import { inArray } from "drizzle-orm";
import type { DrizzleCli } from "@/db/initDrizzle.js";
export class CusReadService {
static async getInInternalIds({

View File

@@ -527,7 +527,10 @@ export class CusSearchService {
}
// Call searchByProduct if we have version filters OR status filters
if ((filters?.version && filters?.version.length > 0) || (filters?.status && filters?.status.length > 0)) {
if (
(filters?.version && filters?.version.length > 0) ||
(filters?.status && filters?.status.length > 0)
) {
return await CusSearchService.searchByProduct({
db,
orgId,

View File

@@ -1,14 +1,14 @@
import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js";
import { ItemSet } from "@/utils/models/ItemSet.js";
import {
AttachConfig,
FullCusProduct,
type AttachConfig,
type FullCusProduct,
getCusProductMinQuantity,
ProductOptions,
type ProductOptions,
} from "@autumn/shared";
import { getStripeSubItems2 } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js";
import type { ItemSet } from "@/utils/models/ItemSet.js";
import { mergeItemSets } from "./mergeItemSets.js";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
export const getProdListWithoutEntities = ({
attachParams,
@@ -19,7 +19,7 @@ export const getProdListWithoutEntities = ({
}) => {
const newProdList = structuredClone(productsList);
for (let i = 0; i < newProdList.length; i++) {
let productOptions = newProdList[i];
const productOptions = newProdList[i];
newProdList[i] = {
...productOptions,
quantity:

View File

@@ -1,4 +1,4 @@
import { ItemSet } from "@/utils/models/ItemSet.js";
import type { ItemSet } from "@/utils/models/ItemSet.js";
export const mergeItemSets = ({
curItemSet,

View File

@@ -1,5 +1,5 @@
import type Stripe from "stripe";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import Stripe from "stripe";
export const getNextCycle = (stripeSubs: Stripe.Subscription[]) => {
const { end } = subToPeriodStartEnd({ sub: stripeSubs[0] });

View File

@@ -1,4 +1,4 @@
import Stripe from "stripe";
import type Stripe from "stripe";
export const shouldCancelSub = ({
sub,

View File

@@ -1,5 +1,4 @@
import { EntitlementWithFeature } from "@autumn/shared";
import { Price } from "@autumn/shared";
import type { EntitlementWithFeature, Price } from "@autumn/shared";
export const hasPriceIdsChanged = ({
oldPrices,

View File

@@ -1,19 +1,18 @@
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { constructPreviewItem } from "@/internal/invoices/previewItemUtils/constructPreviewItem.js";
import { Proration } from "@/internal/invoices/prorationUtils.js";
import { getUsageFromBalance } from "@/internal/products/prices/priceUtils/arrearProratedUtils/getPrevAndNewUsages.js";
import { generateId } from "@/utils/genUtils.js";
import {
FullEntitlement,
FullCustomerEntitlement,
PreviewLineItem,
Price,
AttachReplaceableSchema,
type FullCustomerEntitlement,
type FullEntitlement,
type PreviewLineItem,
type Price,
usageToFeatureName,
} from "@autumn/shared";
import { attachParamsToProduct } from "../convertAttachParams.js";
import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { constructPreviewItem } from "@/internal/invoices/previewItemUtils/constructPreviewItem.js";
import type { Proration } from "@/internal/invoices/prorationUtils.js";
import { getUsageFromBalance } from "@/internal/products/prices/priceUtils/arrearProratedUtils/getPrevAndNewUsages.js";
import { priceToInvoiceItem } from "@/internal/products/prices/priceUtils/priceToInvoiceItem.js";
import { AttachReplaceableSchema } from "@autumn/shared";
import { generateId } from "@/utils/genUtils.js";
import { attachParamsToProduct } from "../convertAttachParams.js";
export const getContUseDowngradeItems = async ({
price,
@@ -34,31 +33,31 @@ export const getContUseDowngradeItems = async ({
proration?: Proration;
logger: any;
}) => {
let prevInvoiceItem = curItem;
let prevBalance = prevCusEnt.entitlement.allowance! - curUsage;
const prevInvoiceItem = curItem;
const prevBalance = prevCusEnt.entitlement.allowance! - curUsage;
const product = attachParamsToProduct({ attachParams });
const feature = prevCusEnt.entitlement.feature;
let { usage: prevUsage, overage: prevOverage } = getUsageFromBalance({
const { usage: prevUsage, overage: prevOverage } = getUsageFromBalance({
ent: prevCusEnt.entitlement,
price,
balance: prevBalance,
});
let { usage: newUsage, overage: newOverage } = getUsageFromBalance({
const { usage: newUsage, overage: newOverage } = getUsageFromBalance({
ent,
price,
balance: prevBalance,
});
if (prevOverage == 0) {
let { usage: newUsage } = getUsageFromBalance({
const { usage: newUsage } = getUsageFromBalance({
ent,
price,
balance: ent.allowance! - curUsage,
});
let newItem = priceToInvoiceItem({
const newItem = priceToInvoiceItem({
price,
ent,
org: attachParams.org,
@@ -87,9 +86,9 @@ export const getContUseDowngradeItems = async ({
now: attachParams.now,
});
let numReplaceables = newUsage - prevUsage;
const numReplaceables = newUsage - prevUsage;
let replaceables = Array.from({ length: numReplaceables }, (_, i) =>
const replaceables = Array.from({ length: numReplaceables }, (_, i) =>
AttachReplaceableSchema.parse({
ent: ent,
id: generateId("rep"),
@@ -103,7 +102,7 @@ export const getContUseDowngradeItems = async ({
feature,
});
let replaceableItem = constructPreviewItem({
const replaceableItem = constructPreviewItem({
priceStr: `${numReplaceables} free ${featureName}`,
price,
description: `${product.name} - ${featureName}`,

View File

@@ -1,7 +1,7 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import type { Entitlement, Price } from "@autumn/shared";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { EntitlementService } from "@/internal/products/entitlements/EntitlementService.js";
import { PriceService } from "@/internal/products/prices/PriceService.js";
import { Entitlement, Price } from "@autumn/shared";
export const insertCustomItems = async ({
db,

View File

@@ -1,4 +1,4 @@
import Stripe from "stripe";
import type Stripe from "stripe";
// Normalize items for comparison (order-agnostic, price id as string)
const normalizePhaseItems = (

View File

@@ -1,5 +1,5 @@
import { differenceInDays } from "date-fns";
import Stripe from "stripe";
import type Stripe from "stripe";
export const phaseAndUnixMatch = ({
phase,

View File

@@ -1,4 +1,4 @@
import { AttachBranch } from "@autumn/shared";
import type { AttachBranch } from "@autumn/shared";
export enum ProrationBehavior {
Immediately = "immediately",

View File

@@ -1,5 +1,5 @@
import { AttachConfig, CusProductStatus } from "@autumn/shared";
import { AttachParams } from "../../cusProducts/AttachParams.js";
import { type AttachConfig, CusProductStatus } from "@autumn/shared";
import type { AttachParams } from "../../cusProducts/AttachParams.js";
export const handleUnifiedAttach = async ({
req,

View File

@@ -1,6 +1,10 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import { InsertReplaceable, Replaceable, replaceables } from "@autumn/shared";
import {
type InsertReplaceable,
type Replaceable,
replaceables,
} from "@autumn/shared";
import { eq, inArray } from "drizzle-orm";
import type { DrizzleCli } from "@/db/initDrizzle.js";
export class RepService {
static async insert({

View File

@@ -1,6 +1,6 @@
// 1. Get min next reset at cus ent
import { Feature, FullCustomerEntitlement } from "@autumn/shared";
import type { Feature, FullCustomerEntitlement } from "@autumn/shared";
export const getMinNextResetAtCusEnt = ({
cusEnts,

View File

@@ -1,9 +1,9 @@
import {
type CustomerEntitlement,
type EntitlementWithFeature,
type FullCusProduct,
type FullCustomerEntitlement,
type Rollover,
import type {
CustomerEntitlement,
EntitlementWithFeature,
FullCusProduct,
FullCustomerEntitlement,
Rollover,
} from "@autumn/shared";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { generateId } from "@/utils/genUtils.js";

View File

@@ -52,9 +52,11 @@ export const setCachedApiCustomer = async ({
});
// Build entity api customers (entity-level features only)
const filteredFullCus = filterEntityLevelCustomerEntitlementsFromFullCustomer({
fullCustomer: fullCus,
});
const filteredFullCus = filterEntityLevelCustomerEntitlementsFromFullCustomer(
{
fullCustomer: fullCus,
},
);
// Build entities first
const entityBatch: {

View File

@@ -75,9 +75,10 @@ export const setCachedGrantedBalance = async ({
// ============================================================================
// 2. Build entity balances batch (entity-level products only)
// ============================================================================
const entityLevelCusProducts = filterEntityLevelCustomerEntitlementsFromFullCustomer({
fullCustomer: fullCus,
}).customer_products;
const entityLevelCusProducts =
filterEntityLevelCustomerEntitlementsFromFullCustomer({
fullCustomer: fullCus,
}).customer_products;
const entityBatch: EntityBatchItem[] = [];

View File

@@ -1,21 +1,21 @@
import type {
AppEnv,
CusProductStatus,
ListCustomersV2Params,
AppEnv,
CusProductStatus,
ListCustomersV2Params,
} from "@autumn/shared";
import { type SQL, sql } from "drizzle-orm";
const buildOptimizedCusProductsCTE = (inStatuses?: CusProductStatus[]) => {
const withStatusFilter = () => {
return inStatuses
? sql`AND cp.status = ANY(ARRAY[${sql.join(
inStatuses.map((status) => sql`${status}`),
sql`, `,
)}])`
: sql``;
};
const withStatusFilter = () => {
return inStatuses
? sql`AND cp.status = ANY(ARRAY[${sql.join(
inStatuses.map((status) => sql`${status}`),
sql`, `,
)}])`
: sql``;
};
return sql`
return sql`
customer_products_with_prices AS (
SELECT
cp.*,
@@ -83,11 +83,11 @@ const buildOptimizedCusProductsCTE = (inStatuses?: CusProductStatus[]) => {
};
const buildEntitiesCTE = (withEntities: boolean) => {
if (!withEntities) {
return sql``;
}
if (!withEntities) {
return sql``;
}
return sql`
return sql`
customer_entities AS (
SELECT
COALESCE(
@@ -105,11 +105,11 @@ const buildEntitiesCTE = (withEntities: boolean) => {
};
const buildEntityCTE = (entityId?: string) => {
if (!entityId) {
return sql``;
}
if (!entityId) {
return sql``;
}
return sql`
return sql`
entity_record AS (
SELECT * FROM entities e
WHERE e.internal_customer_id = (SELECT internal_id FROM customer_record)
@@ -122,15 +122,15 @@ const buildEntityCTE = (entityId?: string) => {
};
const buildTrialsUsedCTE = (
withTrialsUsed: boolean,
orgId: string,
env: AppEnv,
withTrialsUsed: boolean,
orgId: string,
env: AppEnv,
) => {
if (!withTrialsUsed) {
return sql``;
}
if (!withTrialsUsed) {
return sql``;
}
return sql`
return sql`
customer_trials_used AS (
SELECT
COALESCE(
@@ -153,14 +153,14 @@ const buildTrialsUsedCTE = (
};
const buildSubscriptionsCTE = (
withSubs: boolean,
inStatuses?: CusProductStatus[],
withSubs: boolean,
inStatuses?: CusProductStatus[],
) => {
if (!withSubs) {
return sql``;
}
if (!withSubs) {
return sql``;
}
return sql`
return sql`
customer_subscriptions AS (
SELECT
COALESCE(
@@ -177,7 +177,7 @@ const buildSubscriptionsCTE = (
};
const buildExtraEntitlementsCTE = () => {
return sql`
return sql`
extra_customer_entitlements AS (
SELECT
COALESCE(
@@ -222,15 +222,15 @@ const buildExtraEntitlementsCTE = () => {
};
const buildInvoicesCTE = (hasEntityCTE: boolean) => {
const entityFilter = hasEntityCTE
? sql`AND (
const entityFilter = hasEntityCTE
? sql`AND (
NOT EXISTS (SELECT 1 FROM entity_record)
OR i.internal_entity_id = (SELECT internal_id FROM entity_record LIMIT 1)
OR i.internal_entity_id IS NULL
)`
: sql``;
: sql``;
return sql`
return sql`
customer_invoices AS (
SELECT
COALESCE(
@@ -246,21 +246,21 @@ const buildInvoicesCTE = (hasEntityCTE: boolean) => {
};
export const getFullCusQuery = (
idOrInternalId: string,
orgId: string,
env: AppEnv,
inStatuses: CusProductStatus[],
includeInvoices: boolean,
withEntities: boolean,
withTrialsUsed: boolean,
withSubs: boolean,
withEvents: boolean,
entityId?: string,
idOrInternalId: string,
orgId: string,
env: AppEnv,
inStatuses: CusProductStatus[],
includeInvoices: boolean,
withEntities: boolean,
withTrialsUsed: boolean,
withSubs: boolean,
withEvents: boolean,
entityId?: string,
) => {
const sqlChunks: SQL[] = [];
const sqlChunks: SQL[] = [];
// Step 1: Get customer record
sqlChunks.push(sql`
// Step 1: Get customer record
sqlChunks.push(sql`
WITH customer_record AS (
SELECT * FROM customers c
WHERE (
@@ -273,49 +273,49 @@ export const getFullCusQuery = (
)
`);
// Step 2: Get entities
if (withEntities) {
sqlChunks.push(sql`, `);
sqlChunks.push(buildEntitiesCTE(withEntities));
}
// Step 2: Get entities
if (withEntities) {
sqlChunks.push(sql`, `);
sqlChunks.push(buildEntitiesCTE(withEntities));
}
// Step 3: Get entity
if (entityId) {
sqlChunks.push(sql`, `);
sqlChunks.push(buildEntityCTE(entityId));
}
// Step 3: Get entity
if (entityId) {
sqlChunks.push(sql`, `);
sqlChunks.push(buildEntityCTE(entityId));
}
// Add customer products CTE
sqlChunks.push(sql`, `);
// sqlChunks.push(buildCusProductsCTE(inStatuses));
sqlChunks.push(buildOptimizedCusProductsCTE(inStatuses));
// Add customer products CTE
sqlChunks.push(sql`, `);
// sqlChunks.push(buildCusProductsCTE(inStatuses));
sqlChunks.push(buildOptimizedCusProductsCTE(inStatuses));
// Conditionally add trials used CTE
if (withTrialsUsed) {
sqlChunks.push(sql`, `);
sqlChunks.push(buildTrialsUsedCTE(withTrialsUsed, orgId, env));
}
// Conditionally add trials used CTE
if (withTrialsUsed) {
sqlChunks.push(sql`, `);
sqlChunks.push(buildTrialsUsedCTE(withTrialsUsed, orgId, env));
}
// Conditionally add subscriptions CTE
if (withSubs) {
sqlChunks.push(sql`, `);
sqlChunks.push(buildSubscriptionsCTE(withSubs, inStatuses));
}
// Conditionally add subscriptions CTE
if (withSubs) {
sqlChunks.push(sql`, `);
sqlChunks.push(buildSubscriptionsCTE(withSubs, inStatuses));
}
// Unconditionally add extra entitlements CTE
sqlChunks.push(sql`, `);
sqlChunks.push(buildExtraEntitlementsCTE());
// Unconditionally add extra entitlements CTE
sqlChunks.push(sql`, `);
sqlChunks.push(buildExtraEntitlementsCTE());
// Conditionally add invoices CTE
if (includeInvoices) {
sqlChunks.push(sql`, `);
sqlChunks.push(buildInvoicesCTE(!!entityId));
}
// Conditionally add invoices CTE
if (includeInvoices) {
sqlChunks.push(sql`, `);
sqlChunks.push(buildInvoicesCTE(!!entityId));
}
// Conditionally add events CTE
if (withEvents) {
sqlChunks.push(sql`, `);
sqlChunks.push(sql`
// Conditionally add events CTE
if (withEvents) {
sqlChunks.push(sql`, `);
sqlChunks.push(sql`
customer_events AS (
SELECT
COALESCE(
@@ -336,11 +336,11 @@ export const getFullCusQuery = (
AND e.set_usage = false
)
`);
}
}
// Build final SELECT
const selectFieldsChunks: SQL[] = [];
selectFieldsChunks.push(sql`
// Build final SELECT
const selectFieldsChunks: SQL[] = [];
selectFieldsChunks.push(sql`
cr.*,
COALESCE(
(SELECT json_agg(cpwp) FROM customer_products_with_prices cpwp),
@@ -348,131 +348,131 @@ export const getFullCusQuery = (
) AS customer_products
`);
// Add entities to SELECT if withEntities is true
if (withEntities) {
selectFieldsChunks.push(sql`,
// Add entities to SELECT if withEntities is true
if (withEntities) {
selectFieldsChunks.push(sql`,
(SELECT entities FROM customer_entities) AS entities`);
}
}
// Add entity to SELECT if entityId is provided
if (entityId) {
selectFieldsChunks.push(sql`,
// Add entity to SELECT if entityId is provided
if (entityId) {
selectFieldsChunks.push(sql`,
(SELECT row_to_json(er) FROM entity_record er LIMIT 1) AS entity`);
}
}
// Add trials used to SELECT if withTrialsUsed is true
if (withTrialsUsed) {
selectFieldsChunks.push(sql`,
// Add trials used to SELECT if withTrialsUsed is true
if (withTrialsUsed) {
selectFieldsChunks.push(sql`,
(SELECT trials_used FROM customer_trials_used) AS trials_used`);
}
}
// Add subscriptions to SELECT if withSubs is true
if (withSubs) {
selectFieldsChunks.push(sql`,
// Add subscriptions to SELECT if withSubs is true
if (withSubs) {
selectFieldsChunks.push(sql`,
(SELECT subscriptions FROM customer_subscriptions) AS subscriptions`);
}
}
selectFieldsChunks.push(sql`,
selectFieldsChunks.push(sql`,
(SELECT extra_customer_entitlements FROM extra_customer_entitlements) AS extra_customer_entitlements`);
if (includeInvoices) {
selectFieldsChunks.push(sql`,
if (includeInvoices) {
selectFieldsChunks.push(sql`,
(SELECT invoices FROM customer_invoices) AS invoices`);
}
}
if (withEvents) {
selectFieldsChunks.push(sql`,
if (withEvents) {
selectFieldsChunks.push(sql`,
(SELECT events FROM customer_events) AS events`);
}
}
sqlChunks.push(sql`
sqlChunks.push(sql`
SELECT ${sql.join(selectFieldsChunks, sql``)}
FROM customer_record cr
`);
return sql.join(sqlChunks, sql``);
return sql.join(sqlChunks, sql``);
};
export const getPaginatedFullCusQuery = ({
orgId,
env,
inStatuses,
includeInvoices,
withEntities,
withTrialsUsed,
withSubs,
limit = 10,
offset = 0,
withEvents = false,
entityId,
internalCustomerIds,
plans,
search,
orgId,
env,
inStatuses,
includeInvoices,
withEntities,
withTrialsUsed,
withSubs,
limit = 10,
offset = 0,
withEvents = false,
entityId,
internalCustomerIds,
plans,
search,
}: {
orgId: string;
env: AppEnv;
inStatuses?: CusProductStatus[];
includeInvoices: boolean;
withEntities: boolean;
withTrialsUsed: boolean;
withSubs: boolean;
limit: number;
offset: number;
withEvents?: boolean;
entityId?: string;
internalCustomerIds?: string[];
plans?: ListCustomersV2Params["plans"];
search?: string;
orgId: string;
env: AppEnv;
inStatuses?: CusProductStatus[];
includeInvoices: boolean;
withEntities: boolean;
withTrialsUsed: boolean;
withSubs: boolean;
limit: number;
offset: number;
withEvents?: boolean;
entityId?: string;
internalCustomerIds?: string[];
plans?: ListCustomersV2Params["plans"];
search?: string;
}) => {
const withStatusFilter = () => {
return inStatuses?.length
? sql`AND cp.status = ANY(ARRAY[${sql.join(
inStatuses.map((status) => sql`${status}`),
sql`, `,
)}])`
: sql``;
};
const withStatusFilter = () => {
return inStatuses?.length
? sql`AND cp.status = ANY(ARRAY[${sql.join(
inStatuses.map((status) => sql`${status}`),
sql`, `,
)}])`
: sql``;
};
const withCustomerProductFilter = () => {
const hasPlansFilter = plans && plans.length > 0;
const withCustomerProductFilter = () => {
const hasPlansFilter = plans && plans.length > 0;
// Only filter customers by plans, not by status
// Status filtering is applied to customer_products, not to exclude customers
// This allows customers with only loose entitlements (no customer_products) to be included
if (!hasPlansFilter) return sql``;
// Only filter customers by plans, not by status
// Status filtering is applied to customer_products, not to exclude customers
// This allows customers with only loose entitlements (no customer_products) to be included
if (!hasPlansFilter) return sql``;
const planConditions = plans.map((plan) => {
if (plan.versions && plan.versions.length > 0) {
return sql`(p_filter.id = ${plan.id} AND p_filter.version IN (${sql.join(
plan.versions.map((v) => sql`${v}`),
sql`, `,
)}))`;
}
return sql`p_filter.id = ${plan.id}`;
});
const planConditions = plans.map((plan) => {
if (plan.versions && plan.versions.length > 0) {
return sql`(p_filter.id = ${plan.id} AND p_filter.version IN (${sql.join(
plan.versions.map((v) => sql`${v}`),
sql`, `,
)}))`;
}
return sql`p_filter.id = ${plan.id}`;
});
return sql`AND EXISTS (
return sql`AND EXISTS (
SELECT 1 FROM customer_products cp_filter
JOIN products p_filter ON cp_filter.internal_product_id = p_filter.internal_id
WHERE cp_filter.internal_customer_id = c.internal_id
AND (${sql.join(planConditions, sql` OR `)})
)`;
};
};
const withSearchFilter = () => {
if (!search) return sql``;
const pattern = `%${search}%`;
return sql`AND (
const withSearchFilter = () => {
if (!search) return sql``;
const pattern = `%${search}%`;
return sql`AND (
c.id ILIKE ${pattern}
OR c.name ILIKE ${pattern}
OR c.email ILIKE ${pattern}
)`;
};
};
// ADDITION: Unconditionally add extra entitlements CTE (305-308)
// This matches the style of the rest of the CTE construction blocks.
// Extra entitlements are those without a customer_product_id (loose entitlements)
const extraEntitlementsCTE = sql`, extra_customer_entitlements AS (
// ADDITION: Unconditionally add extra entitlements CTE (305-308)
// This matches the style of the rest of the CTE construction blocks.
// Extra entitlements are those without a customer_product_id (loose entitlements)
const extraEntitlementsCTE = sql`, extra_customer_entitlements AS (
SELECT
ce.internal_customer_id,
COALESCE(
@@ -516,19 +516,20 @@ export const getPaginatedFullCusQuery = ({
GROUP BY ce.internal_customer_id
)`;
return sql`
return sql`
WITH customer_records AS (
SELECT c.*
FROM customers c
WHERE c.org_id = ${orgId}
AND c.env = ${env}
${internalCustomerIds && internalCustomerIds.length > 0
? sql`AND c.internal_id IN (${sql.join(
internalCustomerIds.map((id) => sql`${id}`),
sql`, `,
)})`
: sql``
}
${
internalCustomerIds && internalCustomerIds.length > 0
? sql`AND c.internal_id IN (${sql.join(
internalCustomerIds.map((id) => sql`${id}`),
sql`, `,
)})`
: sql``
}
${withCustomerProductFilter()}
${withSearchFilter()}
ORDER BY c.created_at DESC
@@ -607,8 +608,9 @@ export const getPaginatedFullCusQuery = ({
GROUP BY cpwp.internal_customer_id
)
${withSubs
? sql`, customer_subscriptions AS (
${
withSubs
? sql`, customer_subscriptions AS (
SELECT
cpwp.internal_customer_id,
COALESCE(
@@ -619,11 +621,12 @@ export const getPaginatedFullCusQuery = ({
JOIN subscriptions s ON s.stripe_id = ANY(cpwp.subscription_ids)
GROUP BY cpwp.internal_customer_id
)`
: sql``
}
: sql``
}
${withEntities
? sql`, customer_entities AS (
${
withEntities
? sql`, customer_entities AS (
SELECT
e.internal_customer_id,
COALESCE(
@@ -634,11 +637,12 @@ export const getPaginatedFullCusQuery = ({
WHERE e.internal_customer_id IN (SELECT internal_id FROM customer_records)
GROUP BY e.internal_customer_id
)`
: sql``
}
: sql``
}
${includeInvoices
? sql`, customer_invoices AS (
${
includeInvoices
? sql`, customer_invoices AS (
SELECT
i.internal_customer_id,
COALESCE(
@@ -649,11 +653,12 @@ export const getPaginatedFullCusQuery = ({
WHERE i.internal_customer_id IN (SELECT internal_id FROM customer_records)
GROUP BY i.internal_customer_id
)`
: sql``
}
: sql``
}
${withTrialsUsed
? sql`, customer_trials_used AS (
${
withTrialsUsed
? sql`, customer_trials_used AS (
SELECT
cp.internal_customer_id,
json_agg(json_build_object(
@@ -668,8 +673,8 @@ export const getPaginatedFullCusQuery = ({
AND cp.free_trial_id IS NOT NULL
GROUP BY cp.internal_customer_id
)`
: sql``
}
: sql``
}
${extraEntitlementsCTE}

View File

@@ -90,7 +90,9 @@ export const handleDecreaseAndTransfer = async ({
replaceables: [],
entities: fullCus.entities,
features,
internalEntityId: toEntity?.internal_id ? toEntity.internal_id : undefined,
internalEntityId: toEntity?.internal_id
? toEntity.internal_id
: undefined,
entityId: toEntity?.id ? toEntity.id : undefined,
},
product,

View File

@@ -1,17 +1,21 @@
import type {
Feature,
Organization,
ProductItem,
ProductV2,
} from "@autumn/shared";
import { getFeatureNameWithCapital } from "@/internal/features/utils/displayUtils.js";
import {
featurePricetoPricecnItem,
getPriceText,
} from "@/internal/products/pricecn/pricecnUtils.js";
import {
isFeatureItem,
isFeaturePriceItem,
isPriceItem,
} from "@/internal/products/product-items/productItemUtils/getItemType.js";
import { Feature, Organization, ProductItem, ProductV2 } from "@autumn/shared";
import { formatCurrency, formatTiers } from "./previewUtils.js";
import { isFeaturePriceItem } from "@/internal/products/product-items/productItemUtils/getItemType.js";
import { notNullish } from "@/utils/genUtils.js";
import { getFeatureNameWithCapital } from "@/internal/features/utils/displayUtils.js";
import { formatCurrency, formatTiers } from "./previewUtils.js";
export const getProductChargeText = ({
product,
@@ -22,10 +26,10 @@ export const getProductChargeText = ({
org: Organization;
features: Feature[];
}) => {
let basePrices = product.items.filter((i) => isPriceItem(i));
let total = basePrices.reduce((acc, curr) => acc + curr.price!, 0);
const basePrices = product.items.filter((i) => isPriceItem(i));
const total = basePrices.reduce((acc, curr) => acc + curr.price!, 0);
let itemStrs = [];
const itemStrs = [];
if (total > 0) {
itemStrs.push(
formatCurrency({
@@ -35,18 +39,18 @@ export const getProductChargeText = ({
);
}
let prepaidPrices = product.items.filter(
const prepaidPrices = product.items.filter(
(i) => isFeaturePriceItem(i) && i.usage_model == "prepaid",
);
let prepaidStrings = prepaidPrices.map((i) => {
let feature = features.find((f) => f.id === i.feature_id);
let priceStr = formatTiers({
const prepaidStrings = prepaidPrices.map((i) => {
const feature = features.find((f) => f.id === i.feature_id);
const priceStr = formatTiers({
tiers: i.tiers!,
org,
});
let featureStr =
const featureStr =
i.billing_units && i.billing_units > 1
? `${i.billing_units} ${feature?.name}`
: feature?.name;
@@ -67,15 +71,15 @@ export const getItemDescription = ({
product: ProductV2;
org: Organization;
}) => {
let prices = product.items.filter((i) => !isFeatureItem(i));
const prices = product.items.filter((i) => !isFeatureItem(i));
let priceStr = getPriceText({
const priceStr = getPriceText({
item,
org,
});
if (isPriceItem(item)) {
let baseName =
const baseName =
prices.length == 1
? product.name
: notNullish(item.interval)
@@ -84,7 +88,7 @@ export const getItemDescription = ({
return baseName;
} else {
let feature = features.find((f) => f.id === item.feature_id);
const feature = features.find((f) => f.id === item.feature_id);
// let pricecnItem = featurePricetoPricecnItem({
// feature,
// item,

View File

@@ -1,4 +1,4 @@
import { Organization, PriceTier } from "@autumn/shared";
import type { Organization, PriceTier } from "@autumn/shared";
export const formatCurrency = ({
amount,
@@ -30,11 +30,11 @@ export const formatTiers = ({
});
}
let tiersStart = formatCurrency({
const tiersStart = formatCurrency({
amount: tiers[0].amount,
defaultCurrency: org.default_currency!,
});
let tiersEnd = formatCurrency({
const tiersEnd = formatCurrency({
amount: tiers[tiers.length - 1].amount,
defaultCurrency: org.default_currency!,
});
@@ -50,8 +50,8 @@ export const getItemsHtml = ({
org: Organization;
}) => {
let html = "";
let pricedItems = items.filter((item) => item.amount != 0);
let totalAmount = pricedItems.reduce((acc: number, item: any) => {
const pricedItems = items.filter((item) => item.amount != 0);
const totalAmount = pricedItems.reduce((acc: number, item: any) => {
return acc + item.amount;
}, 0);
@@ -74,7 +74,7 @@ export const getItemsHtml = ({
export const itemsToHtml = ({ items }: { items: any[] }) => {
let html = "";
for (let item of items) {
for (const item of items) {
if (item.amount == 0) {
continue;
}

View File

@@ -1,8 +1,8 @@
import { MigrationService } from "../migrations/MigrationService.js";
import { MigrationJobStep, type Organization } from "@autumn/shared";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { sendTextEmail } from "@/external/resend/resendUtils.js";
import { MigrationJobStep, Organization } from "@autumn/shared";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { safeResend } from "@/external/resend/safeResend.js";
import { MigrationService } from "../migrations/MigrationService.js";
import { FROM_AUTUMN } from "./constants.js";
export const sendMigrationEmail = safeResend({
@@ -15,15 +15,15 @@ export const sendMigrationEmail = safeResend({
migrationJobId: string;
org: Organization;
}) => {
let migrationJob = await MigrationService.getJob({
const migrationJob = await MigrationService.getJob({
db,
id: migrationJobId,
});
// Send email
let getCustomersStep =
const getCustomersStep =
migrationJob.step_details[MigrationJobStep.GetCustomers];
let migrateStep =
const migrateStep =
migrationJob.step_details[MigrationJobStep.MigrateCustomers];
console.log("Sending migration email");

View File

@@ -1,6 +1,6 @@
import OTPEmail from "@emails/OTPEmail.js";
import { logger } from "@/external/logtail/logtailUtils.js";
import { createResendCli } from "@/external/resend/resendUtils.js";
import OTPEmail from "@emails/OTPEmail.js";
import { FROM_AUTUMN } from "./constants.js";
const sendOTPEmail = async ({ email, otp }: { email: string; otp: string }) => {

View File

@@ -1,7 +1,7 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import { EntityService } from "@/internal/api/entities/EntityService.js";
import { Feature, getFeatureName } from "@autumn/shared";
import { type Feature, getFeatureName } from "@autumn/shared";
import { AppEnv, Entity } from "autumn-js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { EntityService } from "@/internal/api/entities/EntityService.js";
export const getEntityInvoiceDescription = async ({
db,
@@ -15,12 +15,12 @@ export const getEntityInvoiceDescription = async ({
logger: any;
}) => {
try {
let entity = await EntityService.getByInternalId({
const entity = await EntityService.getByInternalId({
db,
internalId: internalEntityId,
});
let feature = features.find(
const feature = features.find(
(f) => f.internal_id == entity?.internal_feature_id,
);
@@ -32,7 +32,7 @@ export const getEntityInvoiceDescription = async ({
}
if (feature && entDetails) {
let featureName = getFeatureName({
const featureName = getFeatureName({
feature,
plural: false,
capitalize: true,

View File

@@ -1,10 +1,10 @@
import { generateId, notNullish, nullish } from "@/utils/genUtils.js";
import {
AppEnv,
Entity,
type AppEnv,
type Entity,
FeatureType,
FullCusEntWithFullCusProduct,
} from "@autumn/shared";
import { generateId, notNullish, nullish } from "@/utils/genUtils.js";
export const constructEntity = ({
inputEntity,
@@ -21,7 +21,7 @@ export const constructEntity = ({
env: AppEnv;
deleted?: boolean;
}) => {
let entity: Entity = {
const entity: Entity = {
internal_id: generateId("ety"),
id: inputEntity.id,
name: inputEntity.name,

View File

@@ -1,6 +1,5 @@
import { notNullish } from "@/utils/genUtils.js";
import { nullish } from "@/utils/genUtils.js";
import { Entity } from "@autumn/shared";
import type { Entity } from "@autumn/shared";
import { notNullish, nullish } from "@/utils/genUtils.js";
export const logEntityToAction = ({
entityToAction,
@@ -35,11 +34,13 @@ export const getEntityToAction = ({
feature: any;
logger: any;
}) => {
let entityToAction: any = {};
const entityToAction: any = {};
let createCount = 0;
let replacedEntities: string[] = [];
const replacedEntities: string[] = [];
for (const inputEntity of inputEntities) {
let curEntity = existingEntities.find((e: any) => e.id === inputEntity.id);
const curEntity = existingEntities.find(
(e: any) => e.id === inputEntity.id,
);
if (curEntity && curEntity.deleted) {
entityToAction[inputEntity.id] = {

View File

@@ -1,6 +1,4 @@
import { Feature } from "@autumn/shared";
import { Organization } from "@autumn/shared";
import { type Feature, Organization } from "@autumn/shared";
export const getFeatureName = ({
feature,

View File

@@ -1,5 +1,5 @@
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
/**
* Get Stripe invoice by Stripe invoice ID

View File

@@ -1,6 +1,11 @@
import { Organization, Price, Product, UsagePriceConfig } from "@autumn/shared";
import type {
Organization,
Price,
Product,
UsagePriceConfig,
} from "@autumn/shared";
import { Decimal } from "decimal.js";
import Stripe from "stripe";
import type Stripe from "stripe";
export const constructStripeInvoiceItem = ({
product,
@@ -29,7 +34,7 @@ export const constructStripeInvoiceItem = ({
new Decimal(amount).mul(100).round().toNumber(),
);
let priceData =
const priceData =
amountInCents > 0
? {
price_data: {
@@ -43,7 +48,7 @@ export const constructStripeInvoiceItem = ({
currency: org.default_currency || "usd",
};
let invoiceItem: Stripe.InvoiceItemCreateParams = {
const invoiceItem: Stripe.InvoiceItemCreateParams = {
subscription: stripeSubId,
...priceData,

View File

@@ -11,7 +11,8 @@ const invoiceRedirectLimiter = rateLimiter<HonoEnv>({
windowMs: 60 * 1000, // 1 minute
limit: 10,
standardHeaders: "draft-6",
keyGenerator: (c) => c.req.header("x-forwarded-for") ?? c.req.header("x-real-ip") ?? "unknown",
keyGenerator: (c) =>
c.req.header("x-forwarded-for") ?? c.req.header("x-real-ip") ?? "unknown",
});
publicInvoiceRouter.get(
@@ -26,4 +27,4 @@ publicInvoiceRouter.get(
*/
export const invoiceRouter = new Hono<HonoEnv>();
invoiceRouter.get("/:stripe_invoice_id/stripe", ...handleGetStripeInvoice);
invoiceRouter.get("/:stripe_invoice_id/stripe", ...handleGetStripeInvoice);

View File

@@ -1,11 +1,11 @@
import { generateId } from "@/utils/genUtils.js";
import {
AppEnv,
MigrationError,
MigrationJob,
type AppEnv,
type MigrationError,
type MigrationJob,
MigrationJobStep,
Product,
type Product,
} from "@autumn/shared";
import { generateId } from "@/utils/genUtils.js";
export const constructMigrationError = ({
migrationJobId,
@@ -20,7 +20,7 @@ export const constructMigrationError = ({
code: string;
message: string;
}) => {
let migrationError: MigrationError = {
const migrationError: MigrationError = {
migration_job_id: migrationJobId,
internal_customer_id: internalCustomerId,
data,
@@ -40,7 +40,7 @@ export const constructMigrationJob = ({
fromProduct: Product;
toProduct: Product;
}) => {
let migrationJob: MigrationJob = {
const migrationJob: MigrationJob = {
id: generateId("mig_job"),
created_at: Date.now(),
updated_at: Date.now(),

View File

@@ -5,31 +5,31 @@ import { FeatureService } from "@/internal/features/FeatureService";
import { ProductService } from "@/internal/products/ProductService";
export const handleNukeOrganisationConfiguration = createRoute({
handler: async (c) => {
const ctx = c.get("ctx");
const { db, org, env } = ctx;
handler: async (c) => {
const ctx = c.get("ctx");
const { db, org, env } = ctx;
if (env !== AppEnv.Sandbox) {
return c.json({ error: "Cannot clear non-sandbox orgs" }, 400);
}
if (env !== AppEnv.Sandbox) {
return c.json({ error: "Cannot clear non-sandbox orgs" }, 400);
}
await CusService.deleteByOrgId({
db,
orgId: org.id,
env: AppEnv.Sandbox,
});
await CusService.deleteByOrgId({
db,
orgId: org.id,
env: AppEnv.Sandbox,
});
await ProductService.deleteByOrgId({
db,
orgId: org.id,
env: AppEnv.Sandbox,
});
await FeatureService.deleteByOrgId({
db,
orgId: org.id,
env: AppEnv.Sandbox,
});
await ProductService.deleteByOrgId({
db,
orgId: org.id,
env: AppEnv.Sandbox,
});
await FeatureService.deleteByOrgId({
db,
orgId: org.id,
env: AppEnv.Sandbox,
});
return c.json({ message: "Organisation configuration cleared" });
},
return c.json({ message: "Organisation configuration cleared" });
},
});

View File

@@ -1,9 +1,9 @@
import {
apiFeatureToDbFeature,
CreateFeatureV0ParamsSchema,
CreateFreeTrialSchema,
CreateProductItemParamsSchema,
CreateProductSchema,
apiFeatureToDbFeature,
CreateFeatureV0ParamsSchema,
CreateFreeTrialSchema,
CreateProductItemParamsSchema,
CreateProductSchema,
} from "@shared/index";
import { z } from "zod/v4";
import type { DrizzleCli } from "@/db/initDrizzle";
@@ -14,84 +14,84 @@ import { createProduct } from "@/internal/products/handlers/productActions/creat
import { ProductService } from "@/internal/products/ProductService";
const OrganisationConfigurationSchema = z.object({
features: z.array(CreateFeatureV0ParamsSchema).optional().default([]),
products: z.array(
CreateProductSchema.extend({
items: z.array(CreateProductItemParamsSchema).optional().default([]),
free_trial: CreateFreeTrialSchema.nullish().optional().default(null),
}),
),
features: z.array(CreateFeatureV0ParamsSchema).optional().default([]),
products: z.array(
CreateProductSchema.extend({
items: z.array(CreateProductItemParamsSchema).optional().default([]),
free_trial: CreateFreeTrialSchema.nullish().optional().default(null),
}),
),
});
export const handlePushOrganisationConfiguration = createRoute({
body: OrganisationConfigurationSchema,
handler: async (c) => {
const body = c.req.valid("json");
const ctx = c.get("ctx");
const { features, db, org, env } = ctx;
const products = await ProductService.listFull({
db,
orgId: org.id,
env,
});
body: OrganisationConfigurationSchema,
handler: async (c) => {
const body = c.req.valid("json");
const ctx = c.get("ctx");
const { features, db, org, env } = ctx;
const products = await ProductService.listFull({
db,
orgId: org.id,
env,
});
await db.transaction(async (tx) => {
const txDb = tx as unknown as DrizzleCli;
const txCtx = { ...ctx, db: txDb };
await db.transaction(async (tx) => {
const txDb = tx as unknown as DrizzleCli;
const txCtx = { ...ctx, db: txDb };
for (const apiFeature of body.features) {
if (features.some((x) => x.id === apiFeature.id)) {
continue;
}
for (const apiFeature of body.features) {
if (features.some((x) => x.id === apiFeature.id)) {
continue;
}
const dbFeature = apiFeatureToDbFeature({
apiFeature,
});
const dbFeature = apiFeatureToDbFeature({
apiFeature,
});
await createFeature({
ctx: txCtx,
data: {
id: dbFeature.id,
name: dbFeature.name,
type: dbFeature.type,
config: dbFeature.config,
event_names: dbFeature.event_names,
},
});
}
await createFeature({
ctx: txCtx,
data: {
id: dbFeature.id,
name: dbFeature.name,
type: dbFeature.type,
config: dbFeature.config,
event_names: dbFeature.event_names,
},
});
}
const updatedFeatures = await FeatureService.list({
db: txDb,
orgId: org.id,
env,
});
const updatedFeatures = await FeatureService.list({
db: txDb,
orgId: org.id,
env,
});
for (const apiProduct of body.products) {
if (products.some((x) => x.id === apiProduct.id)) {
continue;
}
for (const apiProduct of body.products) {
if (products.some((x) => x.id === apiProduct.id)) {
continue;
}
await createProduct({
ctx: {
...txCtx,
features: updatedFeatures,
},
data: {
id: apiProduct.id,
name: apiProduct.name,
is_add_on: apiProduct.is_add_on,
is_default: apiProduct.is_default,
group: apiProduct.group,
items: apiProduct.items,
free_trial: apiProduct.free_trial,
},
});
}
});
await createProduct({
ctx: {
...txCtx,
features: updatedFeatures,
},
data: {
id: apiProduct.id,
name: apiProduct.name,
is_add_on: apiProduct.is_add_on,
is_default: apiProduct.is_default,
group: apiProduct.group,
items: apiProduct.items,
free_trial: apiProduct.free_trial,
},
});
}
});
return c.json({
features: body.features,
products: body.products,
});
},
return c.json({
features: body.features,
products: body.products,
});
},
});

View File

@@ -18,8 +18,7 @@ export const handleDeleteView = createRoute({
// Remove from list
const listKey = `saved_views_list:${org.id}:${env}`;
const existingViews =
(await CacheManager.getJson<string[]>(listKey)) || [];
const existingViews = (await CacheManager.getJson<string[]>(listKey)) || [];
const updatedViews = existingViews.filter((id: string) => id !== viewId);
await CacheManager.setJson(listKey, updatedViews, "forever");

View File

@@ -39,7 +39,11 @@ export const parseChatProducts = async ({
env: AppEnv.Sandbox,
});
const { prices, entitlements, features: updatedFeatures } = await handleNewProductItems({
const {
prices,
entitlements,
features: updatedFeatures,
} = await handleNewProductItems({
db,
curPrices: [],
curEnts: [],

View File

@@ -1,9 +1,9 @@
import { AppEnv, type Organization } from "@autumn/shared";
import type { Logger } from "@/external/logtail/logtailUtils.js";
import {
deauthorizeAccount,
deleteConnectedAccount,
} from "@/external/connect/connectUtils.js";
import type { Logger } from "@/external/logtail/logtailUtils.js";
import { deleteSvixApp } from "@/external/svix/svixHelpers.js";
import { deleteStripeWebhook } from "../orgUtils.js";

View File

@@ -95,7 +95,10 @@ export const createProduct = async ({
const newFullProduct: FullProduct = {
...product,
prices,
entitlements: getEntsWithFeature({ ents: entitlements, features: updatedFeatures }),
entitlements: getEntsWithFeature({
ents: entitlements,
features: updatedFeatures,
}),
free_trial: newFreeTrial,
};

View File

@@ -1,4 +1,4 @@
import { Entitlement, Price, UsagePriceConfig } from "@autumn/shared";
import type { Entitlement, Price, UsagePriceConfig } from "@autumn/shared";
import { Decimal } from "decimal.js";
export const getUsageFromBalance = ({
@@ -10,17 +10,17 @@ export const getUsageFromBalance = ({
price: Price;
balance: number;
}) => {
let config = price.config as UsagePriceConfig;
let billingUnits = config.billing_units || 1;
const config = price.config as UsagePriceConfig;
const billingUnits = config.billing_units || 1;
let overage = -Math.min(0, balance);
let roundedOverage = new Decimal(overage)
const overage = -Math.min(0, balance);
const roundedOverage = new Decimal(overage)
.div(billingUnits)
.ceil()
.mul(billingUnits)
.toNumber();
let usage = new Decimal(ent.allowance!).sub(balance).toNumber();
const usage = new Decimal(ent.allowance!).sub(balance).toNumber();
let roundedUsage = usage;
if (overage > 0) {

View File

@@ -1,9 +1,14 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import { PriceService } from "../PriceService.js";
import { FixedPriceConfig, prices, PriceType } from "@autumn/shared";
import {
type FixedPriceConfig,
type Price,
PriceType,
prices,
type UsagePriceConfig,
} from "@autumn/shared";
import { eq } from "drizzle-orm";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { generateId } from "@/utils/genUtils.js";
import { Price, UsagePriceConfig } from "@autumn/shared";
import { PriceService } from "../PriceService.js";
export const copyPrice = async ({
db,
@@ -20,7 +25,7 @@ export const copyPrice = async ({
isCustom?: boolean;
withPrevConfig?: boolean;
}) => {
let price = (await db.query.prices.findFirst({
const price = (await db.query.prices.findFirst({
where: eq(prices.id, priceId),
})) as Price;

View File

@@ -1,4 +1,4 @@
import { BillingType, Price } from "@autumn/shared";
import { BillingType, type Price } from "@autumn/shared";
import { getBillingType } from "../priceUtils.js";
const BillingTypeOrder = [
@@ -11,8 +11,8 @@ const BillingTypeOrder = [
export const sortPricesByType = (prices: Price[]) => {
return prices.sort((a, b) => {
let aType = getBillingType(a.config);
let bType = getBillingType(b.config);
const aType = getBillingType(a.config);
const bType = getBillingType(b.config);
return BillingTypeOrder.indexOf(aType) - BillingTypeOrder.indexOf(bType);
});
};

View File

@@ -1,7 +1,7 @@
import {
Feature,
type Feature,
FeatureType,
ProductItem,
type ProductItem,
ProductItemFeatureType,
UsageModel,
} from "@autumn/shared";

View File

@@ -236,11 +236,11 @@ export const toFeatureAndPrice = ({
feature_id: item.feature_id!,
usage_tiers: notNullish(item.price)
? [
{
amount: item.price,
to: TierInfinite,
},
]
{
amount: item.price,
to: TierInfinite,
},
]
: (item.tiers as any),
interval: itemToBillingInterval({ item }) as BillingInterval,
interval_count: item.interval_count || 1,
@@ -257,7 +257,7 @@ export const toFeatureAndPrice = ({
if (shouldProrate(onDecrease) || onDecrease === OnDecrease.Prorate) {
onDecrease =
onIncrease === OnIncrease.ProrateImmediately ||
onIncrease === OnIncrease.BillImmediately
onIncrease === OnIncrease.BillImmediately
? OnDecrease.ProrateImmediately
: OnDecrease.ProrateNextCycle;
}

View File

@@ -36,7 +36,10 @@ honoProductRouter.delete("/:product_id", ...handleDeleteProductHono);
honoProductRouter.post("/:product_id/copy", ...handleCopyProductV2);
// Info before deleting plan
honoProductRouter.get("/:product_id/has_customers", ...handlePlanHasCustomersV2);
honoProductRouter.get(
"/:product_id/has_customers",
...handlePlanHasCustomersV2,
);
honoProductRouter.post(
"/:product_id/has_customers",
...handlePlanHasCustomersV2,

View File

@@ -1,12 +1,15 @@
import { FeatureId, sendProductEvent } from "@/external/autumn/autumnUtils.js";
import { isEntitled } from "@/external/autumn/autumnUtils.js";
import {
FeatureId,
isEntitled,
sendProductEvent,
} from "@/external/autumn/autumnUtils.js";
import { handleRequestError } from "@/utils/errorUtils.js";
export const pricingMiddleware = async (req: any, res: any, next: any) => {
let path = req.url;
let method = req.method;
const path = req.url;
const method = req.method;
let host = req.headers.host;
const host = req.headers.host;
if (host.includes("localhost")) {
next();
return;
@@ -33,7 +36,7 @@ export const pricingMiddleware = async (req: any, res: any, next: any) => {
});
}
if (path.match(/^\/products\/[^\/]+$/) && method === "DELETE") {
if (path.match(/^\/products\/[^/]+$/) && method === "DELETE") {
console.log("sending product delete event");
await sendProductEvent({
org: req.org,

View File

@@ -1,6 +1,6 @@
import { AppEnv, AuthType, ErrCode } from "@autumn/shared";
import { verifyPublicKey } from "@/internal/dev/api-keys/publicKeyUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { AppEnv, AuthType, ErrCode } from "@autumn/shared";
const allowedEndpoints = [
{
@@ -82,7 +82,7 @@ export const verifyBearerPublishableKey = async (
});
}
let env: AppEnv = pkey.startsWith("am_pk_test")
const env: AppEnv = pkey.startsWith("am_pk_test")
? AppEnv.Sandbox
: AppEnv.Live;
@@ -100,7 +100,7 @@ export const verifyBearerPublishableKey = async (
});
}
let { org, features } = data;
const { org, features } = data;
req.minOrg = {
id: org.id,

View File

@@ -1,8 +1,9 @@
export const parseAuthHeader = (req: any) => {
let authHeader = req.headers["Authorization"] || req.headers["authorization"];
const authHeader =
req.headers["Authorization"] || req.headers["authorization"];
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return null;
}
let bearerToken = authHeader.split(" ")[1];
const bearerToken = authHeader.split(" ")[1];
return bearerToken;
};

View File

@@ -4,7 +4,7 @@ import {
isFreeProduct,
} from "@autumn/shared";
export const allCusProductsOnSubFree = ({
export const allCusProductsOnSubFree = ({
fullCus,
subId,
}: {

View File

@@ -1,8 +1,8 @@
import {
CusProductStatus,
cusProductToPrices,
type FullCusProduct,
isFreeProduct,
CusProductStatus,
cusProductToPrices,
type FullCusProduct,
isFreeProduct,
} from "@autumn/shared";
import type { FullCustomer } from "../../../../shared/models/cusModels/fullCusModel";
import { getRelatedCusPrice } from "../../internal/customers/cusProducts/cusEnts/cusEntUtils";

View File

@@ -45,9 +45,6 @@ export const checkSubCountMatch = async ({
.from(metadata)
.where(inArray(metadata.stripe_invoice_id, invoiceIds as string[]));
if (
stripeSubs.length - (metadataCount?.[0]?.count || 0) !==
subIds.length

View File

@@ -17,22 +17,28 @@ export const saveCheckState = async ({
result: StateCheckResult;
}): Promise<void> => {
const newFailedChecks = result.checks
.filter((c): c is typeof c & { type: CheckType } => c.type !== "overall_status")
.filter(
(c): c is typeof c & { type: CheckType } => c.type !== "overall_status",
)
.filter((c) => !c.passed);
// Skip Redis entirely if all checks pass
if (newFailedChecks.length === 0) return;
const stateKey = `state:${org.id}:${env}:${fullCus.internal_id}`;
const existingState = (await upstash.get(stateKey)) as RedisChecksState | null;
const existingState = (await upstash.get(
stateKey,
)) as RedisChecksState | null;
if (existingState) {
// Merge with existing state
const existingFailedTypes = new Set<CheckType>(existingState.checks.map((c) => c.type));
const newFailedTypes = new Set<CheckType>(newFailedChecks.map((c) => c.type));
const existingFailedTypes = new Set<CheckType>(
existingState.checks.map((c) => c.type),
);
const newFailedTypes = new Set<CheckType>(
newFailedChecks.map((c) => c.type),
);
// Checks that were fixed (existed before but not now)
const checksToRemove = new Set<CheckType>(
@@ -44,7 +50,9 @@ export const saveCheckState = async ({
);
// Keep checks that still fail, remove ones that are fixed
const updatedChecks = existingState.checks.filter((c) => !checksToRemove.has(c.type));
const updatedChecks = existingState.checks.filter(
(c) => !checksToRemove.has(c.type),
);
// Add new failing checks
for (const check of newFailedChecks) {
@@ -65,7 +73,6 @@ export const saveCheckState = async ({
checks: updatedChecks,
};
await upstash.set(stateKey, JSON.stringify(updatedState));
} else {
// Create new state
const newState: RedisChecksState = {
@@ -87,7 +94,5 @@ export const saveCheckState = async ({
})),
};
await upstash.set(stateKey, JSON.stringify(newState));
}
};

View File

@@ -50,6 +50,6 @@ export const generatePublishableKey = (env: string) => {
// .replace(/[+/=]/g, "") // Remove non-URL safe characters
// .slice(0, 100); // Ensure exactly 100 characters
let envString = env === AppEnv.Sandbox ? "test_" : "live_";
const envString = env === AppEnv.Sandbox ? "test_" : "live_";
return `am_pk_${envString}${KSUID.randomSync().string}`;
};

View File

@@ -19,8 +19,6 @@ export const checkEnvVars = () => {
process.exit(1);
}
if (!process.env.BETTER_AUTH_SECRET || !process.env.BETTER_AUTH_URL) {
console.error(`BETTER_AUTH_SECRET or BETTER_AUTH_URL is not set`);
process.exit(1);

View File

@@ -1,4 +1,4 @@
import { type Price } from "@autumn/shared";
import type { Price } from "@autumn/shared";
import type Stripe from "stripe";
// export const ItemSetSchema = z.object({

View File

@@ -1,12 +1,12 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
AppEnv,
customers,
features,
Organization,
type Organization,
products,
} from "@autumn/shared";
import { and, eq, inArray } from "drizzle-orm";
import type { DrizzleCli } from "@/db/initDrizzle.js";
export const clearCustomersInBatches = async ({
db,

View File

@@ -11,7 +11,7 @@ export const parseCsv = ({
return new Promise((resolve, reject) => {
const stream = fs.createReadStream(path);
const results: any[] = [];
let headers: string[] = [];
const headers: string[] = [];
stream
.pipe(csv({ separator: delimiter }))
.on("data", (data) => {

View File

@@ -1,8 +1,8 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import { AppEnv, FullCusProduct } from "@autumn/shared";
import { AppEnv, type FullCusProduct } from "@autumn/shared";
import { sql } from "drizzle-orm";
import type { DrizzleCli } from "@/db/initDrizzle.js";
let cusProductsQuery = ({
const cusProductsQuery = ({
lastProductId,
internalProductId,
pageSize = 250,
@@ -87,8 +87,8 @@ export const getAllFullCusProducts = async ({
internalProductId: string;
}) => {
let lastProductId = "";
let allData: any[] = [];
let pageSize = 500;
const allData: any[] = [];
const pageSize = 500;
while (true) {
const data = await db.execute(

View File

@@ -1,7 +1,6 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import { user } from "@autumn/shared";
import { desc } from "drizzle-orm";
import type { DrizzleCli } from "@/db/initDrizzle.js";
export const getAllUsers = async (db: DrizzleCli) => {
const users = [];

View File

@@ -1,9 +1,9 @@
import { Organization, AppEnv } from "@autumn/shared";
import { AppEnv, type Organization } from "@autumn/shared";
import { deleteAllStripeCustomers } from "@/external/stripe/stripeCusUtils.js";
import {
deactivateStripeMeters,
deleteAllStripeProducts,
} from "@/external/stripe/stripeProductUtils.js";
import { deleteAllStripeCustomers } from "@/external/stripe/stripeCusUtils.js";
export const resetOrgStripe = async ({ org }: { org: Organization }) => {
const env = AppEnv.Sandbox;

View File

@@ -1,8 +1,6 @@
import { Duration, EntInterval } from "@autumn/shared";
import { Entitlement } from "@autumn/shared";
import { add } from "date-fns";
import { Duration, EntInterval, Entitlement } from "@autumn/shared";
import { UTCDate } from "@date-fns/utc";
import { add } from "date-fns";
import { formatUnixToDate } from "./genUtils.js";
// Time conversion constants

View File

@@ -1,13 +1,13 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { LegacyVersion } from "@autumn/shared";
import chalk from "chalk";
import { addHours, addMonths } from "date-fns";
import type Stripe from "stripe";
import { TestFeature } from "@tests/setup/v2Features.js";
import { hoursToFinalizeInvoice } from "@tests/utils/constants.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import { getBasePrice } from "@tests/utils/testProductUtils/testProductUtils.js";
import chalk from "chalk";
import { addHours, addMonths } from "date-fns";
import type Stripe from "stripe";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";

View File

@@ -1,12 +1,12 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { LegacyVersion } from "@autumn/shared";
import chalk from "chalk";
import { addDays, addMonths } from "date-fns";
import type Stripe from "stripe";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { addDays, addMonths } from "date-fns";
import type Stripe from "stripe";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js";
import {

View File

@@ -1,8 +1,5 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { LegacyVersion } from "@autumn/shared";
import chalk from "chalk";
import { addMonths } from "date-fns";
import type Stripe from "stripe";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import {
@@ -11,6 +8,9 @@ import {
} from "@tests/utils/expectUtils/expectScheduleUtils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import { getBasePrice } from "@tests/utils/testProductUtils/testProductUtils.js";
import chalk from "chalk";
import { addMonths } from "date-fns";
import type Stripe from "stripe";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";

View File

@@ -1,6 +1,5 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { ApiVersion, type AppEnv, LegacyVersion } from "@autumn/shared";
import chalk from "chalk";
import { TestFeature } from "@tests/setup/v2Features.js";
import {
getPrepaidCusEnt,
@@ -8,6 +7,7 @@ import {
} from "@tests/utils/cusProductUtils/cusEntSearchUtils.js";
import { getMainCusProduct } from "@tests/utils/cusProductUtils/cusProductUtils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { timeout } from "@/utils/genUtils.js";

View File

@@ -1,6 +1,5 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { ApiVersion, type AppEnv, LegacyVersion } from "@autumn/shared";
import chalk from "chalk";
import { TestFeature } from "@tests/setup/v2Features.js";
import {
getLifetimeFreeCusEnt,
@@ -8,6 +7,7 @@ import {
} from "@tests/utils/cusProductUtils/cusEntSearchUtils.js";
import { getMainCusProduct } from "@tests/utils/cusProductUtils/cusProductUtils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { timeout } from "@/utils/genUtils.js";

View File

@@ -2,8 +2,6 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { ApiVersion, type AppEnv } from "@autumn/shared";
import chalk from "chalk";
import { addMonths } from "date-fns";
import { TestFeature } from "@tests/setup/v2Features.js";
import {
getLifetimeFreeCusEnt,
@@ -11,6 +9,8 @@ import {
} from "@tests/utils/cusProductUtils/cusEntSearchUtils.js";
import { getMainCusProduct } from "@tests/utils/cusProductUtils/cusProductUtils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { addMonths } from "date-fns";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { timeout } from "@/utils/genUtils.js";

View File

@@ -3,8 +3,8 @@ import { TestFeature } from "@tests/setup/v2Features.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js";
import {
constructFeatureItem,
constructArrearItem,
constructFeatureItem,
} from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { createSharedProducts } from "@/utils/scriptUtils/testUtils/createSharedProduct.js";

View File

@@ -5,12 +5,12 @@ import {
ProductItemInterval,
UsageModel,
} from "@autumn/shared";
import chalk from "chalk";
import type Stripe from "stripe";
import { TestFeature } from "@tests/setup/v2Features.js";
import { expectCustomerV0Correct } from "@tests/utils/expectUtils/expectCustomerV0Correct.js";
import { getExpectedInvoiceTotal } from "@tests/utils/expectUtils/expectInvoiceUtils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import type Stripe from "stripe";
import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js";
import { convertProductV2ToV1 } from "@/internal/products/productUtils/productV2Utils/convertProductV2ToV1.js";
import { constructRawProduct } from "@/utils/scriptUtils/createTestProducts.js";

View File

@@ -1,10 +1,10 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { ErrCode, LegacyVersion, type LimitedItem } from "@autumn/shared";
import chalk from "chalk";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import { expectAutumnError } from "@tests/utils/expectUtils/expectErrUtils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";

View File

@@ -1,10 +1,10 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { LegacyVersion, type LimitedItem } from "@autumn/shared";
import chalk from "chalk";
import type Stripe from "stripe";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import type Stripe from "stripe";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { timeout } from "@/utils/genUtils.js";
import {

View File

@@ -1,11 +1,11 @@
import { beforeAll, describe, test } from "bun:test";
import { ErrCode, LegacyVersion, type LimitedItem } from "@autumn/shared";
import chalk from "chalk";
import type Stripe from "stripe";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import { expectAutumnError } from "@tests/utils/expectUtils/expectErrUtils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import type Stripe from "stripe";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";

View File

@@ -1,14 +1,14 @@
import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import { addWeeks } from "date-fns";
import type Stripe from "stripe";
import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared";
import { replaceItems } from "@tests/attach/utils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import { expectSubQuantityCorrect } from "@tests/utils/expectUtils/expectContUseUtils.js";
import { advanceTestClock } from "@tests/utils/stripeUtils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { addWeeks } from "date-fns";
import type Stripe from "stripe";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";

View File

@@ -1,12 +1,12 @@
import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared";
import { beforeAll, describe, test } from "bun:test";
import chalk from "chalk";
import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared";
import { replaceItems } from "@tests/attach/utils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import { attachNewContUseAndExpectCorrect } from "@tests/utils/expectUtils/expectContUse/expectUpdateContUse.js";
import { expectSubQuantityCorrect } from "@tests/utils/expectUtils/expectContUseUtils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";

View File

@@ -1,14 +1,14 @@
import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import { addWeeks } from "date-fns";
import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared";
import { replaceItems } from "@tests/attach/utils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import { attachNewContUseAndExpectCorrect } from "@tests/utils/expectUtils/expectContUse/expectUpdateContUse.js";
import { expectSubQuantityCorrect } from "@tests/utils/expectUtils/expectContUseUtils.js";
import { advanceTestClock } from "@tests/utils/stripeUtils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { addWeeks } from "date-fns";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js";

View File

@@ -1,11 +1,11 @@
import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared";
import { beforeAll, describe, test } from "bun:test";
import chalk from "chalk";
import { addWeeks } from "date-fns";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import { advanceTestClock } from "@tests/utils/stripeUtils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { addWeeks } from "date-fns";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";

View File

@@ -1,12 +1,12 @@
import { beforeAll, describe, test } from "bun:test";
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import chalk from "chalk";
import type Stripe from "stripe";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import { expectFeaturesCorrect } from "@tests/utils/expectUtils/expectFeaturesCorrect.js";
import { expectProductAttached } from "@tests/utils/expectUtils/expectProductAttached.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";

View File

@@ -5,13 +5,13 @@ import {
type Entitlement,
ProductItemInterval,
} from "@autumn/shared";
import chalk from "chalk";
import { AutumnCli } from "@tests/cli/AutumnCli.js";
import { TestFeature } from "@tests/setup/v2Features.js";
import { expectCustomerV0Correct } from "@tests/utils/expectUtils/expectCustomerV0Correct.js";
import { timeout } from "@tests/utils/genUtils.js";
import { completeCheckoutForm } from "@tests/utils/stripeUtils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { convertProductV2ToV1 } from "@/internal/products/productUtils/productV2Utils/convertProductV2ToV1.js";

View File

@@ -1,12 +1,12 @@
import { beforeAll, describe, test } from "bun:test";
import { ApiVersion } from "@autumn/shared";
import chalk from "chalk";
import { AutumnCli } from "@tests/cli/AutumnCli.js";
import { TestFeature } from "@tests/setup/v2Features.js";
import { expectCustomerV0Correct } from "@tests/utils/expectUtils/expectCustomerV0Correct.js";
import { timeout } from "@tests/utils/genUtils.js";
import { completeCheckoutForm } from "@tests/utils/stripeUtils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";

View File

@@ -1,7 +1,5 @@
import { beforeAll, describe, test } from "bun:test";
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import chalk from "chalk";
import type Stripe from "stripe";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import {
@@ -9,6 +7,8 @@ import {
expectNextCycleCorrect,
} from "@tests/utils/expectUtils/expectScheduleUtils.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructFeatureItem } from "@/internal/products/product-items/productItemUtils.js";

View File

@@ -1,20 +1,20 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { ProductV2, Organization } from "@autumn/shared";
import { AppEnv } from "autumn-js";
import { expect } from "bun:test";
import type { Organization, ProductV2 } from "@autumn/shared";
import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect.js";
import { expectResetAtCorrect } from "@tests/utils/expectUtils/expectAttach/expectResetAtCorrect.js";
import { expectTrialEndsAtCorrect } from "@tests/utils/expectUtils/expectAttach/expectTrialEndsAt.js";
import { expectFeaturesCorrect } from "@tests/utils/expectUtils/expectFeaturesCorrect.js";
import {
expectSubItemsCorrect,
getSubsFromCusId,
} from "@tests/utils/expectUtils/expectSubUtils.js";
import Stripe from "stripe";
import { expect } from "bun:test";
import { expectFeaturesCorrect } from "@tests/utils/expectUtils/expectFeaturesCorrect.js";
import { expectResetAtCorrect } from "@tests/utils/expectUtils/expectAttach/expectResetAtCorrect.js";
import { isFreeProductV2 } from "@/internal/products/productUtils/classifyProduct.js";
import { expectTrialEndsAtCorrect } from "@tests/utils/expectUtils/expectAttach/expectTrialEndsAt.js";
import { timeout } from "@/utils/genUtils.js";
import type { AppEnv } from "autumn-js";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import type { AutumnInt } from "@/external/autumn/autumnCli.js";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import { expectSubToBeCorrect } from "@tests/merged/mergeUtils/expectSubCorrect.js";
import { isFreeProductV2 } from "@/internal/products/productUtils/classifyProduct.js";
import { timeout } from "@/utils/genUtils.js";
export const expectSubsSame = ({
subsBefore,
@@ -25,8 +25,8 @@ export const expectSubsSame = ({
}) => {
// let invoicesBefore = subsBefore.map((sub) => sub.latest_invoice);
// let invoicesAfter = subsAfter.map((sub) => sub.latest_invoice);
let subIdsBefore = subsBefore.map((sub) => sub.id);
let subIdsAfter = subsAfter.map((sub) => sub.id);
const subIdsBefore = subsBefore.map((sub) => sub.id);
const subIdsAfter = subsAfter.map((sub) => sub.id);
const periodsBefore = subsBefore.map((sub) => subToPeriodStartEnd({ sub }));
const periodsAfter = subsAfter.map((sub) => subToPeriodStartEnd({ sub }));

View File

@@ -1,13 +1,9 @@
import {
BillingInterval,
LegacyVersion,
type ProductV2,
} from "@autumn/shared";
import { beforeAll, describe, test } from "bun:test";
import chalk from "chalk";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import { BillingInterval, LegacyVersion, type ProductV2 } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";

View File

@@ -1,18 +1,18 @@
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import type Stripe from "stripe";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import {
expectDowngradeCorrect,
expectNextCycleCorrect,
} from "@tests/utils/expectUtils/expectScheduleUtils.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import type Stripe from "stripe";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
const testCase = "others1";

View File

@@ -11,83 +11,83 @@ import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js"
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
const pro = constructProduct({
type: "pro",
items: [
constructFeatureItem({
featureId: TestFeature.Credits,
includedUsage: 500,
}),
],
type: "pro",
items: [
constructFeatureItem({
featureId: TestFeature.Credits,
includedUsage: 500,
}),
],
});
const testCase = "others10";
describe(`${chalk.yellowBright(`${testCase}/idempotency: idempotency key already exists`)}`, () => {
const customerId = testCase;
const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
const idempotencyKey = generateId("it");
const customerId = testCase;
const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
const idempotencyKey = generateId("it");
let results: PromiseSettledResult<
Awaited<ReturnType<typeof autumnV1.attach>>
>[];
let results: PromiseSettledResult<
Awaited<ReturnType<typeof autumnV1.attach>>
>[];
beforeAll(async () => {
await initCustomerV3({
ctx,
customerId,
withTestClock: true,
attachPm: "success",
});
beforeAll(async () => {
await initCustomerV3({
ctx,
customerId,
withTestClock: true,
attachPm: "success",
});
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
});
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
});
results = await Promise.allSettled([
autumnV1.attach(
{
customer_id: customerId,
product_id: pro.id,
},
{
"idempotency-key": idempotencyKey,
},
),
autumnV1.attach(
{
customer_id: customerId,
product_id: pro.id,
},
{
"idempotency-key": idempotencyKey,
},
),
]);
});
results = await Promise.allSettled([
autumnV1.attach(
{
customer_id: customerId,
product_id: pro.id,
},
{
"idempotency-key": idempotencyKey,
},
),
autumnV1.attach(
{
customer_id: customerId,
product_id: pro.id,
},
{
"idempotency-key": idempotencyKey,
},
),
]);
});
test("should reject duplicate idempotency key with 409", async () => {
// Exactly one request should succeed
const fulfilled = results.filter((r) => r.status === "fulfilled");
const rejected = results.filter((r) => r.status === "rejected");
test("should reject duplicate idempotency key with 409", async () => {
// Exactly one request should succeed
const fulfilled = results.filter((r) => r.status === "fulfilled");
const rejected = results.filter((r) => r.status === "rejected");
expect(fulfilled).toHaveLength(1);
expect(rejected).toHaveLength(1);
expect(fulfilled).toHaveLength(1);
expect(rejected).toHaveLength(1);
// The successful request should have attached the product
const successResult = fulfilled[0] as PromiseFulfilledResult<
Awaited<ReturnType<typeof autumnV1.attach>>
>;
expect(successResult.value.success).toBe(true);
expect(successResult.value.customer_id).toBe(customerId);
expect(successResult.value.product_ids).toContain(pro.id);
// The successful request should have attached the product
const successResult = fulfilled[0] as PromiseFulfilledResult<
Awaited<ReturnType<typeof autumnV1.attach>>
>;
expect(successResult.value.success).toBe(true);
expect(successResult.value.customer_id).toBe(customerId);
expect(successResult.value.product_ids).toContain(pro.id);
// The rejected request should have the duplicate idempotency key error
const rejectedResult = rejected[0] as PromiseRejectedResult;
expect(rejectedResult.reason).toBeInstanceOf(AutumnError);
expect((rejectedResult.reason as AutumnError).code).toBe(
ErrCode.DuplicateIdempotencyKey,
);
});
// The rejected request should have the duplicate idempotency key error
const rejectedResult = rejected[0] as PromiseRejectedResult;
expect(rejectedResult.reason).toBeInstanceOf(AutumnError);
expect((rejectedResult.reason as AutumnError).code).toBe(
ErrCode.DuplicateIdempotencyKey,
);
});
});

View File

@@ -1,9 +1,9 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { LegacyVersion } from "@autumn/shared";
import chalk from "chalk";
import { TestFeature } from "@tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js";
import { CusService } from "@/internal/customers/CusService.js";

View File

@@ -1,15 +1,15 @@
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import chalk from "chalk";
import type Stripe from "stripe";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js";
import { CusService } from "@/internal/customers/CusService.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
const testCase = "others3";

View File

@@ -1,17 +1,17 @@
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import type Stripe from "stripe";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import { TestFeature } from "@tests/setup/v2Features.js";
import { expectAttachCorrect } from "@tests/utils/expectUtils/expectAttach.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
import { getBasePrice } from "@tests/utils/testProductUtils/testProductUtils.js";
import chalk from "chalk";
import type Stripe from "stripe";
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 { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
export const pro = constructProduct({
items: [constructArrearItem({ featureId: TestFeature.Words })],

Some files were not shown because too many files have changed in this diff Show More