wip
This commit is contained in:
7
bunfig.toml
Normal file
7
bunfig.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[test]
|
||||
preload = ["./server/tests/setup-integration-tests.ts"]
|
||||
timeout = 0
|
||||
|
||||
|
||||
[test.env]
|
||||
NODE_ENV = "test"
|
||||
@@ -1,9 +0,0 @@
|
||||
[phases.install]
|
||||
onlyIncludeFiles = [
|
||||
"package.json",
|
||||
"bun.lock",
|
||||
"./server/package.json",
|
||||
"./shared/package.json",
|
||||
"./vite/package.json",
|
||||
]
|
||||
|
||||
32
scripts/testScripts/getDescribeAtCursor.ts
Normal file
32
scripts/testScripts/getDescribeAtCursor.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
const file = process.argv[2];
|
||||
const lineNum = parseInt(process.argv[3], 10);
|
||||
|
||||
const content = readFileSync(file, "utf-8");
|
||||
const lines = content.split("\n");
|
||||
|
||||
// Walk backwards from cursor to find enclosing describe or test.concurrent
|
||||
for (let i = lineNum - 1; i >= 0; i--) {
|
||||
const line = lines[i];
|
||||
|
||||
// Match describe/test.concurrent with chalk.yellowBright or similar
|
||||
const chalkMatch = line.match(
|
||||
/(?:describe|test\.concurrent)\s*\(\s*`\$\{chalk\.\w+\(["'](.*?)["']\)\}`/,
|
||||
);
|
||||
if (chalkMatch) {
|
||||
console.log(chalkMatch[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Match simple describe/test.concurrent: ("name", ...) or ('name', ...) or (`name`, ...)
|
||||
const simpleMatch = line.match(
|
||||
/(?:describe|test\.concurrent)\s*\(\s*["'`](.*?)["'`]/,
|
||||
);
|
||||
if (simpleMatch) {
|
||||
console.log(simpleMatch[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(".*"); // fallback: run all
|
||||
9
server/src/external/autumn/autumnCli.ts
vendored
9
server/src/external/autumn/autumnCli.ts
vendored
@@ -628,7 +628,10 @@ export class AutumnInt {
|
||||
|
||||
track = async (
|
||||
params: TrackParams,
|
||||
{ skipCache = false }: { skipCache?: boolean } = {},
|
||||
{
|
||||
skipCache = false,
|
||||
timeout,
|
||||
}: { skipCache?: boolean; timeout?: number } = {},
|
||||
) => {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (skipCache) {
|
||||
@@ -636,6 +639,10 @@ export class AutumnInt {
|
||||
}
|
||||
|
||||
const data = await this.post(`/track?${queryParams.toString()}`, params);
|
||||
|
||||
if (timeout) {
|
||||
await new Promise((resolve) => setTimeout(resolve, timeout));
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Hono } from "hono";
|
||||
import { handleUpdateSubscriptionPreview } from "@/internal/billing/v2/subscriptionUpdate/handleUpdateSubscriptionPreview.js";
|
||||
import { handlePreviewUpdateSubscription } from "@/internal/billing/v2/subscriptionUpdate/handlePreviewUpdateSubscription.js";
|
||||
import type { HonoEnv } from "../../honoUtils/HonoEnv.js";
|
||||
import { handleAttach } from "./attach/handleAttach.js";
|
||||
import { handleCheckoutV2 } from "./checkout/handleCheckoutV2.js";
|
||||
@@ -16,6 +16,6 @@ billingRouter.post("/attach_v2", ...handleAttachV2);
|
||||
|
||||
billingRouter.post("/subscriptions/update", ...handleUpdateSubscription);
|
||||
billingRouter.post(
|
||||
"/subscriptions/preview/update",
|
||||
...handleUpdateSubscriptionPreview,
|
||||
"/subscriptions/preview_update",
|
||||
...handlePreviewUpdateSubscription,
|
||||
);
|
||||
|
||||
@@ -35,7 +35,7 @@ export const computeAttachPlan = async ({
|
||||
// When to build checkout action?
|
||||
|
||||
// 2. Build autumn line items
|
||||
const autumnLineItems = buildAutumnLineItems({
|
||||
const lineItems = buildAutumnLineItems({
|
||||
ctx,
|
||||
newCusProducts,
|
||||
ongoingCustomerProduct: ongoingCusProductAction?.cusProduct,
|
||||
@@ -56,13 +56,13 @@ export const computeAttachPlan = async ({
|
||||
// 6. Build stripe invoice action
|
||||
// const stripeInvoiceAction = buildStripeInvoiceAction({
|
||||
// attachContext,
|
||||
// autumnLineItems,
|
||||
// lineItems,
|
||||
// stripeSubAction,
|
||||
// newCusProducts,
|
||||
// });
|
||||
|
||||
return {
|
||||
autumnLineItems,
|
||||
lineItems,
|
||||
|
||||
ongoingCusProductAction,
|
||||
scheduledCusProductAction,
|
||||
|
||||
@@ -30,14 +30,14 @@ export const evaluateStripeBillingPlan = ({
|
||||
finalCustomerProducts: finalFullCustomer.customer_products,
|
||||
});
|
||||
|
||||
const { autumnLineItems } = autumnBillingPlan;
|
||||
const { lineItems } = autumnBillingPlan;
|
||||
|
||||
const stripeInvoiceAction = buildStripeInvoiceAction({
|
||||
lineItems: autumnLineItems,
|
||||
lineItems,
|
||||
});
|
||||
|
||||
const stripeInvoiceItemsAction = buildStripeInvoiceItemsAction({
|
||||
lineItems: autumnLineItems,
|
||||
lineItems,
|
||||
billingContext,
|
||||
});
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ export const computeQuantityUpdateDetails = ({
|
||||
});
|
||||
}
|
||||
|
||||
const autumnLineItems = buildQuantityUpdateLineItems({
|
||||
const lineItems = buildQuantityUpdateLineItems({
|
||||
ctx,
|
||||
customerProduct,
|
||||
feature,
|
||||
@@ -115,6 +115,6 @@ export const computeQuantityUpdateDetails = ({
|
||||
featureId,
|
||||
customerEntitlementId,
|
||||
customerEntitlementBalanceChange,
|
||||
autumnLineItems,
|
||||
lineItems,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -53,7 +53,7 @@ export const computeInvoiceAction = ({
|
||||
// If subscription action is update, we need to create an invoice
|
||||
const stripeSubscriptionActionType = stripeSubscriptionAction?.type;
|
||||
if (stripeSubscriptionActionType === "update") {
|
||||
const autumnLineItems = buildAutumnLineItems({
|
||||
const lineItems = buildAutumnLineItems({
|
||||
ctx,
|
||||
newCusProducts: [toCustomerProduct],
|
||||
ongoingCustomerProduct: fromCustomerProduct,
|
||||
@@ -62,7 +62,7 @@ export const computeInvoiceAction = ({
|
||||
});
|
||||
|
||||
const addLineParams = lineItemsToInvoiceAddLinesParams({
|
||||
lineItems: autumnLineItems,
|
||||
lineItems,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -60,7 +60,7 @@ export const computeSubscriptionUpdateCustomPlan = async ({
|
||||
freeTrialPlan,
|
||||
});
|
||||
|
||||
const autumnLineItems = buildAutumnLineItems({
|
||||
const lineItems = buildAutumnLineItems({
|
||||
ctx,
|
||||
newCustomerProducts: [newFullCustomerProduct],
|
||||
deletedCustomerProduct: customerProduct,
|
||||
@@ -76,6 +76,6 @@ export const computeSubscriptionUpdateCustomPlan = async ({
|
||||
customPrices: customPrices,
|
||||
customEntitlements: customEnts,
|
||||
customFreeTrial: customFreeTrial,
|
||||
autumnLineItems,
|
||||
lineItems,
|
||||
} satisfies AutumnBillingPlan;
|
||||
};
|
||||
|
||||
@@ -31,8 +31,8 @@ export const computeSubscriptionUpdateQuantityPlan = ({
|
||||
}),
|
||||
);
|
||||
|
||||
const autumnLineItems = quantityUpdateDetails.flatMap(
|
||||
(detail) => detail.autumnLineItems,
|
||||
const lineItems = quantityUpdateDetails.flatMap(
|
||||
(detail) => detail.lineItems,
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -54,7 +54,7 @@ export const computeSubscriptionUpdateQuantityPlan = ({
|
||||
balanceChange: detail.customerEntitlementBalanceChange,
|
||||
})),
|
||||
|
||||
autumnLineItems,
|
||||
lineItems,
|
||||
// quantityUpdateDetails,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { createRoute } from "../../../../honoMiddlewares/routeHandler";
|
||||
import { computeSubscriptionUpdatePlan } from "./compute/computeSubscriptionUpdatePlan";
|
||||
import { fetchUpdateSubscriptionBillingContext } from "./fetch/fetchUpdateSubscriptionBillingContext";
|
||||
|
||||
export const handleUpdateSubscriptionPreview = createRoute({
|
||||
export const handlePreviewUpdateSubscription = createRoute({
|
||||
body: UpdateSubscriptionV0ParamsSchema,
|
||||
handler: async (c) => {
|
||||
const ctx = c.get("ctx");
|
||||
@@ -28,6 +28,8 @@ export const handleUpdateSubscriptionPreview = createRoute({
|
||||
autumnBillingPlan,
|
||||
});
|
||||
|
||||
// Convert to preview response
|
||||
|
||||
return c.json(
|
||||
{
|
||||
autumn: autumnBillingPlan,
|
||||
@@ -33,7 +33,7 @@ export const AutumnBillingPlanSchema = z.object({
|
||||
customEntitlements: z.array(EntitlementSchema), // Custom entitlements to insert
|
||||
customFreeTrial: FreeTrialSchema.optional(), // Custom free trial to insert
|
||||
|
||||
autumnLineItems: z.array(LineItemSchema),
|
||||
lineItems: z.array(LineItemSchema),
|
||||
|
||||
updateCustomerEntitlements: z
|
||||
.array(UpdateCustomerEntitlementSchema)
|
||||
|
||||
@@ -48,7 +48,7 @@ export type UpdateOneOffAction = {
|
||||
};
|
||||
|
||||
export type AttachPlan = {
|
||||
autumnLineItems: LineItem[];
|
||||
lineItems: LineItem[];
|
||||
|
||||
// 1. Autumn actions
|
||||
|
||||
@@ -69,7 +69,7 @@ export const QuantityUpdateDetailsSchema = z.object({
|
||||
featureId: z.string(),
|
||||
customerEntitlementId: z.string(),
|
||||
customerEntitlementBalanceChange: z.number(),
|
||||
autumnLineItems: z.array(LineItemSchema),
|
||||
lineItems: z.array(LineItemSchema),
|
||||
});
|
||||
|
||||
export type QuantityUpdateDetails = z.infer<typeof QuantityUpdateDetailsSchema>;
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { BillingPreviewResponse } from "@autumn/shared";
|
||||
import type { BillingPlan } from "@/internal/billing/v2/types/billingPlan";
|
||||
|
||||
export const billingPlanToPreviewResponse = ({
|
||||
billingPlan,
|
||||
}: {
|
||||
billingPlan: BillingPlan;
|
||||
}): BillingPreviewResponse => {
|
||||
// 1. Get lines
|
||||
const autumnBillingPlan = billingPlan.autumn;
|
||||
|
||||
// const previewLineItems = autumnBillingPlan.lineItems.map((line) => ({
|
||||
// description: line.description,
|
||||
// amount: line.amount,
|
||||
// }));
|
||||
|
||||
// const total = autumnBillingPlanLines.reduce(
|
||||
// (acc, line) => acc + line.amount,
|
||||
// 0,
|
||||
// );
|
||||
|
||||
// return {
|
||||
// customer_id: billingPlan.customer_id,
|
||||
// };
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import { clearOrg, setupOrg } from "@tests/utils/setup.js";
|
||||
import { initDrizzle } from "@/db/initDrizzle.js";
|
||||
|
||||
const ORG_SLUG = process.env.TESTS_ORG!;
|
||||
const DEFAULT_ENV = AppEnv.Sandbox;
|
||||
|
||||
describe("Initialize org for tests", () => {
|
||||
it("should initialize org", async function () {
|
||||
this.timeout(1000000000);
|
||||
this.org = await clearOrg({ orgSlug: ORG_SLUG, env: DEFAULT_ENV });
|
||||
this.env = DEFAULT_ENV;
|
||||
const { db, client } = initDrizzle();
|
||||
|
||||
this.db = db;
|
||||
this.client = client;
|
||||
|
||||
await setupOrg({
|
||||
orgId: this.org.id,
|
||||
env: DEFAULT_ENV,
|
||||
});
|
||||
|
||||
console.log("--------------------------------");
|
||||
});
|
||||
});
|
||||
@@ -35,8 +35,8 @@ Tests are organized into logical groups that can be run via shell scripts in `se
|
||||
- **setupMain.ts** - Main setup script that clears and initializes the test organization
|
||||
- **global.ts** - Shared test data (features, products, rewards, etc.)
|
||||
- **utils/** - Test utilities and helper functions
|
||||
- `setupUtils/clearOrg.ts` - Clears test org data
|
||||
- `setupUtils/setupOrg.ts` - Sets up test org with features and products
|
||||
- `setup/clearOrg.ts` - Clears test org data
|
||||
- `setup/setupOrg.ts` - Sets up test org with features and products
|
||||
- `setup.ts` - Re-exports setup utilities
|
||||
- `init.ts` - Test initialization helpers
|
||||
- `stripeUtils.ts` - Stripe-specific test utilities
|
||||
|
||||
@@ -1,31 +1,165 @@
|
||||
# Test Writing Guide
|
||||
|
||||
## Initial Notes (To be organized later)
|
||||
## Quick Start
|
||||
|
||||
### Customer Initialization
|
||||
Use `initTestScenario` for the fastest test setup:
|
||||
|
||||
#### Default Products
|
||||
- For tests involving default products, use the `withDefault: true` flag in `initCustomerV3()`
|
||||
- This ensures the customer is created with the default product attached
|
||||
- Example:
|
||||
```typescript
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
customerData: { fingerprint: "test" },
|
||||
withTestClock: false,
|
||||
withDefault: true, // Attach default product on creation
|
||||
```typescript
|
||||
import { test } from "bun:test";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import { items } from "@tests/utils/fixtures/items.js";
|
||||
import { products } from "@tests/utils/fixtures/products.js";
|
||||
import { initTestScenario } from "@tests/utils/testInitUtils/initTestScenario.js";
|
||||
|
||||
test.concurrent("my test", async () => {
|
||||
const messagesItem = items.monthlyMessages({ includedUsage: 500 });
|
||||
|
||||
const free = products.base({ items: [messagesItem] });
|
||||
|
||||
const { customerId, autumnV1 } = await initTestScenario({
|
||||
customerId: "my-unique-test-id",
|
||||
products: [free],
|
||||
attachProducts: [free.id], // Pass original IDs - auto-prefixed
|
||||
customerOptions: {
|
||||
withTestClock: true,
|
||||
attachPm: "success",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### Fingerprint
|
||||
- For tests involving fingerprint, pass in `fingerprint` through `customerData` in `initCustomerV3()`
|
||||
- Example:
|
||||
```typescript
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
customerData: { fingerprint: "test" }, // Pass fingerprint here
|
||||
withTestClock: false,
|
||||
// Your test logic here
|
||||
await autumnV1.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: 100,
|
||||
});
|
||||
```
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fixtures
|
||||
|
||||
### Item Fixtures (`@tests/utils/fixtures/items`)
|
||||
|
||||
Pre-configured product items for common feature types:
|
||||
|
||||
```typescript
|
||||
import { items } from "@tests/utils/fixtures/items.js";
|
||||
```
|
||||
|
||||
| Item | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `items.dashboard()` | Boolean feature (on/off) | - |
|
||||
| `items.monthlyMessages({ includedUsage })` | Resets monthly | 100 |
|
||||
| `items.monthlyWords({ includedUsage })` | Resets monthly | 100 |
|
||||
| `items.monthlyCredits({ includedUsage })` | Resets monthly | 100 |
|
||||
| `items.unlimitedMessages()` | No usage cap | - |
|
||||
| `items.lifetimeMessages({ includedUsage })` | Never resets | 100 |
|
||||
| `items.prepaidMessages({ includedUsage })` | Buy upfront ($10/unit) | 0 |
|
||||
| `items.consumableMessages({ includedUsage })` | Pay-per-use ($0.10/unit) | 0 |
|
||||
| `items.allocatedUsers({ includedUsage })` | Prorated seats ($10/seat) | 0 |
|
||||
|
||||
### Product Fixtures (`@tests/utils/fixtures/products`)
|
||||
|
||||
```typescript
|
||||
import { products } from "@tests/utils/fixtures/products.js";
|
||||
```
|
||||
|
||||
| Product | Description |
|
||||
|---------|-------------|
|
||||
| `products.base({ items, id?, isDefault? })` | No base price, `id` defaults to "base", `isDefault` defaults to `false` |
|
||||
|
||||
**Example:**
|
||||
```typescript
|
||||
// Simple product
|
||||
const free = products.base({ items: [items.monthlyMessages()] });
|
||||
|
||||
// With custom ID
|
||||
const addon = products.base({
|
||||
id: "messages-addon",
|
||||
items: [items.prepaidMessages()]
|
||||
});
|
||||
|
||||
// As default product
|
||||
const defaultProd = products.base({
|
||||
items: [items.dashboard()],
|
||||
isDefault: true
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test Scenario Initialization
|
||||
|
||||
### `initTestScenario`
|
||||
|
||||
Combines customer creation, product creation, and attachment into one call.
|
||||
|
||||
```typescript
|
||||
import { initTestScenario } from "@tests/utils/testInitUtils/initTestScenario.js";
|
||||
|
||||
const { customerId, products, autumnV1, autumnV2, testClockId, customer, ctx } =
|
||||
await initTestScenario({
|
||||
customerId: "unique-test-id", // Used as customer ID AND product prefix
|
||||
products: [free, addon], // Products to create
|
||||
attachProducts: [free.id], // Original IDs (auto-prefixed with customerId_)
|
||||
customerOptions: {
|
||||
withTestClock: true, // Default: true
|
||||
attachPm: "success", // "success" | "fail" | "authenticate"
|
||||
withDefault: false, // Default: false
|
||||
customerData: { fingerprint }, // Optional customer data
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Important:** Product IDs are prefixed with `customerId_` for test isolation.
|
||||
- You pass: `attachProducts: ["base"]`
|
||||
- Actual product ID becomes: `"my-test-id_base"`
|
||||
|
||||
---
|
||||
|
||||
## Legacy Patterns
|
||||
|
||||
### Customer Initialization (Direct)
|
||||
|
||||
For cases where `initTestScenario` doesn't fit:
|
||||
|
||||
```typescript
|
||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
customerData: { fingerprint: "test" },
|
||||
withTestClock: true,
|
||||
withDefault: true, // Attach default product on creation
|
||||
attachPm: "success",
|
||||
});
|
||||
```
|
||||
|
||||
### Product Initialization (Direct)
|
||||
|
||||
```typescript
|
||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [free, pro],
|
||||
prefix: customerId, // Prefix product IDs for isolation
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Run specific test block
|
||||
Place cursor inside a `test.concurrent()` or `describe()` block and press `Cmd+T`.
|
||||
|
||||
### Rerun last test
|
||||
`Cmd+Shift+P` → "Rerun Last Task"
|
||||
|
||||
### Run entire file
|
||||
```bash
|
||||
bun test path/to/file.test.ts
|
||||
```
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import { clearOrg, setupOrg } from "@tests/utils/setup.js";
|
||||
|
||||
const ORG_SLUG = process.env.TESTS_ORG!;
|
||||
const DEFAULT_ENV = AppEnv.Sandbox;
|
||||
|
||||
describe("Initialize alex org for tests", () => {
|
||||
it("should initialize org", async function () {
|
||||
this.org = await clearOrg({ orgSlug: ORG_SLUG, env: DEFAULT_ENV });
|
||||
this.env = DEFAULT_ENV;
|
||||
await setupOrg({
|
||||
orgId: this.org.id,
|
||||
env: DEFAULT_ENV,
|
||||
});
|
||||
|
||||
console.log("--------------------------------");
|
||||
});
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
import chalk from "chalk";
|
||||
import { AutumnCli } from "@tests/cli/AutumnCli.js";
|
||||
import { compareMainProduct } from "@tests/utils/compare.js";
|
||||
import { initCustomer } from "@tests/utils/init.js";
|
||||
import { alexProducts } from "./init.js";
|
||||
import { runEventsAndCheckBalances } from "./utils.js";
|
||||
|
||||
describe(chalk.yellowBright("Free customer"), () => {
|
||||
const customerId = "alex-free-customer";
|
||||
before("initializing customer", async function () {
|
||||
await initCustomer({
|
||||
customer_data: {
|
||||
id: customerId,
|
||||
// name: null,
|
||||
// email: null,
|
||||
},
|
||||
db: this.db,
|
||||
org: this.org,
|
||||
env: this.env,
|
||||
});
|
||||
});
|
||||
|
||||
// 1. Check that customer has correct product & entitlements
|
||||
it("GET customer has correct product & entitlements", async () => {
|
||||
const customer = await AutumnCli.getCustomer(customerId);
|
||||
compareMainProduct({
|
||||
sent: alexProducts.free,
|
||||
cusRes: customer,
|
||||
});
|
||||
});
|
||||
|
||||
// 3. Run /events for each feature, and check that the balance is updated correctly
|
||||
it("should run /events for each feature and have correct balance afterwards", async () => {
|
||||
const entitlements = Object.values(alexProducts.free.entitlements);
|
||||
|
||||
await runEventsAndCheckBalances({
|
||||
customerId,
|
||||
entitlements,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,58 +0,0 @@
|
||||
import { CusProductStatus, InvoiceStatus } from "@autumn/shared";
|
||||
import { expect } from "chai";
|
||||
import { timeout } from "@tests/utils/genUtils.js";
|
||||
import { AutumnCli } from "@tests/cli/AutumnCli.js";
|
||||
import { compareMainProduct } from "@tests/utils/compare.js";
|
||||
import { initCustomer } from "@tests/utils/init.js";
|
||||
import { completeCheckoutForm } from "@tests/utils/stripeUtils.js";
|
||||
import { alexProducts } from "./init.js";
|
||||
import { runEventsAndCheckBalances } from "./utils.js";
|
||||
import chalk from "chalk";
|
||||
|
||||
describe(chalk.yellowBright("Pro entitlements"), () => {
|
||||
let customerId = "alex-pro-customer";
|
||||
before("initializing customer", async function () {
|
||||
await initCustomer({
|
||||
customer_data: {
|
||||
id: customerId,
|
||||
// name: "Alex Pro Customer",
|
||||
email: "alex-pro-customer@test.com",
|
||||
},
|
||||
db: this.db,
|
||||
org: this.org,
|
||||
env: this.env,
|
||||
});
|
||||
});
|
||||
|
||||
it("should upgrade to Pro after calling /attach", async function () {
|
||||
const res = await AutumnCli.attach({
|
||||
customerId,
|
||||
productId: alexProducts.pro.id,
|
||||
});
|
||||
|
||||
await completeCheckoutForm(res.checkout_url);
|
||||
await timeout(20000);
|
||||
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
expect(typeof cusRes.customer.name).to.equal("string");
|
||||
|
||||
compareMainProduct({
|
||||
sent: alexProducts.pro,
|
||||
cusRes,
|
||||
status: CusProductStatus.Trialing,
|
||||
});
|
||||
|
||||
// Check invoice is correct
|
||||
const invoices = cusRes.invoices;
|
||||
expect(invoices.length).to.equal(1);
|
||||
expect(invoices[0].total).to.equal(0);
|
||||
expect(invoices[0].status).to.equal(InvoiceStatus.Paid);
|
||||
});
|
||||
|
||||
it("should run event sending and check balances for each entitlement", async function () {
|
||||
await runEventsAndCheckBalances({
|
||||
customerId,
|
||||
entitlements: Object.values(alexProducts.pro.entitlements),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,51 +0,0 @@
|
||||
import { compareMainProduct } from "@tests/utils/compare.js";
|
||||
import { AutumnCli } from "@tests/cli/AutumnCli.js";
|
||||
import { initCustomer } from "@tests/utils/init.js";
|
||||
import { alexProducts } from "./init.js";
|
||||
import { CusProductStatus } from "@autumn/shared";
|
||||
import { completeCheckoutForm } from "@tests/utils/stripeUtils.js";
|
||||
import { timeout } from "@tests/utils/genUtils.js";
|
||||
import { runEventsAndCheckBalances } from "./utils.js";
|
||||
import chalk from "chalk";
|
||||
|
||||
describe(chalk.yellowBright("Premium plan"), () => {
|
||||
let customerId = "alex-premium-customer";
|
||||
|
||||
before("initializing customer", async function () {
|
||||
await initCustomer({
|
||||
customer_data: {
|
||||
id: customerId,
|
||||
name: "Alex Premium Customer",
|
||||
email: "alex-premium-customer@test.com",
|
||||
},
|
||||
db: this.db,
|
||||
org: this.org,
|
||||
env: this.env,
|
||||
// attachPm: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should attach premium product", async function () {
|
||||
const res = await AutumnCli.attach({
|
||||
customerId,
|
||||
productId: alexProducts.premium.id,
|
||||
});
|
||||
|
||||
await completeCheckoutForm(res.checkout_url);
|
||||
await timeout(10000);
|
||||
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
compareMainProduct({
|
||||
sent: alexProducts.premium,
|
||||
cusRes,
|
||||
status: CusProductStatus.Trialing,
|
||||
});
|
||||
});
|
||||
|
||||
it("should send events and check balances", async function () {
|
||||
await runEventsAndCheckBalances({
|
||||
customerId,
|
||||
entitlements: Object.values(alexProducts.premium.entitlements),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,219 +0,0 @@
|
||||
import {
|
||||
AllowanceType,
|
||||
ApiVersion,
|
||||
CusProductStatus,
|
||||
EntInterval,
|
||||
type Entitlement,
|
||||
} from "@autumn/shared";
|
||||
import { expect } from "chai";
|
||||
import chalk from "chalk";
|
||||
import { AutumnCli } from "@tests/cli/AutumnCli.js";
|
||||
import { compareMainProduct } from "@tests/utils/compare.js";
|
||||
import { timeout } from "@tests/utils/genUtils.js";
|
||||
import { completeCheckoutForm } from "@tests/utils/stripeUtils.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { initCustomerV2 } from "@/utils/scriptUtils/initCustomer.js";
|
||||
import { alexFeatures, alexProducts } from "./init.js";
|
||||
import {
|
||||
checkFeatureHasCorrectBalance,
|
||||
runEventsAndCheckBalances,
|
||||
} from "./utils.js";
|
||||
|
||||
const autumn = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
describe(chalk.yellowBright("Top ups"), () => {
|
||||
const customerId = "alex-top-up-customer";
|
||||
before("initializing customer", async function () {
|
||||
await initCustomerV2({
|
||||
autumn,
|
||||
customerId,
|
||||
org: this.org,
|
||||
env: this.env,
|
||||
db: this.db,
|
||||
attachPm: "success",
|
||||
});
|
||||
// await initCustomer({
|
||||
// customer_data: {
|
||||
// id: customerId,
|
||||
// name: "Alex Top Up Customer",
|
||||
// email: "alex-top-up-customer@test.com",
|
||||
// },
|
||||
// db: this.db,
|
||||
// org: this.org,
|
||||
// env: this.env,
|
||||
// attachPm: true,
|
||||
// });
|
||||
});
|
||||
|
||||
it("should attach pro product", async () => {
|
||||
await timeout(5000);
|
||||
await AutumnCli.attach({
|
||||
customerId,
|
||||
productId: alexProducts.pro.id,
|
||||
});
|
||||
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
compareMainProduct({
|
||||
sent: alexProducts.pro,
|
||||
cusRes,
|
||||
status: CusProductStatus.Trialing,
|
||||
});
|
||||
});
|
||||
|
||||
const overrideQuantity = 5;
|
||||
const billingUnits =
|
||||
alexProducts.topUpMessages.prices[0].config.billing_units;
|
||||
const prodEnt = alexProducts.topUpMessages.entitlements.topUpMessage;
|
||||
let leftoverBalance = 0;
|
||||
it("should attach top up messages through force checkout", async () => {
|
||||
const res = await AutumnCli.attach({
|
||||
customerId,
|
||||
productId: alexProducts.topUpMessages.id,
|
||||
forceCheckout: true,
|
||||
});
|
||||
|
||||
await completeCheckoutForm(res.checkout_url, overrideQuantity);
|
||||
await timeout(10000);
|
||||
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
// Get product
|
||||
const product = cusRes.add_ons.find(
|
||||
(p: any) => p.id === alexProducts.topUpMessages.id,
|
||||
);
|
||||
expect(product).to.exist;
|
||||
expect(product.status).to.equal(CusProductStatus.Active);
|
||||
|
||||
// Check quantity is correct
|
||||
const cusEnt = cusRes.entitlements.find(
|
||||
(e: any) => e.feature_id === alexFeatures.topUpMessage.id,
|
||||
);
|
||||
|
||||
expect(cusEnt).to.exist;
|
||||
expect(cusEnt.balance).to.equal(overrideQuantity * billingUnits);
|
||||
expect(cusEnt.interval).to.equal(prodEnt.interval);
|
||||
});
|
||||
|
||||
// Try buy again
|
||||
it("should buy top ups again and have correct balance", async () => {
|
||||
// 1. Update leftover balance
|
||||
const { allowed, balanceObj }: any = await AutumnCli.entitled(
|
||||
customerId,
|
||||
alexFeatures.topUpMessage.id,
|
||||
true,
|
||||
);
|
||||
leftoverBalance = balanceObj.balance;
|
||||
|
||||
const res = await AutumnCli.attach({
|
||||
customerId,
|
||||
productId: alexProducts.topUpMessages.id,
|
||||
forceCheckout: true,
|
||||
});
|
||||
|
||||
await completeCheckoutForm(res.checkout_url, overrideQuantity);
|
||||
await timeout(10000);
|
||||
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
// Get product
|
||||
const product = cusRes.add_ons.find(
|
||||
(p: any) => p.id === alexProducts.topUpMessages.id,
|
||||
);
|
||||
expect(product).to.exist;
|
||||
expect(product.status).to.equal(CusProductStatus.Active);
|
||||
|
||||
// Check quantity is correct
|
||||
const cusEnt = cusRes.entitlements.find(
|
||||
(e: any) => e.feature_id === alexFeatures.topUpMessage.id,
|
||||
);
|
||||
expect(cusEnt).to.exist;
|
||||
expect(cusEnt.balance).to.equal(
|
||||
leftoverBalance + overrideQuantity * billingUnits,
|
||||
);
|
||||
expect(cusEnt.interval).to.equal(prodEnt.interval);
|
||||
});
|
||||
});
|
||||
|
||||
describe(chalk.yellowBright("Testing o1 message top up"), () => {
|
||||
const customerId = "alex-o1-top-up-customer";
|
||||
const o1TopUpQuantity = Math.floor(Math.random() * 15);
|
||||
const billingUnits = alexProducts.o1TopUps.prices[0].config.billing_units;
|
||||
const proAllowance = alexProducts.pro.entitlements.o1Message.allowance!;
|
||||
|
||||
before("initializing customer", async function () {
|
||||
await initCustomerV2({
|
||||
autumn,
|
||||
customerId,
|
||||
org: this.org,
|
||||
env: this.env,
|
||||
db: this.db,
|
||||
attachPm: "success",
|
||||
});
|
||||
// await initCustomer({
|
||||
// customer_data: {
|
||||
// id: customerId,
|
||||
// name: "Alex O1 Top Up Customer",
|
||||
// email: "alex-o1-top-up-customer@test.com",
|
||||
// },
|
||||
// db: this.db,
|
||||
// org: this.org,
|
||||
// env: this.env,
|
||||
// attachPm: true,
|
||||
// });
|
||||
});
|
||||
|
||||
it("should attach pro product", async () => {
|
||||
await AutumnCli.attach({
|
||||
customerId,
|
||||
productId: alexProducts.pro.id,
|
||||
});
|
||||
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
compareMainProduct({
|
||||
sent: alexProducts.pro,
|
||||
cusRes,
|
||||
status: CusProductStatus.Trialing,
|
||||
});
|
||||
});
|
||||
|
||||
it("should buy o1 messages and have correct balance", async () => {
|
||||
const res = await AutumnCli.attach({
|
||||
customerId,
|
||||
productId: alexProducts.o1TopUps.id,
|
||||
forceCheckout: true,
|
||||
});
|
||||
|
||||
await completeCheckoutForm(res.checkout_url, o1TopUpQuantity);
|
||||
await timeout(14000);
|
||||
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
// Get product
|
||||
const product = cusRes.add_ons.find(
|
||||
(p: any) => p.id === alexProducts.o1TopUps.id,
|
||||
);
|
||||
|
||||
expect(product).to.exist;
|
||||
expect(product.status).to.equal(CusProductStatus.Active);
|
||||
|
||||
// Check quantity is correct
|
||||
await checkFeatureHasCorrectBalance({
|
||||
customerId,
|
||||
feature: alexFeatures.o1Message,
|
||||
entitlement: alexProducts.o1TopUps.entitlements.o1Message,
|
||||
expectedBalance: o1TopUpQuantity * billingUnits + proAllowance,
|
||||
});
|
||||
});
|
||||
|
||||
it("should send events and check balances", async () => {
|
||||
const allowance = o1TopUpQuantity * billingUnits + proAllowance;
|
||||
|
||||
await runEventsAndCheckBalances({
|
||||
customerId,
|
||||
entitlements: [
|
||||
{
|
||||
interval: EntInterval.Lifetime,
|
||||
feature_id: alexFeatures.o1Message.id,
|
||||
allowance,
|
||||
allowance_type: AllowanceType.Fixed,
|
||||
} as Entitlement, // manual entitlement because in advance pricing
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,210 +0,0 @@
|
||||
import { ApiVersion, CusProductStatus, type Customer } from "@autumn/shared";
|
||||
import { expect } from "chai";
|
||||
import chalk from "chalk";
|
||||
import { addDays, addHours } from "date-fns";
|
||||
import type Stripe from "stripe";
|
||||
import { AutumnCli } from "@tests/cli/AutumnCli.js";
|
||||
import { compareMainProduct } from "@tests/utils/compare.js";
|
||||
import { hoursToFinalizeInvoice } from "@tests/utils/constants.js";
|
||||
import { timeout } from "@tests/utils/genUtils.js";
|
||||
import { advanceTestClock } from "@tests/utils/stripeUtils.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { createStripeCli } from "@/external/connect/createStripeCli.js";
|
||||
import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js";
|
||||
import { initCustomerV2 } from "@/utils/scriptUtils/initCustomer.js";
|
||||
import { alexProducts } from "./init.js";
|
||||
|
||||
// CANCEL AT
|
||||
const getProductFromCusRes = ({
|
||||
cusRes,
|
||||
productId,
|
||||
}: {
|
||||
cusRes: any;
|
||||
productId: string;
|
||||
}) => {
|
||||
return cusRes.products.find((p: any) => p.id === productId);
|
||||
};
|
||||
|
||||
describe(chalk.yellowBright("05_cancel"), () => {
|
||||
const autumn = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
describe("Testing cancel_at_period_end and cancel now", () => {
|
||||
let stripeCli: Stripe;
|
||||
const customerId = "alex-cancel-customer";
|
||||
const autumn: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
|
||||
before("initializing customer", async function () {
|
||||
stripeCli = createStripeCli({
|
||||
org: this.org,
|
||||
env: this.env,
|
||||
});
|
||||
|
||||
await initCustomerV2({
|
||||
autumn,
|
||||
customerId,
|
||||
org: this.org,
|
||||
env: this.env,
|
||||
db: this.db,
|
||||
attachPm: "success",
|
||||
});
|
||||
});
|
||||
|
||||
it("should attach pro product ", async () => {
|
||||
const result = await AutumnCli.attach({
|
||||
customerId,
|
||||
productId: alexProducts.pro.id,
|
||||
});
|
||||
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
compareMainProduct({
|
||||
sent: alexProducts.pro,
|
||||
cusRes,
|
||||
status: CusProductStatus.Trialing,
|
||||
});
|
||||
});
|
||||
|
||||
it("should cancel at period end", async () => {
|
||||
// 1. Get pro product
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
const proProduct = getProductFromCusRes({
|
||||
cusRes,
|
||||
productId: alexProducts.pro.id,
|
||||
});
|
||||
|
||||
for (const subId of proProduct.subscription_ids) {
|
||||
await stripeCli.subscriptions.update(subId, {
|
||||
cancel_at_period_end: true,
|
||||
});
|
||||
}
|
||||
|
||||
await timeout(5000);
|
||||
|
||||
const newCusRes = await AutumnCli.getCustomer(customerId);
|
||||
const newProProduct = getProductFromCusRes({
|
||||
cusRes: newCusRes,
|
||||
productId: alexProducts.pro.id,
|
||||
});
|
||||
|
||||
expect(newProProduct).to.exist;
|
||||
expect(newProProduct.canceled_at).to.not.equal(null);
|
||||
expect(newProProduct.status).to.equal(CusProductStatus.Trialing);
|
||||
});
|
||||
|
||||
it("should cancel now", async () => {
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
const proProduct = getProductFromCusRes({
|
||||
cusRes,
|
||||
productId: alexProducts.pro.id,
|
||||
});
|
||||
|
||||
for (const subId of proProduct.subscription_ids) {
|
||||
await stripeCli.subscriptions.cancel(subId);
|
||||
}
|
||||
|
||||
await timeout(5000);
|
||||
|
||||
const newCusRes = await AutumnCli.getCustomer(customerId);
|
||||
const newProProduct = getProductFromCusRes({
|
||||
cusRes: newCusRes,
|
||||
productId: alexProducts.pro.id,
|
||||
});
|
||||
|
||||
expect(newProProduct).to.not.exist;
|
||||
|
||||
const freeProduct = getProductFromCusRes({
|
||||
cusRes: newCusRes,
|
||||
productId: alexProducts.free.id,
|
||||
});
|
||||
|
||||
expect(freeProduct).to.exist;
|
||||
expect(freeProduct.status).to.equal(CusProductStatus.Active);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Testing past due", () => {
|
||||
const customerId = "alex-past-due-customer";
|
||||
let customer: Customer;
|
||||
let testClockId: string;
|
||||
let stripeCli: Stripe;
|
||||
before("initializing customer", async function () {
|
||||
stripeCli = createStripeCli({
|
||||
org: this.org,
|
||||
env: this.env,
|
||||
});
|
||||
|
||||
const res = await initCustomerV2({
|
||||
autumn,
|
||||
customerId,
|
||||
org: this.org,
|
||||
env: this.env,
|
||||
db: this.db,
|
||||
attachPm: "success",
|
||||
});
|
||||
|
||||
testClockId = res.testClockId;
|
||||
customer = res.customer;
|
||||
});
|
||||
|
||||
it("should attach pro product ", async () => {
|
||||
await AutumnCli.attach({
|
||||
customerId,
|
||||
productId: alexProducts.pro.id,
|
||||
});
|
||||
await timeout(5000);
|
||||
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
compareMainProduct({
|
||||
sent: alexProducts.pro,
|
||||
cusRes,
|
||||
status: CusProductStatus.Trialing,
|
||||
});
|
||||
});
|
||||
|
||||
it("should attach failed card", async () => {
|
||||
await attachFailedPaymentMethod({ stripeCli, customer });
|
||||
});
|
||||
|
||||
it("should advance clock to next cycle", async () => {
|
||||
await advanceTestClock({
|
||||
stripeCli,
|
||||
testClockId,
|
||||
advanceTo: addHours(
|
||||
addDays(new Date(), 7),
|
||||
hoursToFinalizeInvoice,
|
||||
).getTime(),
|
||||
});
|
||||
|
||||
await timeout(5000);
|
||||
});
|
||||
|
||||
// // TODO: Edit so that it doesn't auto cancel for unit test org
|
||||
// it("should have expired / past_dued pro product", async function () {
|
||||
// const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
|
||||
// let org = this.org;
|
||||
|
||||
// console.log("Cancel on past due:", org.config.cancel_on_past_due);
|
||||
// if (org.config.cancel_on_past_due) {
|
||||
// const proProduct = getProductFromCusRes({
|
||||
// cusRes,
|
||||
// productId: alexProducts.pro.id,
|
||||
// });
|
||||
|
||||
// expect(proProduct).to.not.exist;
|
||||
|
||||
// const freeProduct = getProductFromCusRes({
|
||||
// cusRes,
|
||||
// productId: alexProducts.free.id,
|
||||
// });
|
||||
|
||||
// expect(freeProduct).to.exist;
|
||||
// expect(freeProduct.status).to.equal(CusProductStatus.Active);
|
||||
// } else {
|
||||
// compareMainProduct({
|
||||
// sent: alexProducts.pro,
|
||||
// cusRes,
|
||||
// status: CusProductStatus.PastDue,
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
});
|
||||
});
|
||||
@@ -1,232 +0,0 @@
|
||||
import { ApiVersion, CusProductStatus } from "@autumn/shared";
|
||||
import { expect } from "chai";
|
||||
import chalk from "chalk";
|
||||
import { addDays, addHours } from "date-fns";
|
||||
import type Stripe from "stripe";
|
||||
import { AutumnCli } from "@tests/cli/AutumnCli.js";
|
||||
import { compareMainProduct } from "@tests/utils/compare.js";
|
||||
import { timeout } from "@tests/utils/genUtils.js";
|
||||
import { advanceTestClock } from "@tests/utils/stripeUtils.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { createStripeCli } from "@/external/connect/createStripeCli.js";
|
||||
import { initCustomerV2 } from "@/utils/scriptUtils/initCustomer.js";
|
||||
import { alexProducts } from "./init.js";
|
||||
|
||||
describe(
|
||||
chalk.yellowBright(
|
||||
"06_switch: Testing upgrades / downgrades from pro <-> premium",
|
||||
),
|
||||
() => {
|
||||
const customerId = "alex-upgrade-downgrade-customer";
|
||||
let testClockId = "";
|
||||
const fingerprint = "fp1";
|
||||
|
||||
let stripeCli: Stripe;
|
||||
const autumn = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
before("initializing customer", async function () {
|
||||
stripeCli = createStripeCli({
|
||||
org: this.org,
|
||||
env: this.env,
|
||||
});
|
||||
// const { testClockId: newTestClockId } = await initCustomerWithTestClock({
|
||||
// customerId,
|
||||
// db: this.db,
|
||||
// org: this.org,
|
||||
// env: this.env,
|
||||
// fingerprint,
|
||||
// });
|
||||
const { testClockId: newTestClockId } = await initCustomerV2({
|
||||
autumn,
|
||||
customerId,
|
||||
customerData: { fingerprint },
|
||||
org: this.org,
|
||||
env: this.env,
|
||||
db: this.db,
|
||||
attachPm: "success",
|
||||
});
|
||||
testClockId = newTestClockId;
|
||||
});
|
||||
|
||||
describe("First upgrade from pro to premium (trial to trial)", () => {
|
||||
it("should attach pro product", async () => {
|
||||
await timeout(10000);
|
||||
await AutumnCli.attach({
|
||||
customerId,
|
||||
productId: alexProducts.pro.id,
|
||||
});
|
||||
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
compareMainProduct({
|
||||
sent: alexProducts.pro,
|
||||
cusRes,
|
||||
status: CusProductStatus.Trialing,
|
||||
});
|
||||
});
|
||||
|
||||
it("should upgrade to premium", async () => {
|
||||
await AutumnCli.attach({
|
||||
customerId,
|
||||
productId: alexProducts.premium.id,
|
||||
});
|
||||
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
compareMainProduct({
|
||||
sent: alexProducts.premium,
|
||||
cusRes,
|
||||
status: CusProductStatus.Trialing,
|
||||
});
|
||||
|
||||
// Should have 2 invoices
|
||||
expect(cusRes.invoices.length).to.equal(2);
|
||||
expect(cusRes.invoices[0].total).to.equal(0);
|
||||
});
|
||||
});
|
||||
// return;
|
||||
|
||||
describe("Downgrade from premium to pro (trial to paid)", () => {
|
||||
it("should attach pro product (downgrade from premium trial)", async () => {
|
||||
await AutumnCli.attach({
|
||||
customerId,
|
||||
productId: alexProducts.pro.id,
|
||||
});
|
||||
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
compareMainProduct({
|
||||
sent: alexProducts.premium,
|
||||
cusRes,
|
||||
status: CusProductStatus.Trialing,
|
||||
});
|
||||
|
||||
const proProduct = cusRes.products.find(
|
||||
(p: any) => p.id === alexProducts.pro.id,
|
||||
);
|
||||
|
||||
expect(proProduct.status).to.equal(CusProductStatus.Scheduled);
|
||||
expect(proProduct.starts_at).to.exist;
|
||||
expect(proProduct.starts_at).to.be.greaterThan(Date.now());
|
||||
|
||||
await advanceTestClock({
|
||||
stripeCli,
|
||||
testClockId,
|
||||
advanceTo: addHours(new Date(proProduct.starts_at), 10).getTime(),
|
||||
});
|
||||
});
|
||||
|
||||
it("should have pro product and last invoice for $20", async () => {
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
compareMainProduct({
|
||||
sent: alexProducts.pro,
|
||||
cusRes,
|
||||
status: CusProductStatus.Active,
|
||||
});
|
||||
|
||||
const lastInvoice = cusRes.invoices[0];
|
||||
expect(lastInvoice.total).to.equal(20);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Upgrade from pro to premium (paid to paid)", () => {
|
||||
// Now, advance 15 days and upgrade again, and check that the new invoice is for between 25 and 35 (because of the prorated amount)
|
||||
it("should advance clock by 15 days and attach premium", async () => {
|
||||
await advanceTestClock({
|
||||
stripeCli,
|
||||
testClockId,
|
||||
advanceTo: addDays(new Date(), 7 + 15).getTime(),
|
||||
});
|
||||
|
||||
await AutumnCli.attach({
|
||||
customerId,
|
||||
productId: alexProducts.premium.id,
|
||||
});
|
||||
|
||||
await timeout(10000);
|
||||
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
compareMainProduct({
|
||||
sent: alexProducts.premium,
|
||||
cusRes,
|
||||
});
|
||||
});
|
||||
|
||||
it("should have new invoice for roughly 15 days of premium (due to prorated)", async () => {
|
||||
const premiumPrice = alexProducts.premium.prices[0].config.amount;
|
||||
const proPrice = alexProducts.pro.prices[0].config.amount;
|
||||
|
||||
const proratedAmount = ((premiumPrice - proPrice) * 15) / 30;
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
const lastInvoice = cusRes.invoices[0];
|
||||
|
||||
// Expect invoice to be prorated amount +/- 10%
|
||||
expect(lastInvoice.product_ids[0]).to.equal(alexProducts.premium.id);
|
||||
expect(lastInvoice.total).to.be.greaterThan(proratedAmount * 0.9);
|
||||
expect(lastInvoice.total).to.be.lessThan(proratedAmount * 1.1);
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Also, downgrade and cancel pro
|
||||
describe(chalk.yellowBright("06_switch: Testing fingerprint"), () => {
|
||||
const customerId = "alex-fingerprint-test";
|
||||
const fingerprint = "fp1";
|
||||
|
||||
const autumn = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
|
||||
before("initializing customer", async function () {
|
||||
await initCustomerV2({
|
||||
autumn,
|
||||
customerId,
|
||||
customerData: { fingerprint },
|
||||
org: this.org,
|
||||
env: this.env,
|
||||
db: this.db,
|
||||
attachPm: "success",
|
||||
});
|
||||
// await initCustomer({
|
||||
// customer_data: {
|
||||
// id: customerId,
|
||||
// name: "Alex Fingerprint Test",
|
||||
// email: "alex-fingerprint-test@test.com",
|
||||
// fingerprint,
|
||||
// },
|
||||
// attachPm: true,
|
||||
// db: this.db,
|
||||
// org: this.org,
|
||||
// env: this.env,
|
||||
// });
|
||||
});
|
||||
|
||||
it("should attach pro product and have invoice for $20", async () => {
|
||||
await AutumnCli.attach({
|
||||
customerId,
|
||||
productId: alexProducts.pro.id,
|
||||
});
|
||||
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
compareMainProduct({
|
||||
sent: alexProducts.pro,
|
||||
cusRes,
|
||||
status: CusProductStatus.Active,
|
||||
});
|
||||
|
||||
const invoices = cusRes.invoices;
|
||||
expect(invoices[0].total).to.equal(20);
|
||||
});
|
||||
|
||||
it("should attach premium product and have invoice for $30", async () => {
|
||||
await AutumnCli.attach({
|
||||
customerId,
|
||||
productId: alexProducts.premium.id,
|
||||
});
|
||||
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
compareMainProduct({
|
||||
sent: alexProducts.premium,
|
||||
cusRes,
|
||||
status: CusProductStatus.Active,
|
||||
});
|
||||
|
||||
const invoices = cusRes.invoices;
|
||||
expect(invoices[0].total).to.equal(30);
|
||||
});
|
||||
});
|
||||
@@ -1,455 +0,0 @@
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
import {
|
||||
AggregateType,
|
||||
AllowanceType,
|
||||
AppEnv,
|
||||
BillingInterval,
|
||||
EntInterval,
|
||||
FeatureType,
|
||||
} from "@autumn/shared";
|
||||
import { initDrizzle } from "@/db/initDrizzle.js";
|
||||
import { FeatureService } from "@/internal/features/FeatureService.js";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
import {
|
||||
initEntitlement,
|
||||
initFeature,
|
||||
initPrice,
|
||||
initProduct,
|
||||
} from "../utils/init.js";
|
||||
|
||||
export const alexFeatures = {
|
||||
chatMessage: initFeature({
|
||||
id: "chatMessage",
|
||||
type: FeatureType.Metered,
|
||||
aggregateType: AggregateType.Count,
|
||||
eventName: "chat_message",
|
||||
}),
|
||||
deepseekMessage: initFeature({
|
||||
id: "deepseekMessage",
|
||||
type: FeatureType.Metered,
|
||||
aggregateType: AggregateType.Count,
|
||||
eventName: "deepseek_message",
|
||||
}),
|
||||
o1Message: initFeature({
|
||||
id: "o1Message",
|
||||
type: FeatureType.Metered,
|
||||
aggregateType: AggregateType.Count,
|
||||
eventName: "o1_message",
|
||||
}),
|
||||
applyCode: initFeature({
|
||||
id: "applyCode",
|
||||
type: FeatureType.Metered,
|
||||
aggregateType: AggregateType.Count,
|
||||
eventName: "apply",
|
||||
}),
|
||||
gitCommit: initFeature({
|
||||
id: "gitCommit",
|
||||
type: FeatureType.Metered,
|
||||
aggregateType: AggregateType.Count,
|
||||
eventName: "git_commit",
|
||||
}),
|
||||
tabToComplete: initFeature({
|
||||
id: "tabToComplete",
|
||||
type: FeatureType.Metered,
|
||||
aggregateType: AggregateType.Count,
|
||||
eventName: "suggestion",
|
||||
}),
|
||||
topUpMessage: initFeature({
|
||||
id: "topUpMessage",
|
||||
type: FeatureType.Metered,
|
||||
aggregateType: AggregateType.Count,
|
||||
eventName: "top_up_message",
|
||||
}),
|
||||
voiceInput: initFeature({
|
||||
id: "voiceInput",
|
||||
type: FeatureType.Metered,
|
||||
aggregateType: AggregateType.Count,
|
||||
eventName: "voice_input",
|
||||
}),
|
||||
figmaIntegration: initFeature({
|
||||
id: "figmaIntegration",
|
||||
type: FeatureType.Boolean,
|
||||
eventName: "figma_integration",
|
||||
}),
|
||||
githubIssuesIntegration: initFeature({
|
||||
id: "githubIssuesIntegration",
|
||||
type: FeatureType.Boolean,
|
||||
eventName: "github_issues_integration",
|
||||
}),
|
||||
linearIntegration: initFeature({
|
||||
id: "linearIntegration",
|
||||
type: FeatureType.Boolean,
|
||||
eventName: "linear_integration",
|
||||
}),
|
||||
canAddSeats: initFeature({
|
||||
id: "canAddSeats",
|
||||
type: FeatureType.Boolean,
|
||||
eventName: "can_add_seats",
|
||||
}),
|
||||
seats: initFeature({
|
||||
id: "seats",
|
||||
type: FeatureType.Metered,
|
||||
aggregateType: AggregateType.Count,
|
||||
eventName: "seats",
|
||||
}),
|
||||
};
|
||||
|
||||
export const alexProducts = {
|
||||
free: initProduct({
|
||||
id: "free",
|
||||
isDefault: true,
|
||||
entitlements: {
|
||||
chatMessage: initEntitlement({
|
||||
feature: alexFeatures.chatMessage,
|
||||
allowance: 50,
|
||||
interval: EntInterval.Month,
|
||||
}),
|
||||
applyCode: initEntitlement({
|
||||
feature: alexFeatures.applyCode,
|
||||
allowance: 5,
|
||||
interval: EntInterval.Month,
|
||||
}),
|
||||
tabToComplete: initEntitlement({
|
||||
feature: alexFeatures.tabToComplete,
|
||||
allowance: 50,
|
||||
interval: EntInterval.Month,
|
||||
}),
|
||||
voiceInput: initEntitlement({
|
||||
feature: alexFeatures.voiceInput,
|
||||
allowance: 5,
|
||||
interval: EntInterval.Month,
|
||||
}),
|
||||
gitCommit: initEntitlement({
|
||||
feature: alexFeatures.gitCommit,
|
||||
allowance: 5,
|
||||
interval: EntInterval.Month,
|
||||
}),
|
||||
topUpMessage: initEntitlement({
|
||||
feature: alexFeatures.topUpMessage,
|
||||
allowance: 0,
|
||||
interval: EntInterval.Lifetime,
|
||||
}),
|
||||
o1Message: initEntitlement({
|
||||
feature: alexFeatures.o1Message,
|
||||
allowance: 5,
|
||||
interval: EntInterval.Lifetime,
|
||||
}),
|
||||
deepseekMessage: initEntitlement({
|
||||
feature: alexFeatures.deepseekMessage,
|
||||
allowance: 50,
|
||||
interval: EntInterval.Month,
|
||||
}),
|
||||
},
|
||||
prices: [],
|
||||
freeTrial: null,
|
||||
}),
|
||||
|
||||
pro: initProduct({
|
||||
id: "pro",
|
||||
isDefault: false,
|
||||
entitlements: {
|
||||
chatMessage: initEntitlement({
|
||||
feature: alexFeatures.chatMessage,
|
||||
allowance: 500,
|
||||
interval: EntInterval.Month,
|
||||
}),
|
||||
deepseekMessage: initEntitlement({
|
||||
feature: alexFeatures.deepseekMessage,
|
||||
allowanceType: AllowanceType.Unlimited,
|
||||
}),
|
||||
applyCode: initEntitlement({
|
||||
feature: alexFeatures.applyCode,
|
||||
allowanceType: AllowanceType.Unlimited,
|
||||
}),
|
||||
gitCommit: initEntitlement({
|
||||
feature: alexFeatures.gitCommit,
|
||||
allowanceType: AllowanceType.Unlimited,
|
||||
}),
|
||||
voiceInput: initEntitlement({
|
||||
feature: alexFeatures.voiceInput,
|
||||
allowanceType: AllowanceType.Unlimited,
|
||||
}),
|
||||
tabToComplete: initEntitlement({
|
||||
feature: alexFeatures.tabToComplete,
|
||||
allowanceType: AllowanceType.Unlimited,
|
||||
}),
|
||||
o1Message: initEntitlement({
|
||||
feature: alexFeatures.o1Message,
|
||||
allowance: 5,
|
||||
interval: EntInterval.Lifetime,
|
||||
}),
|
||||
topUpMessage: initEntitlement({
|
||||
feature: alexFeatures.topUpMessage,
|
||||
allowance: 0,
|
||||
interval: EntInterval.Lifetime,
|
||||
}),
|
||||
},
|
||||
freeTrial: null,
|
||||
prices: [
|
||||
initPrice({
|
||||
amount: 20.0, // $20.00
|
||||
billingInterval: BillingInterval.Month,
|
||||
type: "monthly",
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
||||
topUpMessages: initProduct({
|
||||
id: "topUpMessages",
|
||||
isDefault: false,
|
||||
isAddOn: true,
|
||||
entitlements: {
|
||||
topUpMessage: initEntitlement({
|
||||
feature: alexFeatures.topUpMessage,
|
||||
allowance: 0,
|
||||
interval: EntInterval.Lifetime,
|
||||
}),
|
||||
},
|
||||
prices: [
|
||||
initPrice({
|
||||
type: "in_advance",
|
||||
amount: 9,
|
||||
billingUnits: 250,
|
||||
billingInterval: BillingInterval.OneOff,
|
||||
feature: alexFeatures.topUpMessage,
|
||||
}),
|
||||
],
|
||||
freeTrial: null,
|
||||
}),
|
||||
|
||||
o1TopUps: initProduct({
|
||||
id: "o1TopUps",
|
||||
isDefault: false,
|
||||
isAddOn: true,
|
||||
entitlements: {
|
||||
o1Message: initEntitlement({
|
||||
feature: alexFeatures.o1Message,
|
||||
allowance: 0,
|
||||
interval: EntInterval.Lifetime,
|
||||
}),
|
||||
},
|
||||
prices: [
|
||||
initPrice({
|
||||
type: "in_advance",
|
||||
amount: 9,
|
||||
billingUnits: 25,
|
||||
billingInterval: BillingInterval.OneOff,
|
||||
feature: alexFeatures.o1Message,
|
||||
}),
|
||||
],
|
||||
freeTrial: null,
|
||||
}),
|
||||
|
||||
premium: initProduct({
|
||||
id: "premium",
|
||||
isDefault: false,
|
||||
entitlements: {
|
||||
chatMessage: initEntitlement({
|
||||
feature: alexFeatures.chatMessage,
|
||||
allowance: 1000,
|
||||
interval: EntInterval.Month,
|
||||
}),
|
||||
deepseekMessage: initEntitlement({
|
||||
feature: alexFeatures.deepseekMessage,
|
||||
allowanceType: AllowanceType.Unlimited,
|
||||
}),
|
||||
o1Message: initEntitlement({
|
||||
feature: alexFeatures.o1Message,
|
||||
allowance: 5,
|
||||
interval: EntInterval.Month,
|
||||
}),
|
||||
applyCode: initEntitlement({
|
||||
feature: alexFeatures.applyCode,
|
||||
allowanceType: AllowanceType.Unlimited,
|
||||
}),
|
||||
gitCommit: initEntitlement({
|
||||
feature: alexFeatures.gitCommit,
|
||||
allowanceType: AllowanceType.Unlimited,
|
||||
}),
|
||||
voiceInput: initEntitlement({
|
||||
feature: alexFeatures.voiceInput,
|
||||
allowanceType: AllowanceType.Unlimited,
|
||||
}),
|
||||
tabToComplete: initEntitlement({
|
||||
feature: alexFeatures.tabToComplete,
|
||||
allowanceType: AllowanceType.Unlimited,
|
||||
}),
|
||||
topUpMessage: initEntitlement({
|
||||
feature: alexFeatures.topUpMessage,
|
||||
allowance: 0,
|
||||
interval: EntInterval.Lifetime,
|
||||
}),
|
||||
figmaIntegration: initEntitlement({
|
||||
feature: alexFeatures.figmaIntegration,
|
||||
allowanceType: AllowanceType.Unlimited,
|
||||
}),
|
||||
githubIssuesIntegration: initEntitlement({
|
||||
feature: alexFeatures.githubIssuesIntegration,
|
||||
allowanceType: AllowanceType.Unlimited,
|
||||
}),
|
||||
linearIntegration: initEntitlement({
|
||||
feature: alexFeatures.linearIntegration,
|
||||
allowanceType: AllowanceType.Unlimited,
|
||||
}),
|
||||
},
|
||||
prices: [
|
||||
initPrice({
|
||||
type: "monthly",
|
||||
amount: 50,
|
||||
}),
|
||||
],
|
||||
freeTrial: null,
|
||||
}),
|
||||
|
||||
proTeam: initProduct({
|
||||
id: "proTeam",
|
||||
isDefault: false,
|
||||
isAddOn: true,
|
||||
entitlements: {
|
||||
topUpMessage: initEntitlement({
|
||||
feature: alexFeatures.topUpMessage,
|
||||
allowance: 0,
|
||||
interval: EntInterval.Lifetime,
|
||||
}),
|
||||
tabToComplete: initEntitlement({
|
||||
feature: alexFeatures.tabToComplete,
|
||||
allowanceType: AllowanceType.Unlimited,
|
||||
}),
|
||||
o1Message: initEntitlement({
|
||||
feature: alexFeatures.o1Message,
|
||||
allowance: 5,
|
||||
interval: EntInterval.Lifetime,
|
||||
}),
|
||||
chatMessage: initEntitlement({
|
||||
feature: alexFeatures.chatMessage,
|
||||
allowance: 500,
|
||||
interval: EntInterval.Month,
|
||||
}),
|
||||
deepseekMessage: initEntitlement({
|
||||
feature: alexFeatures.deepseekMessage,
|
||||
allowanceType: AllowanceType.Unlimited,
|
||||
}),
|
||||
applyCode: initEntitlement({
|
||||
feature: alexFeatures.applyCode,
|
||||
allowanceType: AllowanceType.Unlimited,
|
||||
}),
|
||||
gitCommit: initEntitlement({
|
||||
feature: alexFeatures.gitCommit,
|
||||
allowanceType: AllowanceType.Unlimited,
|
||||
}),
|
||||
voiceInput: initEntitlement({
|
||||
feature: alexFeatures.voiceInput,
|
||||
allowanceType: AllowanceType.Unlimited,
|
||||
}),
|
||||
},
|
||||
prices: [],
|
||||
freeTrial: null,
|
||||
}),
|
||||
teamManager: initProduct({
|
||||
id: "teamManager",
|
||||
isDefault: false,
|
||||
isAddOn: true,
|
||||
entitlements: {
|
||||
topUpMessage: initEntitlement({
|
||||
feature: alexFeatures.topUpMessage,
|
||||
allowance: 0,
|
||||
interval: EntInterval.Lifetime,
|
||||
}),
|
||||
chatMessage: initEntitlement({
|
||||
feature: alexFeatures.chatMessage,
|
||||
allowance: 1000,
|
||||
interval: EntInterval.Month,
|
||||
}),
|
||||
deepseekMessage: initEntitlement({
|
||||
feature: alexFeatures.deepseekMessage,
|
||||
allowanceType: AllowanceType.Unlimited,
|
||||
}),
|
||||
o1Message: initEntitlement({
|
||||
feature: alexFeatures.o1Message,
|
||||
allowance: 5,
|
||||
interval: EntInterval.Month,
|
||||
}),
|
||||
|
||||
applyCode: initEntitlement({
|
||||
feature: alexFeatures.applyCode,
|
||||
allowanceType: AllowanceType.Unlimited,
|
||||
}),
|
||||
gitCommit: initEntitlement({
|
||||
feature: alexFeatures.gitCommit,
|
||||
allowanceType: AllowanceType.Unlimited,
|
||||
}),
|
||||
voiceInput: initEntitlement({
|
||||
feature: alexFeatures.voiceInput,
|
||||
allowanceType: AllowanceType.Unlimited,
|
||||
}),
|
||||
|
||||
tabToComplete: initEntitlement({
|
||||
feature: alexFeatures.tabToComplete,
|
||||
allowanceType: AllowanceType.Unlimited,
|
||||
}),
|
||||
canAddSeats: initEntitlement({
|
||||
feature: alexFeatures.canAddSeats,
|
||||
}),
|
||||
seats: initEntitlement({
|
||||
feature: alexFeatures.seats,
|
||||
allowance: 0,
|
||||
interval: EntInterval.Lifetime,
|
||||
}),
|
||||
},
|
||||
prices: [
|
||||
initPrice({
|
||||
type: "in_advance",
|
||||
amount: 50,
|
||||
billingUnits: 1,
|
||||
billingInterval: BillingInterval.Month,
|
||||
feature: alexFeatures.seats,
|
||||
}),
|
||||
],
|
||||
freeTrial: null,
|
||||
}),
|
||||
};
|
||||
|
||||
const orgSlug = process.env.TESTS_ORG!;
|
||||
before(async function () {
|
||||
try {
|
||||
this.env = AppEnv.Sandbox;
|
||||
const { db, client } = initDrizzle();
|
||||
this.db = db;
|
||||
this.client = client;
|
||||
this.org = await OrgService.getBySlug({
|
||||
db: this.db,
|
||||
slug: orgSlug,
|
||||
});
|
||||
|
||||
const dbFeatures = await FeatureService.list({
|
||||
db: this.db,
|
||||
orgId: this.org.id,
|
||||
env: this.env,
|
||||
});
|
||||
|
||||
for (const featureId in alexFeatures) {
|
||||
const feature = alexFeatures[featureId as keyof typeof alexFeatures];
|
||||
const dbFeature = dbFeatures.find((f: any) => f.id === feature.id);
|
||||
if (!dbFeature) {
|
||||
continue;
|
||||
throw new Error(`Feature ${feature.id} not found`);
|
||||
}
|
||||
alexFeatures[featureId as keyof typeof alexFeatures].internal_id =
|
||||
dbFeature.internal_id;
|
||||
if (feature.type === FeatureType.Metered) {
|
||||
// Ignore this for now
|
||||
alexFeatures[featureId as keyof typeof alexFeatures].eventName =
|
||||
dbFeature.event_names?.[0] || dbFeature.id;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
await this.client.end();
|
||||
});
|
||||
@@ -1,241 +0,0 @@
|
||||
import {
|
||||
Entitlement,
|
||||
FeatureType,
|
||||
AllowanceType,
|
||||
Feature,
|
||||
} from "@autumn/shared";
|
||||
import { expect } from "chai";
|
||||
import { timeout } from "@tests/utils/genUtils.js";
|
||||
import { AutumnCli } from "@tests/cli/AutumnCli.js";
|
||||
import { alexFeatures } from "./init.js";
|
||||
|
||||
const checkEntitledOnFeatures = async (customerId: string, product: any) => {
|
||||
const entitlements: Entitlement[] = Object.values(product.entitlements);
|
||||
|
||||
for (const entitlement of entitlements) {
|
||||
const { allowed, balanceObj }: any = await AutumnCli.entitled(
|
||||
customerId,
|
||||
entitlement.feature_id!,
|
||||
true,
|
||||
);
|
||||
|
||||
// Type assertion to tell TypeScript that feature_id is a key of alexFeatures
|
||||
let feature =
|
||||
alexFeatures[entitlement.feature_id! as keyof typeof alexFeatures];
|
||||
|
||||
try {
|
||||
if (feature.type === FeatureType.Boolean) {
|
||||
expect(allowed).to.equal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (entitlement.allowance === 0) {
|
||||
expect(allowed).to.equal(false);
|
||||
expect(balanceObj?.balance).to.equal(0);
|
||||
return;
|
||||
} else if (entitlement.allowance_type === AllowanceType.Unlimited) {
|
||||
expect(allowed).to.equal(true);
|
||||
expect(balanceObj?.balance).to.equal(null);
|
||||
expect(balanceObj?.unlimited).to.equal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
expect(allowed).to.equal(true);
|
||||
expect(balanceObj?.balance).to.equal(entitlement.allowance);
|
||||
} catch (error) {
|
||||
console.log("Checking entitlement: ", entitlement);
|
||||
console.log("Expected balance: ", entitlement.allowance);
|
||||
console.log("Entitled response: ", { allowed, balanceObj });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getFeatureFromEntitlement = (entitlement: Entitlement) => {
|
||||
return alexFeatures[
|
||||
entitlement.feature_id! as keyof typeof alexFeatures
|
||||
] as Feature & { eventName: string };
|
||||
};
|
||||
|
||||
export const checkFeatureHasCorrectBalance = async ({
|
||||
customerId,
|
||||
feature,
|
||||
entitlement,
|
||||
expectedBalance,
|
||||
}: {
|
||||
customerId: string;
|
||||
feature: Feature;
|
||||
entitlement: Entitlement;
|
||||
expectedBalance: number;
|
||||
}) => {
|
||||
const [entitledRes, cusRes] = await Promise.all([
|
||||
AutumnCli.entitled(customerId, feature.id, true),
|
||||
AutumnCli.getCustomer(customerId),
|
||||
]);
|
||||
|
||||
if (feature.type === FeatureType.Boolean) {
|
||||
console.log(" - Checking boolean feature: ", feature.id);
|
||||
const { allowed, balanceObj }: any = entitledRes;
|
||||
expect(allowed).to.equal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
` - Checking entitlement ${feature.id} has ${
|
||||
entitlement.allowance_type == AllowanceType.Unlimited
|
||||
? "unlimited balance"
|
||||
: `balance of ${expectedBalance}`
|
||||
}`,
|
||||
);
|
||||
|
||||
// Get ent from cusRes
|
||||
const { entitlements: cusEnts }: any = cusRes;
|
||||
const { allowed, balanceObj }: any = entitledRes;
|
||||
|
||||
const cusEnt = cusEnts.find(
|
||||
(e: any) =>
|
||||
e.feature_id === feature.id && e.interval == entitlement.interval,
|
||||
);
|
||||
|
||||
try {
|
||||
expect(cusEnt).to.exist;
|
||||
} catch (error) {
|
||||
console.log(
|
||||
`Expected cus ent ${feature.id}, interval ${entitlement.interval} to exist`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (entitlement.allowance_type === AllowanceType.Unlimited) {
|
||||
// Cus ent
|
||||
expect(cusEnt.balance).to.equal(null);
|
||||
expect(cusEnt.used).to.equal(null);
|
||||
expect(cusEnt.unlimited).to.equal(true);
|
||||
|
||||
// Entitled res
|
||||
expect(allowed).to.equal(true);
|
||||
expect(balanceObj?.balance).to.equal(null);
|
||||
expect(balanceObj?.unlimited).to.equal(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (expectedBalance === 0) {
|
||||
expect(allowed).to.equal(false);
|
||||
expect(balanceObj?.balance).to.equal(0);
|
||||
expect(cusEnt.balance).to.equal(0);
|
||||
return;
|
||||
}
|
||||
|
||||
expect(balanceObj?.balance).to.equal(expectedBalance);
|
||||
expect(cusEnt.balance).to.equal(expectedBalance);
|
||||
};
|
||||
|
||||
export const runEventsAndCheckBalances = async ({
|
||||
customerId,
|
||||
entitlements,
|
||||
}: {
|
||||
customerId: string;
|
||||
entitlements: Entitlement[];
|
||||
}) => {
|
||||
for (const entitlement of entitlements) {
|
||||
if (
|
||||
entitlement.allowance === 0 &&
|
||||
entitlement.allowance_type !== AllowanceType.Unlimited
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let feature = getFeatureFromEntitlement(entitlement);
|
||||
|
||||
if (
|
||||
entitlement.allowance_type === AllowanceType.Unlimited ||
|
||||
feature.type === FeatureType.Boolean
|
||||
) {
|
||||
await checkFeatureHasCorrectBalance({
|
||||
customerId,
|
||||
feature,
|
||||
entitlement,
|
||||
expectedBalance: entitlement.allowance!,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// 1. Check that feature has full balance
|
||||
await checkFeatureHasCorrectBalance({
|
||||
customerId,
|
||||
feature,
|
||||
entitlement,
|
||||
expectedBalance: entitlement.allowance!,
|
||||
});
|
||||
|
||||
// console.log(" - Running events & entitled check for:", feature.id);
|
||||
|
||||
let firstHalf = Math.min(Math.floor(entitlement.allowance! / 2), 50);
|
||||
let secondHalf = entitlement.allowance! - firstHalf;
|
||||
|
||||
// 1. Send first half
|
||||
let batchUpdate = [];
|
||||
for (let i = 0; i < firstHalf; i++) {
|
||||
batchUpdate.push(
|
||||
AutumnCli.sendEvent({
|
||||
customerId,
|
||||
eventName: feature.eventName!,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
let timeoutMilli = Math.max(Math.floor(firstHalf / 2.5), 2) * 2500;
|
||||
await timeout(timeoutMilli);
|
||||
|
||||
await Promise.all(batchUpdate);
|
||||
|
||||
await checkFeatureHasCorrectBalance({
|
||||
customerId,
|
||||
feature,
|
||||
entitlement,
|
||||
expectedBalance: entitlement.allowance! - firstHalf,
|
||||
});
|
||||
|
||||
if (secondHalf > 50) {
|
||||
continue;
|
||||
// TODO: Make balance 0 and check that it's blocked...
|
||||
await AutumnCli.sendEvent({
|
||||
customerId,
|
||||
eventName: feature.eventName!,
|
||||
properties: {
|
||||
value: secondHalf,
|
||||
},
|
||||
});
|
||||
|
||||
await timeout(1000);
|
||||
|
||||
await checkFeatureHasCorrectBalance({
|
||||
customerId,
|
||||
feature,
|
||||
entitlement,
|
||||
expectedBalance: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < secondHalf; i++) {
|
||||
batchUpdate.push(
|
||||
AutumnCli.sendEvent({
|
||||
customerId,
|
||||
eventName: feature.eventName!,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await timeout(timeoutMilli);
|
||||
|
||||
await Promise.all(batchUpdate);
|
||||
|
||||
await checkFeatureHasCorrectBalance({
|
||||
customerId,
|
||||
feature,
|
||||
entitlement,
|
||||
expectedBalance: 0,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,341 +0,0 @@
|
||||
import { compareMainProduct } from "../utils/compare.js";
|
||||
import { initCustomer } from "../utils/init.js";
|
||||
import { features, products } from "../global.js";
|
||||
import { AutumnCli } from "../cli/AutumnCli.js";
|
||||
import { assert, expect } from "chai";
|
||||
import { timeout } from "../utils/genUtils.js";
|
||||
import { completeCheckoutForm } from "../utils/stripeUtils.js";
|
||||
import { getAxiosInstance } from "../utils/setup.js";
|
||||
import chalk from "chalk";
|
||||
|
||||
const oneTimeQuantity = 2;
|
||||
const oneTimePurchaseCount = 2;
|
||||
const oneTimeOverrideQuantity = 4;
|
||||
const monthlyQuantity = 2;
|
||||
|
||||
// UNCOMMENT FROM HERE
|
||||
describe(`${chalk.yellowBright(
|
||||
"01_product: Testing attach -- free, pro & one-time / monthly add on",
|
||||
)}`, () => {
|
||||
let customerId = "attach1";
|
||||
before(async function () {
|
||||
await initCustomer({
|
||||
customerId,
|
||||
db: this.db,
|
||||
org: this.org,
|
||||
env: this.env,
|
||||
});
|
||||
});
|
||||
|
||||
describe("Create customer -- check free is active", () => {
|
||||
it("GET /customers/:id -- checking default product & entitlements", async function () {
|
||||
const axiosInstance = getAxiosInstance();
|
||||
const { data } = await axiosInstance.get(`/v1/customers/${customerId}`);
|
||||
|
||||
compareMainProduct({
|
||||
sent: products.free,
|
||||
cusRes: data,
|
||||
});
|
||||
});
|
||||
|
||||
it("GET /entitled -- metered1", async function () {
|
||||
// Checking metered1 entitlement
|
||||
const expectedEntitlement = products.free.entitlements.metered1;
|
||||
|
||||
const entitled: any = await AutumnCli.entitled(
|
||||
customerId,
|
||||
features.metered1.id,
|
||||
);
|
||||
|
||||
const metered1Balance = entitled!.balances.find(
|
||||
(balance: any) => balance.feature_id === features.metered1.id,
|
||||
);
|
||||
|
||||
try {
|
||||
expect(entitled!.allowed).to.be.true;
|
||||
expect(metered1Balance).to.exist;
|
||||
expect(metered1Balance!.balance).to.equal(
|
||||
expectedEntitlement.allowance,
|
||||
);
|
||||
expect(metered1Balance!.unlimited).to.not.exist;
|
||||
} catch (error) {
|
||||
console.group();
|
||||
console.group();
|
||||
console.log("Looking for: ", expectedEntitlement);
|
||||
console.log("Received (entitled res): ", entitled);
|
||||
console.groupEnd();
|
||||
console.groupEnd();
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
it("GET /entitled -- boolean1", async function () {
|
||||
const entitled = await AutumnCli.entitled(
|
||||
customerId,
|
||||
features.boolean1.id,
|
||||
);
|
||||
|
||||
expect(entitled!.allowed).to.be.false;
|
||||
});
|
||||
});
|
||||
|
||||
describe("Attach pro -- check products & entitlements", () => {
|
||||
it("POST /attach -- attaching pro (force checkout)", async function () {
|
||||
const res = await AutumnCli.attach({
|
||||
customerId: customerId,
|
||||
productId: products.pro.id,
|
||||
});
|
||||
|
||||
assert.exists(res.checkout_url);
|
||||
|
||||
await completeCheckoutForm(res.checkout_url);
|
||||
await timeout(10000); // for webhook to be processed
|
||||
console.log(` ${chalk.greenBright("Attached pro")}`);
|
||||
});
|
||||
|
||||
it("GET /customers/:id -- checking product & entitlements (pro)", async function () {
|
||||
const res = await AutumnCli.getCustomer(customerId);
|
||||
// console.log("Res: ", res);
|
||||
compareMainProduct({
|
||||
sent: products.pro,
|
||||
cusRes: res,
|
||||
});
|
||||
assert.isTrue(res.invoices.length > 0);
|
||||
});
|
||||
|
||||
// return;
|
||||
|
||||
it("GET /entitled -- checking entitlements for metered1 && boolean1", async function () {
|
||||
const proEntitlements = products.pro.entitlements;
|
||||
|
||||
for (const entitlement of Object.values(proEntitlements)) {
|
||||
const allowance = entitlement.allowance;
|
||||
|
||||
const res: any = await AutumnCli.entitled(
|
||||
customerId,
|
||||
entitlement.feature_id!,
|
||||
);
|
||||
|
||||
const entBalance = res!.balances.find(
|
||||
(b: any) => b.feature_id === entitlement.feature_id,
|
||||
);
|
||||
|
||||
try {
|
||||
expect(res!.allowed).to.be.true;
|
||||
expect(entBalance).to.exist;
|
||||
if (entitlement.allowance) {
|
||||
expect(entBalance!.balance).to.equal(allowance);
|
||||
}
|
||||
// console.log(` - ${entitlement.feature_id} -- Passed`);
|
||||
} catch (error) {
|
||||
console.group();
|
||||
console.group();
|
||||
console.log("Looking for: ", entitlement);
|
||||
console.log("Received: ", res);
|
||||
console.groupEnd();
|
||||
console.groupEnd();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const oneTimeBillingUnits =
|
||||
products.oneTimeAddOnMetered1.prices[0].config.billing_units;
|
||||
const monthlyBillingUnits =
|
||||
products.monthlyAddOnMetered1.prices[0].config.billing_units;
|
||||
|
||||
describe("One time add on (force checkout)", () => {
|
||||
// PURCHASE ONE TIME ADD ON
|
||||
|
||||
it("POST /attach -- attaching one time add on (force checkout) [no quantity passed in]", async function () {
|
||||
try {
|
||||
for (let i = 0; i < oneTimePurchaseCount; i++) {
|
||||
const res = await AutumnCli.attach({
|
||||
customerId: customerId,
|
||||
productId: products.oneTimeAddOnMetered1.id,
|
||||
forceCheckout: true,
|
||||
});
|
||||
|
||||
await completeCheckoutForm(res.checkout_url, oneTimeOverrideQuantity);
|
||||
await timeout(10000); // for webhook to be processed
|
||||
console.log(` ${chalk.greenBright("Attached one time add on")}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.group();
|
||||
console.group();
|
||||
console.log("Failed to attach one time add on");
|
||||
console.log("Error data:", error);
|
||||
console.groupEnd();
|
||||
console.groupEnd();
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: Attach one time add on again (with quantity?)
|
||||
|
||||
it("GET /customers/:id -- checking product & entitlements (one time add on)", async function () {
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
|
||||
// 1. Metered1 balance should be pro + one time add on
|
||||
|
||||
// Fetch balance
|
||||
const addOnBalance = cusRes.entitlements.find(
|
||||
(e: any) =>
|
||||
e.feature_id === features.metered1.id &&
|
||||
e.interval ==
|
||||
products.oneTimeAddOnMetered1.entitlements.metered1.interval,
|
||||
);
|
||||
|
||||
const expectedAmt =
|
||||
(oneTimeOverrideQuantity || oneTimeQuantity) *
|
||||
oneTimeBillingUnits *
|
||||
oneTimePurchaseCount;
|
||||
|
||||
try {
|
||||
assert.equal(addOnBalance!.balance, expectedAmt);
|
||||
assert.equal(cusRes.add_ons.length, 1);
|
||||
assert.equal(cusRes.add_ons[0].id, products.oneTimeAddOnMetered1.id);
|
||||
assert.equal(cusRes.invoices.length, 1 + oneTimePurchaseCount);
|
||||
} catch (error) {
|
||||
console.group();
|
||||
console.group();
|
||||
console.log("GET customer, balances failed");
|
||||
console.log(
|
||||
"Add on entitlement:",
|
||||
products.oneTimeAddOnMetered1.entitlements.metered1,
|
||||
);
|
||||
console.log("Customer entitlements:", cusRes.entitlements);
|
||||
|
||||
console.groupEnd();
|
||||
console.groupEnd();
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
it("GET /entitled -- checking entitled for metered1", async function () {
|
||||
const res: any = await AutumnCli.entitled(
|
||||
customerId,
|
||||
features.metered1.id,
|
||||
);
|
||||
|
||||
expect(res!.allowed).to.be.true;
|
||||
|
||||
// pro metered1
|
||||
const proMetered1Amt = products.pro.entitlements.metered1.allowance;
|
||||
|
||||
const addOnBalance = res!.balances.find(
|
||||
(b: any) => b.feature_id === features.metered1.id,
|
||||
);
|
||||
|
||||
expect(res!.allowed).to.be.true;
|
||||
expect(addOnBalance!.balance).to.equal(
|
||||
proMetered1Amt! +
|
||||
(oneTimeOverrideQuantity || oneTimeQuantity) *
|
||||
oneTimeBillingUnits *
|
||||
oneTimePurchaseCount,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// PURCHASE MONTHLY ADD ON
|
||||
describe("Monthly add on", () => {
|
||||
it("POST /attach -- attaching monthly add on", async function () {
|
||||
await AutumnCli.attach({
|
||||
customerId: customerId,
|
||||
productId: products.monthlyAddOnMetered1.id,
|
||||
forceCheckout: false,
|
||||
options: [
|
||||
{
|
||||
feature_id: features.metered1.id,
|
||||
quantity: monthlyQuantity * monthlyBillingUnits,
|
||||
},
|
||||
],
|
||||
});
|
||||
await timeout(10000);
|
||||
|
||||
console.log(` ${chalk.greenBright("Attached monthly top up")}`);
|
||||
});
|
||||
|
||||
it("GET /customers/:id -- checking product & entitlements (monthly add on)", async function () {
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
|
||||
// 1. Metered1 balance should be pro + one time add on
|
||||
const proMetered1 = products.pro.entitlements.metered1.allowance;
|
||||
|
||||
// Fetch balance
|
||||
const monthlyMetered1Balance = cusRes.entitlements.find(
|
||||
(e: any) =>
|
||||
e.feature_id === features.metered1.id &&
|
||||
e.interval ==
|
||||
products.monthlyAddOnMetered1.entitlements.metered1.interval,
|
||||
);
|
||||
|
||||
try {
|
||||
assert.equal(
|
||||
monthlyMetered1Balance!.balance,
|
||||
proMetered1! + monthlyQuantity * monthlyBillingUnits,
|
||||
);
|
||||
|
||||
assert.equal(cusRes.add_ons.length, 2);
|
||||
const monthlyAddOnId = cusRes.add_ons.find(
|
||||
(a: any) => a.id === products.monthlyAddOnMetered1.id,
|
||||
);
|
||||
|
||||
assert.exists(monthlyAddOnId);
|
||||
expect(cusRes.invoices.length).to.equal(2 + oneTimePurchaseCount);
|
||||
} catch (error) {
|
||||
console.group();
|
||||
console.group();
|
||||
console.log("GET customer, balances failed");
|
||||
console.log(
|
||||
"Add on entitlement:",
|
||||
products.monthlyAddOnMetered1.entitlements.metered1,
|
||||
);
|
||||
console.log("Customer entitlements:", cusRes.entitlements);
|
||||
|
||||
console.groupEnd();
|
||||
console.groupEnd();
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
it("GET /entitled -- checking entitlements (monthly add on)", async function () {
|
||||
const res: any = await AutumnCli.entitled(
|
||||
customerId,
|
||||
features.metered1.id,
|
||||
);
|
||||
|
||||
const metered1Balance = res!.balances.find(
|
||||
(b: any) => b.feature_id === features.metered1.id,
|
||||
);
|
||||
|
||||
const proMetered1Amt = products.pro.entitlements.metered1.allowance;
|
||||
const monthlyAddOnMetered1Amt = monthlyQuantity * monthlyBillingUnits;
|
||||
|
||||
const oneTimeAddOnMetered1Amt =
|
||||
(oneTimeOverrideQuantity || oneTimeQuantity) *
|
||||
oneTimeBillingUnits *
|
||||
oneTimePurchaseCount;
|
||||
|
||||
try {
|
||||
expect(metered1Balance!.balance).to.equal(
|
||||
proMetered1Amt! + monthlyAddOnMetered1Amt + oneTimeAddOnMetered1Amt,
|
||||
);
|
||||
} catch (error) {
|
||||
console.group();
|
||||
console.group();
|
||||
console.log("GET entitled, balances failed");
|
||||
|
||||
console.log("/entitled response:", res);
|
||||
console.log("Pro metered1 amt:", proMetered1Amt);
|
||||
console.log("Monthly add on metered1 amt:", monthlyAddOnMetered1Amt);
|
||||
console.log("One time add on metered1 amt:", oneTimeAddOnMetered1Amt);
|
||||
|
||||
console.groupEnd();
|
||||
console.groupEnd();
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,245 +0,0 @@
|
||||
import chalk from "chalk";
|
||||
import { initCustomer } from "../utils/init.js";
|
||||
import { features, products } from "../global.js";
|
||||
import { AutumnCli } from "../cli/AutumnCli.js";
|
||||
import { timeout } from "../utils/genUtils.js";
|
||||
import { expect } from "chai";
|
||||
|
||||
const checkEntitledOnProduct = async ({
|
||||
customerId,
|
||||
product,
|
||||
totalAllowance,
|
||||
finish = false,
|
||||
usageBased = false,
|
||||
}: {
|
||||
customerId: string;
|
||||
product: any;
|
||||
totalAllowance?: number;
|
||||
finish?: boolean;
|
||||
usageBased?: boolean;
|
||||
}) => {
|
||||
// 1. Send events
|
||||
const allowance = totalAllowance || product.entitlements.metered1.allowance;
|
||||
// const randomNum = Math.floor(Math.random() * (allowance - 1));
|
||||
const randomNum = 3;
|
||||
|
||||
const batchUpdates = [];
|
||||
for (let i = 0; i < randomNum; i++) {
|
||||
batchUpdates.push(
|
||||
AutumnCli.sendEvent({
|
||||
customerId: customerId,
|
||||
eventName: features.metered1.eventName,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(batchUpdates);
|
||||
await timeout(8000);
|
||||
let used = randomNum;
|
||||
|
||||
// 2. Check entitled
|
||||
const { allowed, balanceObj }: any = await AutumnCli.entitled(
|
||||
customerId,
|
||||
features.metered1.id,
|
||||
true,
|
||||
);
|
||||
|
||||
try {
|
||||
expect(allowed).to.be.true;
|
||||
expect(balanceObj!.balance).to.equal(allowance - randomNum);
|
||||
|
||||
if (!finish) {
|
||||
return used;
|
||||
}
|
||||
} catch (error) {
|
||||
console.group();
|
||||
console.group();
|
||||
console.log("Allowance: ", allowance, "Random num: ", randomNum);
|
||||
console.log("Expected balance to be: ", allowance - randomNum);
|
||||
console.log("Entitled res: ", { allowed, balanceObj });
|
||||
console.groupEnd();
|
||||
console.groupEnd();
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Finish up
|
||||
const batchUpdates2 = [];
|
||||
for (let i = 0; i < allowance - randomNum; i++) {
|
||||
batchUpdates2.push(
|
||||
AutumnCli.sendEvent({
|
||||
customerId: customerId,
|
||||
eventName: features.metered1.eventName,
|
||||
}),
|
||||
);
|
||||
}
|
||||
await Promise.all(batchUpdates2);
|
||||
await timeout(8000);
|
||||
used += allowance - randomNum;
|
||||
|
||||
// 3. Check entitled again
|
||||
const { allowed: allowed2, balanceObj: balanceObj2 }: any =
|
||||
await AutumnCli.entitled(customerId, features.metered1.id, true);
|
||||
try {
|
||||
if (usageBased) {
|
||||
expect(allowed2).to.be.true;
|
||||
} else {
|
||||
expect(allowed2).to.be.false;
|
||||
}
|
||||
expect(balanceObj2!.balance).to.equal(0);
|
||||
return used;
|
||||
} catch (error) {
|
||||
console.group();
|
||||
console.group();
|
||||
console.log("Expected balance to be: ", 0);
|
||||
console.log("Entitled res: ", { allowed2, balanceObj2 });
|
||||
console.groupEnd();
|
||||
console.groupEnd();
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Add test case for unlimited feature
|
||||
|
||||
describe(`${chalk.yellowBright(
|
||||
"04_entitled: Testing /events and /entitled, for pro, one time top up",
|
||||
)}`, () => {
|
||||
const customerId = "entitledCustomer";
|
||||
|
||||
let curAllowance = 0;
|
||||
const oneTimeBillingUnits =
|
||||
products.oneTimeAddOnMetered1.prices[0].config.billing_units!;
|
||||
let oneTimeQuantity = 2 * oneTimeBillingUnits;
|
||||
|
||||
before(async function () {
|
||||
await initCustomer({
|
||||
customer_data: {
|
||||
id: customerId,
|
||||
name: customerId,
|
||||
email: `test@test.com`,
|
||||
},
|
||||
db: this.db,
|
||||
org: this.org,
|
||||
env: this.env,
|
||||
attachPm: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should have correct entitlements (free)", async function () {
|
||||
await checkEntitledOnProduct({
|
||||
customerId: customerId,
|
||||
product: products.free,
|
||||
finish: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should attach pro", async function () {
|
||||
await AutumnCli.attach({
|
||||
customerId: customerId,
|
||||
productId: products.pro.id,
|
||||
});
|
||||
});
|
||||
|
||||
it("should have correct entitlements (pro)", async function () {
|
||||
const used = await checkEntitledOnProduct({
|
||||
customerId: customerId,
|
||||
product: products.pro,
|
||||
finish: false,
|
||||
});
|
||||
|
||||
curAllowance = products.pro.entitlements.metered1.allowance! - used;
|
||||
});
|
||||
|
||||
it("should attach one time top up", async function () {
|
||||
await AutumnCli.attach({
|
||||
customerId: customerId,
|
||||
productId: products.oneTimeAddOnMetered1.id,
|
||||
options: [
|
||||
{
|
||||
feature_id: features.metered1.id,
|
||||
quantity: oneTimeQuantity,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should have correct entitlements (one time top up)", async function () {
|
||||
// const oneTimeAmt = oneTimeBillingUnits * oneTimeQuantity;
|
||||
|
||||
await checkEntitledOnProduct({
|
||||
customerId: customerId,
|
||||
product: products.oneTimeAddOnMetered1,
|
||||
finish: true,
|
||||
totalAllowance: curAllowance + oneTimeQuantity,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(`${chalk.yellowBright(
|
||||
"04_entitled: Testing /entitled & /events, for pro with overage",
|
||||
)}`, () => {
|
||||
const customerId = "entitledCustomerUsageBased";
|
||||
|
||||
before(async function () {
|
||||
await initCustomer({
|
||||
customer_data: {
|
||||
id: customerId,
|
||||
name: customerId,
|
||||
email: "test@test.com",
|
||||
},
|
||||
db: this.db,
|
||||
org: this.org,
|
||||
env: this.env,
|
||||
attachPm: true,
|
||||
});
|
||||
});
|
||||
|
||||
// PRO WITH OVERAGE
|
||||
it("should attach pro (with overage)", async function () {
|
||||
await AutumnCli.attach({
|
||||
customerId: customerId,
|
||||
productId: products.proWithOverage.id,
|
||||
});
|
||||
});
|
||||
|
||||
it("should have correct entitlements (pro with overage)", async function () {
|
||||
await checkEntitledOnProduct({
|
||||
customerId: customerId,
|
||||
product: products.proWithOverage,
|
||||
finish: true,
|
||||
totalAllowance: products.proWithOverage.entitlements.metered1.allowance!,
|
||||
usageBased: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should have correct usage-based balance (balance < 0)", async function () {
|
||||
const { allowed, balanceObj }: any = await AutumnCli.entitled(
|
||||
customerId,
|
||||
features.metered1.id,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(allowed).to.be.true;
|
||||
expect(balanceObj!.balance).to.equal(0);
|
||||
|
||||
// Sent 5 events
|
||||
const batchUpdates = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
batchUpdates.push(
|
||||
AutumnCli.sendEvent({
|
||||
customerId: customerId,
|
||||
eventName: features.metered1.eventName,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(batchUpdates);
|
||||
await timeout(5000);
|
||||
|
||||
const { allowed: allowed2, balanceObj: balanceObj2 }: any =
|
||||
await AutumnCli.entitled(customerId, features.metered1.id, true);
|
||||
|
||||
expect(allowed2).to.be.true;
|
||||
expect(balanceObj2!.balance).to.equal(-5);
|
||||
expect(balanceObj2!.usage_allowed).to.be.true;
|
||||
});
|
||||
});
|
||||
@@ -1,164 +0,0 @@
|
||||
import { assert } from "chai";
|
||||
import { features, products } from "../global.js";
|
||||
import { initCustomer } from "../utils/init.js";
|
||||
import { getPublicAxiosInstance } from "../utils/setup.js";
|
||||
import { completeCheckoutForm } from "../utils/stripeUtils.js";
|
||||
import { timeout } from "../utils/genUtils.js";
|
||||
import { ErrCode } from "@autumn/shared";
|
||||
import { compareMainProduct } from "../utils/compare.js";
|
||||
import { AutumnCli } from "../cli/AutumnCli.js";
|
||||
import chalk from "chalk";
|
||||
|
||||
describe(`${chalk.yellowBright("08_pkey: Testing publishable key")}`, () => {
|
||||
// 1. Initialize customer with card
|
||||
let customerId = "pkeyTestCustomer";
|
||||
const bearerPublicAxios = getPublicAxiosInstance({
|
||||
withBearer: true,
|
||||
});
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000);
|
||||
await initCustomer({
|
||||
customer_data: {
|
||||
id: customerId,
|
||||
name: customerId,
|
||||
email: "test@test.com",
|
||||
fingerprint: "fp1",
|
||||
},
|
||||
db: this.db,
|
||||
org: this.org,
|
||||
env: this.env,
|
||||
attachPm: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return a 401 if the pkey is invalid", async function () {
|
||||
this.timeout(30000);
|
||||
const axiosInstance = getPublicAxiosInstance({
|
||||
withBearer: true,
|
||||
pkey: "am_pk_test_invalid",
|
||||
});
|
||||
|
||||
try {
|
||||
const { data } = await axiosInstance.post("/v1/attach", {
|
||||
customer_id: customerId,
|
||||
product_id: products.pro.id,
|
||||
});
|
||||
|
||||
throw new Error("Should not be able to attach");
|
||||
} catch (error: any) {
|
||||
assert.equal(error.response.status, 401);
|
||||
}
|
||||
});
|
||||
|
||||
it("should return checkout URL for both bearer key", async function () {
|
||||
this.timeout(30000);
|
||||
const axiosInstanceBearer = getPublicAxiosInstance({
|
||||
withBearer: true,
|
||||
});
|
||||
|
||||
// 1. Should be able to upgrade to pro
|
||||
const { data } = await axiosInstanceBearer.post("/v1/attach", {
|
||||
customer_id: customerId,
|
||||
product_id: products.pro.id,
|
||||
});
|
||||
|
||||
assert.exists(data.checkout_url);
|
||||
|
||||
await completeCheckoutForm(data.checkout_url);
|
||||
|
||||
await timeout(5000);
|
||||
});
|
||||
|
||||
it("should have customer with product", async function () {
|
||||
this.timeout(30000);
|
||||
const res = await AutumnCli.getCustomer(customerId);
|
||||
compareMainProduct({
|
||||
sent: products.pro,
|
||||
cusRes: res,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return error if try to upgrade or downgrade without pkey", async function () {
|
||||
this.timeout(30000);
|
||||
const axiosInstance = getPublicAxiosInstance({
|
||||
withBearer: true,
|
||||
});
|
||||
|
||||
try {
|
||||
await axiosInstance.post("/v1/attach", {
|
||||
customer_id: customerId,
|
||||
product_id: products.premium.id,
|
||||
});
|
||||
|
||||
throw new Error("Should not be able to attach");
|
||||
} catch (error: any) {
|
||||
assert.equal(error.response.status, 400);
|
||||
assert.equal(error.response.data.code, ErrCode.InvalidRequest);
|
||||
}
|
||||
});
|
||||
|
||||
it("should return error if try to downgrade to free", async function () {
|
||||
try {
|
||||
await bearerPublicAxios.post("/v1/attach", {
|
||||
customer_id: customerId,
|
||||
product_id: products.free.id,
|
||||
});
|
||||
|
||||
throw new Error("Should not be able to attach");
|
||||
} catch (error: any) {
|
||||
assert.equal(error.response.status, 400);
|
||||
assert.equal(error.response.data.code, ErrCode.InvalidRequest);
|
||||
}
|
||||
});
|
||||
|
||||
// Next, check entitled for pro
|
||||
it("should return correct metered1 amount for pro", async function () {
|
||||
const { data } = await bearerPublicAxios.post("/v1/entitled", {
|
||||
customer_id: customerId,
|
||||
feature_id: features.metered1.id,
|
||||
});
|
||||
|
||||
assert.equal(data.allowed, true);
|
||||
const metered1Balance = data.balances.find(
|
||||
(b: any) => b.feature_id === features.metered1.id,
|
||||
);
|
||||
assert.equal(
|
||||
metered1Balance.balance,
|
||||
products.pro.entitlements.metered1.allowance,
|
||||
);
|
||||
});
|
||||
|
||||
it("should return same balance for entitled with bearer and x-publishable-key", async function () {
|
||||
const { data } = await bearerPublicAxios.post("/v1/entitled", {
|
||||
customer_id: customerId,
|
||||
feature_id: features.metered1.id,
|
||||
});
|
||||
|
||||
assert.equal(data.allowed, true);
|
||||
const metered1Balance = data.balances.find(
|
||||
(b: any) => b.feature_id === features.metered1.id,
|
||||
);
|
||||
assert.equal(
|
||||
metered1Balance.balance,
|
||||
products.pro.entitlements.metered1.allowance,
|
||||
);
|
||||
});
|
||||
|
||||
it("should return error when try to send event", async function () {
|
||||
try {
|
||||
await bearerPublicAxios.post("/v1/events", {
|
||||
customer_id: customerId,
|
||||
event_name: features.metered1.id,
|
||||
properties: {
|
||||
value: 10,
|
||||
},
|
||||
});
|
||||
|
||||
throw new Error("Should not be able to send event");
|
||||
} catch (error: any) {
|
||||
assert.equal(error.response.status, 401);
|
||||
assert.equal(error.response.data.code, ErrCode.EndpointNotPublic);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,298 +0,0 @@
|
||||
// import chalk from "chalk";
|
||||
// import Stripe from "stripe";
|
||||
|
||||
// import { expect } from "chai";
|
||||
// import { AutumnCli } from "@tests/cli/AutumnCli.js";
|
||||
// import { advanceProducts, features } from "@tests/global.js";
|
||||
// import { compareMainProduct } from "@tests/utils/compare.js";
|
||||
// import { advanceTestClock } from "@tests/utils/stripeUtils.js";
|
||||
// import { timeout } from "@tests/utils/genUtils.js";
|
||||
|
||||
// import { addDays, addMonths, format } from "date-fns";
|
||||
|
||||
// import { initCustomerWithTestClock } from "@tests/utils/testInitUtils.js";
|
||||
// import { checkSubscriptionContainsProducts } from "@tests/utils/scheduleCheckUtils.js";
|
||||
// import { CacheManager } from "@/external/caching/CacheManager.js";
|
||||
// import { CacheType } from "@/external/caching/cacheActions.js";
|
||||
// import { hashApiKey } from "@/internal/dev/api-keys/apiKeyUtils.js";
|
||||
|
||||
// const advanceAPThroughBalances = async ({
|
||||
// stripeSub,
|
||||
// stripeCli,
|
||||
// testClockId,
|
||||
// customerId,
|
||||
// billingUnits,
|
||||
// startingFrom,
|
||||
// startingBalance,
|
||||
// }: {
|
||||
// stripeSub: Stripe.Subscription;
|
||||
// stripeCli: Stripe;
|
||||
// testClockId: string;
|
||||
// customerId: string;
|
||||
// billingUnits: number;
|
||||
// startingFrom?: number;
|
||||
// startingBalance?: number;
|
||||
// }) => {
|
||||
// // 1. Get total period
|
||||
// let totalPeriod =
|
||||
// (stripeSub.current_period_end - stripeSub.current_period_start) * 1000;
|
||||
|
||||
// // 2. Get allowance
|
||||
// let allowance =
|
||||
// advanceProducts.proratedArrearSeats.entitlements.seats.allowance!;
|
||||
|
||||
// // 3. Get starting balance
|
||||
// let balance = startingBalance || allowance;
|
||||
|
||||
// // 4. Get price per seat
|
||||
// let pricePerSeat =
|
||||
// advanceProducts.proratedArrearSeats.prices[1].config.usage_tiers[0].amount;
|
||||
|
||||
// let skipDays = 2;
|
||||
// // 5. Get accrued price
|
||||
// let accruedPrice = 0;
|
||||
// if (startingBalance) {
|
||||
// let proratedPrice =
|
||||
// (-startingBalance *
|
||||
// pricePerSeat *
|
||||
// (startingFrom! - stripeSub.current_period_start * 1000)) /
|
||||
// totalPeriod;
|
||||
|
||||
// let previouslyPaid = pricePerSeat * -startingBalance;
|
||||
// let priceToPay = proratedPrice - previouslyPaid;
|
||||
|
||||
// accruedPrice = priceToPay;
|
||||
// // accruedPrice = Math.max(accruedPrice, 0);
|
||||
// console.log(" 🔍 Starting balance: ", startingBalance);
|
||||
// console.log(" 🔍 Starting price: ", accruedPrice);
|
||||
// }
|
||||
|
||||
// let curTime = startingFrom || stripeSub.current_period_start * 1000;
|
||||
// let numberOfEvents = 2;
|
||||
|
||||
// console.group();
|
||||
// console.group();
|
||||
// for (let i = 0; i < numberOfEvents; i++) {
|
||||
// let sign = balance > 0 ? 1 : Math.random() > 0.7 ? 1 : -1;
|
||||
|
||||
// let currentUsage = allowance - balance;
|
||||
|
||||
// let nextBoundary =
|
||||
// Math.ceil((currentUsage + 1) / billingUnits) * billingUnits;
|
||||
|
||||
// let prevBoundary = nextBoundary - billingUnits;
|
||||
|
||||
// let valueNeeded = 0;
|
||||
// if (sign > 0) {
|
||||
// // Add random amount to push above next boundary
|
||||
// const valueToGetToNegative = balance + 1;
|
||||
// valueNeeded =
|
||||
// Math.floor(Math.random() * 10) + (nextBoundary - currentUsage + 1);
|
||||
|
||||
// valueNeeded = Math.max(valueNeeded, valueToGetToNegative);
|
||||
// } else {
|
||||
// valueNeeded = -(
|
||||
// Math.floor(Math.random() * 10) +
|
||||
// (currentUsage - prevBoundary + 1)
|
||||
// );
|
||||
// }
|
||||
|
||||
// let newBalance = balance - valueNeeded;
|
||||
|
||||
// await AutumnCli.updateBalances({
|
||||
// customerId,
|
||||
// balances: [
|
||||
// {
|
||||
// feature_id: features.seats.id,
|
||||
// balance: newBalance,
|
||||
// },
|
||||
// ],
|
||||
// });
|
||||
|
||||
// await timeout(2000);
|
||||
|
||||
// let prevBalance = balance;
|
||||
// balance = newBalance;
|
||||
|
||||
// // Calculate prorated price only when crossing boundary
|
||||
// let newPrice = Math.max(0, -balance * pricePerSeat);
|
||||
// let prevCurTime = curTime;
|
||||
// curTime = addDays(curTime, 2).getTime();
|
||||
|
||||
// if (i === numberOfEvents - 1) {
|
||||
// curTime = stripeSub.current_period_end * 1000;
|
||||
// }
|
||||
|
||||
// let proratedPrice = (newPrice * (curTime - prevCurTime)) / totalPeriod;
|
||||
// accruedPrice += Number(proratedPrice.toFixed(2));
|
||||
|
||||
// console.log(`Event ${i + 1}:`);
|
||||
// console.log(` - Value added: ${valueNeeded}`);
|
||||
// console.log(` - Balance: ${prevBalance} -> ${balance}`);
|
||||
// console.log(` - Prorated price: ${proratedPrice.toFixed(2)}`);
|
||||
// console.log(` - Accrued price: ${accruedPrice.toFixed(2)}`);
|
||||
|
||||
// await advanceTestClock({
|
||||
// stripeCli,
|
||||
// testClockId,
|
||||
// numberOfDays: 2,
|
||||
// startingFrom: new Date(prevCurTime),
|
||||
// });
|
||||
// }
|
||||
|
||||
// console.groupEnd();
|
||||
// console.groupEnd();
|
||||
|
||||
// // Advance test clock to end of period
|
||||
|
||||
// let advanceTo = addDays(addMonths(new Date(), 1), 2);
|
||||
// let advanceToStart = startingFrom ? new Date(startingFrom) : new Date();
|
||||
// await advanceTestClock({
|
||||
// stripeCli,
|
||||
// testClockId,
|
||||
// numberOfDays: 2,
|
||||
// startingFrom: addMonths(advanceToStart, 1),
|
||||
// });
|
||||
|
||||
// // Check invoice amount
|
||||
// const res = await AutumnCli.getCustomer(customerId);
|
||||
// let invoice = res.invoices[0];
|
||||
|
||||
// let basePrice = advanceProducts.proratedArrearSeats.prices[0].config.amount;
|
||||
// let nextMonthUsagePrice = Math.max(-balance * pricePerSeat, 0);
|
||||
// console.log(" 🔍 Next month usage price: ", nextMonthUsagePrice);
|
||||
|
||||
// let expectedTotal = invoice.total;
|
||||
// expect(Number(expectedTotal.toFixed(2))).to.lessThan(invoice.total + 0.1);
|
||||
// expect(Number(expectedTotal.toFixed(2))).to.greaterThan(invoice.total - 0.1);
|
||||
|
||||
// return {
|
||||
// balance,
|
||||
// advancedTo: advanceTo.getTime(),
|
||||
// };
|
||||
// };
|
||||
|
||||
// describe(`${chalk.yellowBright(
|
||||
// "arrear_prorated2: testing update in arrear prorated through /balances",
|
||||
// )}`, () => {
|
||||
// const customerId = "arrear-prorated-balances";
|
||||
|
||||
// let testClockId = "";
|
||||
// let stripeCli: Stripe;
|
||||
// let subId = "";
|
||||
// let stripeSub: Stripe.Subscription;
|
||||
// let billingUnits =
|
||||
// advanceProducts.proratedArrearSeats.prices[1].config.billing_units || 1;
|
||||
|
||||
// before(async function () {
|
||||
// const { testClockId: createdTestClockId } = await initCustomerWithTestClock(
|
||||
// {
|
||||
// customerId,
|
||||
// org: this.org,
|
||||
// env: this.env,
|
||||
// db: this.db,
|
||||
// },
|
||||
// );
|
||||
|
||||
// stripeCli = createStripeCli({
|
||||
// org: this.org,
|
||||
// env: this.env,
|
||||
// });
|
||||
|
||||
// testClockId = createdTestClockId;
|
||||
|
||||
// await this.sb
|
||||
// .from("organizations")
|
||||
// .update({
|
||||
// config: {
|
||||
// ...this.org.config,
|
||||
// bill_upgrade_immediately: false,
|
||||
// },
|
||||
// })
|
||||
// .eq("id", this.org.id);
|
||||
|
||||
// await CacheManager.invalidate({
|
||||
// action: CacheType.SecretKey,
|
||||
// value: hashApiKey(process.env.UNIT_TEST_AUTUMN_SECRET_KEY!),
|
||||
// });
|
||||
// await CacheManager.disconnect();
|
||||
// });
|
||||
|
||||
// it("should attach in arrear prorated seats", async () => {
|
||||
// await timeout(5000);
|
||||
// await AutumnCli.attach({
|
||||
// customerId,
|
||||
// productId: advanceProducts.proratedArrearSeats.id,
|
||||
// });
|
||||
// });
|
||||
|
||||
// it("should have correct product", async function () {
|
||||
// const res = await AutumnCli.getCustomer(customerId);
|
||||
// compareMainProduct({
|
||||
// sent: advanceProducts.proratedArrearSeats,
|
||||
// cusRes: res,
|
||||
// });
|
||||
|
||||
// // 2. Get subscription period start and period end
|
||||
// subId = res.products[0].subscription_ids[0];
|
||||
// stripeSub = await stripeCli.subscriptions.retrieve(subId);
|
||||
|
||||
// await checkSubscriptionContainsProducts({
|
||||
// db: this.db,
|
||||
// org: this.org,
|
||||
// env: this.env,
|
||||
// subscriptionId: subId,
|
||||
// productIds: [advanceProducts.proratedArrearSeats.id],
|
||||
// });
|
||||
// });
|
||||
|
||||
// let advancedTo: number;
|
||||
// let balance: number;
|
||||
// it("should run first cycles and have correct invoice / balance", async () => {
|
||||
// // Do it again
|
||||
// let { advancedTo: advancedTo1, balance: balance1 } =
|
||||
// await advanceAPThroughBalances({
|
||||
// stripeSub,
|
||||
// stripeCli,
|
||||
// testClockId,
|
||||
// customerId,
|
||||
// billingUnits,
|
||||
// });
|
||||
|
||||
// advancedTo = advancedTo1;
|
||||
// balance = balance1;
|
||||
// });
|
||||
|
||||
// it("should run second cycle and have correct invoice / balance", async () => {
|
||||
// console.log(` Advanced to ${format(new Date(advancedTo), "yyyy-MM-dd")}`);
|
||||
|
||||
// let newStripeSub = await stripeCli.subscriptions.retrieve(subId);
|
||||
// await advanceAPThroughBalances({
|
||||
// stripeSub: newStripeSub,
|
||||
// stripeCli,
|
||||
// testClockId,
|
||||
// customerId,
|
||||
// billingUnits,
|
||||
// startingFrom: advancedTo,
|
||||
// startingBalance: balance,
|
||||
// });
|
||||
// });
|
||||
|
||||
// after(async function () {
|
||||
// await this.sb
|
||||
// .from("organizations")
|
||||
// .update({
|
||||
// config: {
|
||||
// ...this.org.config,
|
||||
// bill_upgrade_immediately: true,
|
||||
// },
|
||||
// })
|
||||
// .eq("id", this.org.id);
|
||||
// void CacheManager.invalidate({
|
||||
// action: CacheType.SecretKey,
|
||||
// value: hashApiKey(process.env.UNIT_TEST_AUTUMN_SECRET_KEY!),
|
||||
// });
|
||||
// });
|
||||
// // TODO: Test reset at for in arrear prorated with Ent Interval = Lifetime
|
||||
// // TODO: Test in arrear prorated for entitlements with billing units > 1
|
||||
// });
|
||||
@@ -1,276 +0,0 @@
|
||||
import { expect } from "chai";
|
||||
import chalk from "chalk";
|
||||
import { addDays, addMonths, format } from "date-fns";
|
||||
import { Decimal } from "decimal.js";
|
||||
import type Stripe from "stripe";
|
||||
import { AutumnCli } from "@tests/cli/AutumnCli.js";
|
||||
import { advanceProducts, features } from "@tests/global.js";
|
||||
import { compareMainProduct } from "@tests/utils/compare.js";
|
||||
import { timeout } from "@tests/utils/genUtils.js";
|
||||
import { checkSubscriptionContainsProducts } from "@tests/utils/scheduleCheckUtils.js";
|
||||
import { advanceTestClock } from "@tests/utils/stripeUtils.js";
|
||||
import { initCustomerWithTestClock } from "@tests/utils/testInitUtils.js";
|
||||
import { createStripeCli } from "@/external/connect/createStripeCli.js";
|
||||
|
||||
const advanceAPThroughBalances = async ({
|
||||
stripeSub,
|
||||
stripeCli,
|
||||
testClockId,
|
||||
customerId,
|
||||
billingUnits,
|
||||
startingFrom,
|
||||
startingBalance,
|
||||
}: {
|
||||
stripeSub: Stripe.Subscription;
|
||||
stripeCli: Stripe;
|
||||
testClockId: string;
|
||||
customerId: string;
|
||||
billingUnits: number;
|
||||
startingFrom?: number;
|
||||
startingBalance?: number;
|
||||
}) => {
|
||||
// 1. Get total period
|
||||
const totalPeriod =
|
||||
(stripeSub.current_period_end - stripeSub.current_period_start) * 1000;
|
||||
|
||||
// 2. Get allowance
|
||||
const allowance =
|
||||
advanceProducts.proratedArrearSeats.entitlements.seats.allowance!;
|
||||
|
||||
// 3. Get starting balance
|
||||
let balance = startingBalance || allowance;
|
||||
|
||||
// 4. Get price per seat
|
||||
const pricePerSeat =
|
||||
advanceProducts.proratedArrearSeats.prices[1].config.usage_tiers[0].amount;
|
||||
|
||||
const skipDays = 2;
|
||||
// 5. Get accrued price
|
||||
let accruedPrice = 0;
|
||||
if (startingBalance && startingBalance < 0) {
|
||||
const proratedPrice =
|
||||
(-startingBalance *
|
||||
pricePerSeat *
|
||||
(startingFrom! - stripeSub.current_period_start * 1000)) /
|
||||
totalPeriod;
|
||||
|
||||
const previouslyPaid = pricePerSeat * -startingBalance;
|
||||
const priceToPay = proratedPrice - previouslyPaid;
|
||||
|
||||
accruedPrice = priceToPay;
|
||||
// accruedPrice = Math.max(accruedPrice, 0);
|
||||
console.log(" 🔍 Starting balance: ", startingBalance);
|
||||
console.log(" 🔍 Starting price: ", accruedPrice);
|
||||
}
|
||||
|
||||
let curTime = startingFrom || stripeSub.current_period_start * 1000;
|
||||
const numberOfEvents = 2;
|
||||
|
||||
console.group();
|
||||
console.group();
|
||||
for (let i = 0; i < numberOfEvents; i++) {
|
||||
const sign = balance > 0 ? 1 : Math.random() > 0.7 ? 1 : -1;
|
||||
|
||||
const currentUsage = allowance - balance;
|
||||
|
||||
const nextBoundary =
|
||||
Math.ceil((currentUsage + 1) / billingUnits) * billingUnits;
|
||||
|
||||
const prevBoundary = nextBoundary - billingUnits;
|
||||
|
||||
let valueNeeded = 0;
|
||||
if (sign > 0) {
|
||||
// Add random amount to push above next boundary
|
||||
const valueToGetToNegative = balance + 1;
|
||||
valueNeeded =
|
||||
Math.floor(Math.random() * 10) + (nextBoundary - currentUsage + 1);
|
||||
|
||||
valueNeeded = Math.max(valueNeeded, valueToGetToNegative);
|
||||
} else {
|
||||
valueNeeded = -(
|
||||
Math.floor(Math.random() * 10) +
|
||||
(currentUsage - prevBoundary + 1)
|
||||
);
|
||||
}
|
||||
|
||||
const newBalance = balance - valueNeeded;
|
||||
const totalUsage = allowance - newBalance;
|
||||
|
||||
await AutumnCli.usage({
|
||||
customerId,
|
||||
featureId: features.seats.id,
|
||||
value: totalUsage,
|
||||
});
|
||||
|
||||
await timeout(2000);
|
||||
|
||||
const prevBalance = balance;
|
||||
balance = newBalance;
|
||||
|
||||
// Calculate prorated price only when crossing boundary
|
||||
const newPrice = Math.max(0, -balance * pricePerSeat);
|
||||
const prevCurTime = curTime;
|
||||
curTime = addDays(curTime, 2).getTime();
|
||||
|
||||
if (i === numberOfEvents - 1) {
|
||||
curTime = stripeSub.current_period_end * 1000;
|
||||
}
|
||||
|
||||
const proratedPrice = new Decimal(newPrice)
|
||||
.mul(curTime - prevCurTime)
|
||||
.div(totalPeriod);
|
||||
accruedPrice = new Decimal(accruedPrice)
|
||||
.plus(proratedPrice)
|
||||
.toDecimalPlaces(2)
|
||||
.toNumber();
|
||||
|
||||
console.log(`Event ${i + 1}:`);
|
||||
console.log(` - Value added: ${valueNeeded}`);
|
||||
console.log(` - Balance: ${prevBalance} -> ${balance}`);
|
||||
console.log(` - Prorated price: ${proratedPrice.toFixed(2)}`);
|
||||
console.log(` - Accrued price: ${accruedPrice.toFixed(2)}`);
|
||||
|
||||
await advanceTestClock({
|
||||
stripeCli,
|
||||
testClockId,
|
||||
numberOfDays: 2,
|
||||
startingFrom: new Date(prevCurTime),
|
||||
});
|
||||
}
|
||||
|
||||
console.groupEnd();
|
||||
console.groupEnd();
|
||||
|
||||
// Advance test clock to end of period
|
||||
|
||||
const advanceTo = addDays(addMonths(new Date(), 1), 2);
|
||||
const advanceToStart = startingFrom ? new Date(startingFrom) : new Date();
|
||||
|
||||
await advanceTestClock({
|
||||
stripeCli,
|
||||
testClockId,
|
||||
// numberOfHours: hoursToFinalizeInvoice,
|
||||
numberOfDays: 2,
|
||||
startingFrom: addMonths(advanceToStart, 1),
|
||||
});
|
||||
|
||||
// Check invoice amount
|
||||
const res = await AutumnCli.getCustomer(customerId);
|
||||
const invoice = res.invoices[0];
|
||||
|
||||
const basePrice = advanceProducts.proratedArrearSeats.prices[0].config.amount;
|
||||
const nextMonthUsagePrice = Math.max(-balance * pricePerSeat, 0);
|
||||
|
||||
const expectedInvoiceTotal = Number(
|
||||
(accruedPrice + basePrice + nextMonthUsagePrice).toFixed(2),
|
||||
);
|
||||
console.log(
|
||||
`Invoice total = ${accruedPrice} (Accrued) + ${nextMonthUsagePrice} (Next month usage) + ${basePrice} (Base) = ${expectedInvoiceTotal}`,
|
||||
);
|
||||
|
||||
expect(expectedInvoiceTotal).to.lte(
|
||||
new Decimal(invoice.total).plus(0.01).toNumber(),
|
||||
);
|
||||
expect(expectedInvoiceTotal).to.gte(
|
||||
new Decimal(invoice.total).minus(0.01).toNumber(),
|
||||
);
|
||||
|
||||
return {
|
||||
balance,
|
||||
advancedTo: advanceTo.getTime(),
|
||||
};
|
||||
};
|
||||
|
||||
describe(`${chalk.yellowBright(
|
||||
"arrear_prorated3: Testing through /usage",
|
||||
)}`, () => {
|
||||
const customerId = "arrear_prorated3";
|
||||
|
||||
let testClockId = "";
|
||||
let stripeCli: Stripe;
|
||||
let subId = "";
|
||||
let stripeSub: Stripe.Subscription;
|
||||
const billingUnits =
|
||||
advanceProducts.proratedArrearSeats.prices[1].config.billing_units || 1;
|
||||
|
||||
before(async function () {
|
||||
const { testClockId: createdTestClockId } = await initCustomerWithTestClock(
|
||||
{
|
||||
customerId,
|
||||
org: this.org,
|
||||
env: this.env,
|
||||
db: this.db,
|
||||
},
|
||||
);
|
||||
|
||||
stripeCli = createStripeCli({
|
||||
org: this.org,
|
||||
env: this.env,
|
||||
});
|
||||
|
||||
testClockId = createdTestClockId;
|
||||
});
|
||||
|
||||
it("arrear_prorated3: should attach in arrear prorated seats", async () => {
|
||||
await AutumnCli.attach({
|
||||
customerId,
|
||||
productId: advanceProducts.proratedArrearSeats.id,
|
||||
});
|
||||
});
|
||||
|
||||
it("arrear_prorated3: should have correct product", async function () {
|
||||
const res = await AutumnCli.getCustomer(customerId);
|
||||
compareMainProduct({
|
||||
sent: advanceProducts.proratedArrearSeats,
|
||||
cusRes: res,
|
||||
});
|
||||
|
||||
// 2. Get subscription period start and period end
|
||||
subId = res.products[0].subscription_ids[0];
|
||||
stripeSub = await stripeCli.subscriptions.retrieve(subId);
|
||||
|
||||
await checkSubscriptionContainsProducts({
|
||||
db: this.db,
|
||||
org: this.org,
|
||||
env: this.env,
|
||||
subscriptionId: subId,
|
||||
productIds: [advanceProducts.proratedArrearSeats.id],
|
||||
});
|
||||
});
|
||||
|
||||
let advancedTo: number;
|
||||
let balance: number;
|
||||
it("arrear_prorated3: should run first cycles and have correct invoice / balance", async () => {
|
||||
// Do it again
|
||||
const { advancedTo: advancedTo1, balance: balance1 } =
|
||||
await advanceAPThroughBalances({
|
||||
stripeSub,
|
||||
stripeCli,
|
||||
testClockId,
|
||||
customerId,
|
||||
billingUnits,
|
||||
});
|
||||
|
||||
advancedTo = advancedTo1;
|
||||
balance = balance1;
|
||||
});
|
||||
|
||||
it("arrear_prorated3: should run second cycle and have correct invoice / balance", async () => {
|
||||
if (advancedTo) {
|
||||
console.log(
|
||||
` Advanced to ${format(new Date(advancedTo), "yyyy-MM-dd")}`,
|
||||
);
|
||||
}
|
||||
|
||||
const newStripeSub = await stripeCli.subscriptions.retrieve(subId);
|
||||
await advanceAPThroughBalances({
|
||||
stripeSub: newStripeSub,
|
||||
stripeCli,
|
||||
testClockId,
|
||||
customerId,
|
||||
billingUnits,
|
||||
startingFrom: advancedTo,
|
||||
startingBalance: balance,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,84 +0,0 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
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";
|
||||
import {
|
||||
getFixedPriceAmount,
|
||||
getUsagePriceTiers,
|
||||
timeout,
|
||||
} from "@tests/utils/genUtils.js";
|
||||
import { completeCheckoutForm } from "@tests/utils/stripeUtils.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
|
||||
const testCase = "basic10";
|
||||
|
||||
describe(`${chalk.yellowBright("basic10: Multi attach, all one off")}`, () => {
|
||||
const customerId = testCase;
|
||||
const quantity = 1000;
|
||||
const options = [
|
||||
{
|
||||
feature_id: features.metered2.id,
|
||||
quantity,
|
||||
},
|
||||
];
|
||||
|
||||
beforeAll(async () => {
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
withTestClock: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("should attach monthly with one time", async () => {
|
||||
const res = await AutumnCli.attach({
|
||||
customerId,
|
||||
productIds: [
|
||||
oneTimeProducts.oneTimeMetered1.id,
|
||||
oneTimeProducts.oneTimeMetered2.id,
|
||||
],
|
||||
options,
|
||||
});
|
||||
|
||||
await completeCheckoutForm(res.checkout_url);
|
||||
await timeout(20000);
|
||||
});
|
||||
|
||||
test("should have correct main product and entitlements", async () => {
|
||||
const cusRes = await AutumnCli.getCustomer(customerId);
|
||||
|
||||
compareMainProduct({
|
||||
sent: oneTimeProducts.oneTimeMetered1,
|
||||
cusRes,
|
||||
});
|
||||
|
||||
compareMainProduct({
|
||||
sent: oneTimeProducts.oneTimeMetered2,
|
||||
cusRes,
|
||||
optionsList: options,
|
||||
});
|
||||
|
||||
const invoices = cusRes.invoices;
|
||||
const metered1Amount = getFixedPriceAmount(oneTimeProducts.oneTimeMetered1);
|
||||
const metered2Tiers = getUsagePriceTiers({
|
||||
product: oneTimeProducts.oneTimeMetered2,
|
||||
featureId: features.metered2.id,
|
||||
});
|
||||
|
||||
const metered2Amount = metered2Tiers[0].amount;
|
||||
|
||||
const numBillingUnits = new Decimal(options[0].quantity).div(
|
||||
oneTimeProducts.oneTimeMetered2.prices[0].config.billing_units,
|
||||
);
|
||||
|
||||
const expectedTotal = new Decimal(metered2Amount)
|
||||
.mul(numBillingUnits)
|
||||
.add(metered1Amount)
|
||||
.toNumber();
|
||||
|
||||
expect(invoices[0].total).toBe(expectedTotal);
|
||||
});
|
||||
});
|
||||
@@ -1,162 +0,0 @@
|
||||
// import { beforeAll, describe, expect, 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 ctx from "@tests/utils/testInitUtils/createTestContext.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 { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
// import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
// import { getExpectedInvoiceTotal } from "../../utils/expectUtils/expectInvoiceUtils.js";
|
||||
// import { timeout } from "../../utils/genUtils.js";
|
||||
// import { advanceToNextInvoice } from "../../utils/testAttachUtils/testAttachUtils.js";
|
||||
// import { getBasePrice } from "../../utils/testProductUtils/testProductUtils.js";
|
||||
// import { expectSubToBeCorrect } from "../mergeUtils/expectSubCorrect.js";
|
||||
|
||||
// // UNCOMMENT FROM HERE
|
||||
// const premium = constructProduct({
|
||||
// id: "premium",
|
||||
// items: [constructArrearItem({ featureId: TestFeature.Words })],
|
||||
// type: "premium",
|
||||
// });
|
||||
// const pro = constructProduct({
|
||||
// id: "pro",
|
||||
// items: [constructArrearItem({ featureId: TestFeature.Words })],
|
||||
// type: "pro",
|
||||
// });
|
||||
|
||||
// const testCase = "mergedAdd2";
|
||||
// describe(`${chalk.yellowBright(`${testCase}: Testing merged subs, downgrade`)}`, () => {
|
||||
// const customerId = testCase;
|
||||
// const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
|
||||
|
||||
// let stripeCli: Stripe;
|
||||
// let testClockId: string;
|
||||
// let curUnix: number;
|
||||
// let db: DrizzleCli;
|
||||
// let org: Organization;
|
||||
// let env: AppEnv;
|
||||
|
||||
// beforeAll(async () => {
|
||||
// await initProductsV0({
|
||||
// ctx,
|
||||
// products: [premium, pro],
|
||||
// prefix: testCase,
|
||||
// customerId,
|
||||
// });
|
||||
|
||||
// const res = await initCustomerV3({
|
||||
// ctx,
|
||||
// customerId,
|
||||
// customerData: {},
|
||||
// attachPm: "success",
|
||||
// withTestClock: true,
|
||||
// });
|
||||
|
||||
// stripeCli = ctx.stripeCli;
|
||||
// db = ctx.db;
|
||||
// org = ctx.org;
|
||||
// env = ctx.env;
|
||||
// testClockId = res.testClockId!;
|
||||
// });
|
||||
|
||||
// const entities = [
|
||||
// {
|
||||
// id: "1",
|
||||
// name: "Entity 1",
|
||||
// feature_id: TestFeature.Users,
|
||||
// },
|
||||
// {
|
||||
// id: "2",
|
||||
// name: "Entity 2",
|
||||
// feature_id: TestFeature.Users,
|
||||
// },
|
||||
// ];
|
||||
|
||||
// test("should attach premium, product", async () => {
|
||||
// await autumn.entities.create(customerId, entities);
|
||||
|
||||
// await autumn.attach({
|
||||
// customer_id: customerId,
|
||||
// product_id: premium.id,
|
||||
// entity_id: "1",
|
||||
// });
|
||||
|
||||
// await autumn.attach({
|
||||
// customer_id: customerId,
|
||||
// product_id: premium.id,
|
||||
// entity_id: "2",
|
||||
// });
|
||||
// await autumn.attach({
|
||||
// customer_id: customerId,
|
||||
// product_id: pro.id,
|
||||
// entity_id: "2",
|
||||
// });
|
||||
|
||||
// const customer = await autumn.customers.get(customerId);
|
||||
// const invoice = customer.invoices;
|
||||
|
||||
// await expectSubToBeCorrect({
|
||||
// db,
|
||||
// customerId,
|
||||
// org,
|
||||
// env,
|
||||
// });
|
||||
// });
|
||||
|
||||
// test("should track usage and have correct invoice end of month", async () => {
|
||||
// const value1 = 110000;
|
||||
// const value2 = 310000;
|
||||
// const values = [value1, value2];
|
||||
// await autumn.track({
|
||||
// customer_id: customerId,
|
||||
// feature_id: TestFeature.Words,
|
||||
// value: value1,
|
||||
// entity_id: "1",
|
||||
// });
|
||||
|
||||
// await autumn.track({
|
||||
// customer_id: customerId,
|
||||
// feature_id: TestFeature.Words,
|
||||
// value: value2,
|
||||
// entity_id: "2",
|
||||
// });
|
||||
|
||||
// await timeout(3000);
|
||||
|
||||
// await advanceToNextInvoice({
|
||||
// stripeCli,
|
||||
// testClockId,
|
||||
// });
|
||||
|
||||
// let total = 0;
|
||||
// for (let i = 0; i < entities.length; i++) {
|
||||
// const expectedTotal = await getExpectedInvoiceTotal({
|
||||
// customerId,
|
||||
// productId: pro.id,
|
||||
// usage: [{ featureId: TestFeature.Words, value: values[i] }],
|
||||
// onlyIncludeUsage: true,
|
||||
// stripeCli,
|
||||
// db,
|
||||
// org,
|
||||
// env,
|
||||
// });
|
||||
// total += expectedTotal;
|
||||
// }
|
||||
|
||||
// const basePrice = getBasePrice({ product: pro });
|
||||
|
||||
// const customer = await autumn.customers.get(customerId);
|
||||
// const invoice = customer.invoices;
|
||||
// expect(invoice[0].total).toBe(basePrice * 2 + total);
|
||||
// });
|
||||
// });
|
||||
|
||||
// // const expectedTotal = await getAttachPreviewTotal({
|
||||
// // customerId,
|
||||
// // productId: pro.id,
|
||||
// // entityId: "2",
|
||||
// // });
|
||||
@@ -1,50 +0,0 @@
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import { Autumn as AutumnJS } from "autumn-js";
|
||||
import { after } from "mocha";
|
||||
import { initDrizzle } from "@/db/initDrizzle.js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { createStripeCli } from "@/external/connect/createStripeCli.js";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
|
||||
const ORG_SLUG = process.env.TESTS_ORG!;
|
||||
const DEFAULT_ENV = AppEnv.Sandbox;
|
||||
|
||||
export const setupBefore = async (instance: any) => {
|
||||
try {
|
||||
const { db, client } = initDrizzle();
|
||||
|
||||
const org = await OrgService.getBySlug({ db, slug: ORG_SLUG });
|
||||
if (!org) {
|
||||
throw new Error("Org not found");
|
||||
}
|
||||
const env = DEFAULT_ENV;
|
||||
const autumnSecretKey = process.env.UNIT_TEST_AUTUMN_SECRET_KEY!;
|
||||
const autumn = new AutumnInt({ apiKey: autumnSecretKey });
|
||||
|
||||
const autumnJs = new AutumnJS({
|
||||
secretKey: autumnSecretKey,
|
||||
url: "http://localhost:8080/v1",
|
||||
});
|
||||
|
||||
const stripeCli = createStripeCli({ org, env });
|
||||
instance.org = org;
|
||||
instance.env = env;
|
||||
instance.autumn = autumn;
|
||||
instance.stripeCli = stripeCli;
|
||||
instance.autumnJs = autumnJs;
|
||||
instance.db = db;
|
||||
instance.client = client;
|
||||
} catch (error) {
|
||||
console.log("Error setting up before", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Return a cleanup function
|
||||
after(async () => {
|
||||
await instance.client?.end();
|
||||
});
|
||||
};
|
||||
@@ -1,25 +1 @@
|
||||
update boolean features
|
||||
- add / remove boolean features
|
||||
- no invoice created, subscription stays the same
|
||||
|
||||
### update included usage
|
||||
|
||||
Notes:
|
||||
- included usage changes
|
||||
- usage stays the same
|
||||
|
||||
Cases:
|
||||
- update included usage on multiple features
|
||||
- update included usage on paid features
|
||||
-> prepaid feature price: nothing changes
|
||||
-> allocated feature price: price in Stripe changes, proration created for difference in overage
|
||||
-> consumable feature price (unsure of effects yet, but handle this case)
|
||||
-> updating multiple paid features at once
|
||||
|
||||
update price
|
||||
|
||||
update-edge-cases
|
||||
- update max purchase, etc.
|
||||
|
||||
|
||||
Unit Tests:
|
||||
custom-plan
|
||||
@@ -0,0 +1,74 @@
|
||||
import { test } from "bun:test";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import { items } from "@tests/utils/fixtures/items.js";
|
||||
import { products } from "@tests/utils/fixtures/products.js";
|
||||
import { initTestScenario } from "@tests/utils/testInitUtils/initTestScenario.js";
|
||||
import chalk from "chalk";
|
||||
|
||||
test.concurrent(`${chalk.yellowBright("custom-plan: update free plan")}`, async () => {
|
||||
const messagesItem = items.monthlyMessages({ includedUsage: 500 });
|
||||
const dashboardItem = items.dashboard();
|
||||
const wordsItem = items.monthlyWords({ includedUsage: 100 });
|
||||
|
||||
const free = products.base({ items: [messagesItem] });
|
||||
|
||||
const { customerId, autumnV1 } = await initTestScenario({
|
||||
customerId: "update-sub-free1",
|
||||
products: [free],
|
||||
attachProducts: [free.id],
|
||||
customerOptions: {
|
||||
withTestClock: true,
|
||||
attachPm: "success",
|
||||
},
|
||||
});
|
||||
|
||||
const messagesUsage = 100;
|
||||
await autumnV1.track(
|
||||
{
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: messagesUsage,
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
|
||||
await autumnV1.subscriptions.update({
|
||||
customer_id: customerId,
|
||||
product_id: free.id,
|
||||
items: [messagesItem, dashboardItem, wordsItem],
|
||||
});
|
||||
});
|
||||
|
||||
test.concurrent(`${chalk.yellowBright("custom-plan: something else")}`, async () => {
|
||||
const messagesItem = items.monthlyMessages({ includedUsage: 500 });
|
||||
const dashboardItem = items.dashboard();
|
||||
const wordsItem = items.monthlyWords({ includedUsage: 100 });
|
||||
|
||||
const free = products.base({ items: [messagesItem] });
|
||||
|
||||
const { customerId, autumnV1 } = await initTestScenario({
|
||||
customerId: "custom-plan-something-else",
|
||||
products: [free],
|
||||
attachProducts: [free.id],
|
||||
customerOptions: {
|
||||
withTestClock: true,
|
||||
attachPm: "success",
|
||||
},
|
||||
});
|
||||
|
||||
const messagesUsage = 100;
|
||||
await autumnV1.track(
|
||||
{
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: messagesUsage,
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
|
||||
await autumnV1.subscriptions.update({
|
||||
customer_id: customerId,
|
||||
product_id: free.id,
|
||||
items: [messagesItem, dashboardItem, wordsItem],
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
import { beforeAll, describe, test } from "bun:test";
|
||||
import { ApiVersion } from "@autumn/shared";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructFeatureItem } 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";
|
||||
|
||||
describe(`${chalk.yellowBright("custom-plan: update free plan")}`, () => {
|
||||
const testCase = "custom-plan-update-free-plan";
|
||||
|
||||
const messagesItem = constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 500,
|
||||
});
|
||||
|
||||
const dashboardItem = constructFeatureItem({
|
||||
featureId: TestFeature.Dashboard,
|
||||
isBoolean: true,
|
||||
});
|
||||
|
||||
const wordsItem = constructFeatureItem({
|
||||
featureId: TestFeature.Words,
|
||||
includedUsage: 100,
|
||||
});
|
||||
|
||||
const free = constructProduct({
|
||||
type: "free",
|
||||
items: [messagesItem],
|
||||
});
|
||||
|
||||
const customerId = testCase;
|
||||
const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
|
||||
beforeAll(async () => {
|
||||
console.log("FILE 2 - describe 1 started at:", new Date().toISOString());
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
withTestClock: true,
|
||||
attachPm: "success",
|
||||
});
|
||||
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [free],
|
||||
prefix: testCase,
|
||||
});
|
||||
|
||||
await autumnV1.attach({
|
||||
customer_id: customerId,
|
||||
product_id: free.id,
|
||||
});
|
||||
});
|
||||
|
||||
const messagesUsage = 100;
|
||||
test("should add boolean and metered feature to free plan", async () => {
|
||||
await autumnV1.track(
|
||||
{
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: messagesUsage,
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
|
||||
await autumnV1.subscriptions.update({
|
||||
customer_id: customerId,
|
||||
product_id: free.id,
|
||||
items: [messagesItem, dashboardItem, wordsItem],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(`${chalk.yellowBright("custom-plan: something else")}`, () => {
|
||||
const testCase = "custom-plan-update-free-plan";
|
||||
|
||||
const messagesItem = constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage: 500,
|
||||
});
|
||||
|
||||
const dashboardItem = constructFeatureItem({
|
||||
featureId: TestFeature.Dashboard,
|
||||
isBoolean: true,
|
||||
});
|
||||
|
||||
const wordsItem = constructFeatureItem({
|
||||
featureId: TestFeature.Words,
|
||||
includedUsage: 100,
|
||||
});
|
||||
|
||||
const free = constructProduct({
|
||||
type: "free",
|
||||
items: [messagesItem],
|
||||
});
|
||||
|
||||
const customerId = testCase;
|
||||
const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
|
||||
beforeAll(async () => {
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
withTestClock: true,
|
||||
attachPm: "success",
|
||||
});
|
||||
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [free],
|
||||
prefix: testCase,
|
||||
});
|
||||
|
||||
await autumnV1.attach({
|
||||
customer_id: customerId,
|
||||
product_id: free.id,
|
||||
});
|
||||
});
|
||||
|
||||
const messagesUsage = 100;
|
||||
test("should add boolean and metered feature to free plan", async () => {
|
||||
await autumnV1.track(
|
||||
{
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Messages,
|
||||
value: messagesUsage,
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
);
|
||||
|
||||
await autumnV1.subscriptions.update({
|
||||
customer_id: customerId,
|
||||
product_id: free.id,
|
||||
items: [messagesItem, dashboardItem, wordsItem],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,186 +0,0 @@
|
||||
import { Hyperbrowser } from "@hyperbrowser/sdk";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
const client = new Hyperbrowser({
|
||||
apiKey: process.env.HYPERBROWSER_API_KEY,
|
||||
});
|
||||
|
||||
let sessionPromise: Promise<any> | null = null;
|
||||
let currentSession: any | null = null;
|
||||
|
||||
// File paths for coordination across processes
|
||||
const TEMP_DIR = path.join(process.cwd(), ".tmp");
|
||||
const SESSION_FILE = path.join(TEMP_DIR, "browser-session.json");
|
||||
const LOCK_FILE = path.join(TEMP_DIR, "browser-session.lock");
|
||||
|
||||
// Ensure temp directory exists
|
||||
if (!fs.existsSync(TEMP_DIR)) {
|
||||
fs.mkdirSync(TEMP_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
export const getBrowserSession = async () => {
|
||||
// If we already have a session in memory, return it
|
||||
if (currentSession) {
|
||||
return currentSession;
|
||||
}
|
||||
|
||||
// Check if another process has already created a session
|
||||
const existingSession = loadSessionFromFile();
|
||||
if (existingSession) {
|
||||
currentSession = existingSession;
|
||||
console.log("Using existing browser session:", existingSession.id);
|
||||
return existingSession;
|
||||
}
|
||||
|
||||
// If session creation is already in progress, wait for it
|
||||
if (sessionPromise) {
|
||||
return await sessionPromise;
|
||||
}
|
||||
|
||||
// Try to acquire lock and create session
|
||||
sessionPromise = createSessionWithLock();
|
||||
|
||||
try {
|
||||
currentSession = await sessionPromise;
|
||||
return currentSession;
|
||||
} catch (error) {
|
||||
// Reset promise on failure so we can retry
|
||||
sessionPromise = null;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const createSessionWithLock = async (): Promise<any> => {
|
||||
// Try to acquire lock
|
||||
const lockAcquired = await acquireLock();
|
||||
|
||||
if (!lockAcquired) {
|
||||
// Another process is creating the session, wait for it
|
||||
console.log("Waiting for another process to create browser session...");
|
||||
return await waitForSession();
|
||||
}
|
||||
|
||||
try {
|
||||
// Double-check if session was created while we were acquiring lock
|
||||
const existingSession = loadSessionFromFile();
|
||||
if (existingSession) {
|
||||
console.log(
|
||||
"Session was created by another process:",
|
||||
existingSession.id,
|
||||
);
|
||||
return existingSession;
|
||||
}
|
||||
|
||||
// Create new session
|
||||
console.log("Creating new browser session...");
|
||||
const session = await client.sessions.create();
|
||||
console.log("Browser session created successfully:", session.id);
|
||||
|
||||
// Save session to file for other processes
|
||||
saveSessionToFile(session);
|
||||
|
||||
return session;
|
||||
} finally {
|
||||
// Always release lock
|
||||
releaseLock();
|
||||
}
|
||||
};
|
||||
|
||||
const acquireLock = async (): Promise<boolean> => {
|
||||
try {
|
||||
// Try to create lock file exclusively
|
||||
fs.writeFileSync(LOCK_FILE, process.pid.toString(), { flag: "wx" });
|
||||
return true;
|
||||
} catch (error) {
|
||||
// Lock file exists, another process has the lock
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const releaseLock = () => {
|
||||
try {
|
||||
if (fs.existsSync(LOCK_FILE)) {
|
||||
fs.unlinkSync(LOCK_FILE);
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore errors when releasing lock
|
||||
console.warn("Warning: Could not release lock file:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const saveSessionToFile = (session: any) => {
|
||||
try {
|
||||
fs.writeFileSync(SESSION_FILE, JSON.stringify(session));
|
||||
} catch (error) {
|
||||
console.error("Failed to save session to file:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadSessionFromFile = (): any | null => {
|
||||
try {
|
||||
if (fs.existsSync(SESSION_FILE)) {
|
||||
const sessionData = fs.readFileSync(SESSION_FILE, "utf-8");
|
||||
return JSON.parse(sessionData);
|
||||
}
|
||||
} catch (error) {
|
||||
// If file is corrupted or doesn't exist, ignore
|
||||
console.warn("Could not load session from file:", error);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const waitForSession = async (): Promise<any> => {
|
||||
// Poll for session file to appear
|
||||
for (let i = 0; i < 30; i++) {
|
||||
// Wait up to 30 seconds
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1 second
|
||||
|
||||
const session = loadSessionFromFile();
|
||||
if (session) {
|
||||
console.log("Found session created by another process:", session.id);
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"Timeout waiting for browser session to be created by another process",
|
||||
);
|
||||
};
|
||||
|
||||
// Reset session state (call this between test suites if needed)
|
||||
export const resetBrowserSession = () => {
|
||||
console.log("Resetting browser session state...");
|
||||
currentSession = null;
|
||||
sessionPromise = null;
|
||||
|
||||
// Clean up files
|
||||
try {
|
||||
if (fs.existsSync(SESSION_FILE)) {
|
||||
fs.unlinkSync(SESSION_FILE);
|
||||
}
|
||||
if (fs.existsSync(LOCK_FILE)) {
|
||||
fs.unlinkSync(LOCK_FILE);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Warning: Could not clean up session files:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Cleanup on process exit
|
||||
process.on("exit", () => {
|
||||
if (currentSession) {
|
||||
console.log("Process exiting, cleaning up browser session files...");
|
||||
releaseLock();
|
||||
}
|
||||
});
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
resetBrowserSession();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
resetBrowserSession();
|
||||
process.exit(0);
|
||||
});
|
||||
@@ -6,8 +6,8 @@ dotenv.config();
|
||||
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { clearOrg } from "./utils/setupUtils/clearOrg.js";
|
||||
import { setupOrg } from "./utils/setupUtils/setupOrg.js";
|
||||
import { clearOrg } from "./utils/setup/clearOrg.js";
|
||||
import { setupOrg } from "./utils/setup/setupOrg.js";
|
||||
|
||||
async function main() {
|
||||
console.log(chalk.blue("\n🧹 Clearing Master Org...\n"));
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { LegacyVersion } from "@autumn/shared";
|
||||
import chalk from "chalk";
|
||||
import { addWeeks, addYears } from "date-fns";
|
||||
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, addYears } from "date-fns";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
@@ -68,7 +68,6 @@ describe(`${chalk.yellowBright("interval1: Should upgrade from pro to pro annual
|
||||
advanceTo: addWeeks(new Date(), 2).getTime(),
|
||||
});
|
||||
});
|
||||
return;
|
||||
|
||||
test("should upgrade to pro annual and have correct next cycle at", async () => {
|
||||
const checkoutRes = await autumn.checkout({
|
||||
@@ -96,7 +95,7 @@ describe(`${chalk.yellowBright("interval1: Should upgrade from pro to pro annual
|
||||
|
||||
const subItem = sub!.items.data[0];
|
||||
expect(subItem.current_period_end * 1000).toBeCloseTo(
|
||||
checkoutRes.next_cycle?.starts_at!,
|
||||
checkoutRes.next_cycle?.starts_at ?? 0,
|
||||
-Math.log10(toMilliseconds.days(1)), // +- 1 day
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,32 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { loadLocalEnv } from "../src/utils/envUtils.js";
|
||||
|
||||
// ... rest of your existing setup code
|
||||
|
||||
const loadInfisicalSecrets = async () => {
|
||||
// Load infisical secrets into process.env
|
||||
try {
|
||||
const secrets = execSync("infisical export --env=dev --format=dotenv", {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
for (const line of secrets.split("\n")) {
|
||||
const match = line.match(/^([^=]+)=(.*)$/);
|
||||
if (match) {
|
||||
process.env[match[1]] = match[2].replace(/^["']|["']$/g, "");
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to load infisical secrets:", e);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Bun test preload script for integration tests.
|
||||
* Loads environment variables before any test file runs.
|
||||
*/
|
||||
import { loadLocalEnv } from "../src/utils/envUtils.js";
|
||||
|
||||
console.log("--- Setup integration tests ---");
|
||||
await loadLocalEnv();
|
||||
await loadInfisicalSecrets();
|
||||
loadLocalEnv();
|
||||
console.log("--- Setup integration tests complete ---");
|
||||
|
||||
@@ -3,7 +3,7 @@ import { loadLocalEnv } from "../src/utils/envUtils";
|
||||
loadLocalEnv();
|
||||
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import { setupOrg } from "@tests/utils/setupUtils/setupOrg.js";
|
||||
import { setupOrg } from "@tests/utils/setup/setupOrg.js";
|
||||
import { initDrizzle } from "@/db/initDrizzle.js";
|
||||
import { OrgService } from "@/internal/orgs/OrgService.js";
|
||||
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
// import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
// import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
|
||||
// import { LegacyVersion, AppEnv, Organization } from "@autumn/shared";
|
||||
// import chalk from "chalk";
|
||||
// import Stripe from "stripe";
|
||||
// import { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
// import { setupBefore } from "@tests/before.js";
|
||||
// import { createProducts } from "@tests/utils/productUtils.js";
|
||||
// import { addPrefixToProducts } from "../utils.js";
|
||||
// import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
// import {
|
||||
// constructArrearProratedItem,
|
||||
// constructFeatureItem,
|
||||
// } from "@/utils/scriptUtils/constructItem.js";
|
||||
// import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
|
||||
// export let pro = constructProduct({
|
||||
// items: [
|
||||
// constructArrearProratedItem({
|
||||
// featureId: TestFeature.Users,
|
||||
// pricePerUnit: 50,
|
||||
// }),
|
||||
// constructFeatureItem({
|
||||
// featureId: TestFeature.Messages,
|
||||
// includedUsage: 100,
|
||||
// entityFeatureId: TestFeature.Users,
|
||||
// }),
|
||||
// ],
|
||||
// type: "pro",
|
||||
// });
|
||||
|
||||
// const testCase = "entity1";
|
||||
|
||||
// describe(`${chalk.yellowBright(`${testCase}: Testing entities`)}`, () => {
|
||||
// let customerId = testCase;
|
||||
// let autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.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;
|
||||
// db = this.db;
|
||||
// org = this.org;
|
||||
// env = this.env;
|
||||
|
||||
// stripeCli = this.stripeCli;
|
||||
|
||||
// addPrefixToProducts({
|
||||
// products: [pro],
|
||||
// prefix: testCase,
|
||||
// });
|
||||
|
||||
// await createProducts({
|
||||
// autumn,
|
||||
// products: [pro],
|
||||
// customerId,
|
||||
// db,
|
||||
// orgId: org.id,
|
||||
// env,
|
||||
// });
|
||||
|
||||
// const { testClockId: testClockId1 } = await initCustomer({
|
||||
// autumn: autumnJs,
|
||||
// customerId,
|
||||
// db,
|
||||
// org,
|
||||
// env,
|
||||
// attachPm: "success",
|
||||
// });
|
||||
|
||||
// testClockId = testClockId1!;
|
||||
// });
|
||||
|
||||
// it("should create entity, then attach pro product", async function () {
|
||||
// console.log("Here!");
|
||||
// });
|
||||
// });
|
||||
167
server/tests/utils/fixtures/items.ts
Normal file
167
server/tests/utils/fixtures/items.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { TestFeature } from "@tests/setup/v2Features";
|
||||
import {
|
||||
constructArrearItem,
|
||||
constructArrearProratedItem,
|
||||
constructFeatureItem,
|
||||
constructPrepaidItem,
|
||||
} from "@/utils/scriptUtils/constructItem";
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// BOOLEAN FEATURES
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Boolean feature - on/off access (no usage tracking)
|
||||
* @returns Dashboard feature item
|
||||
*/
|
||||
const dashboard = () =>
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Dashboard,
|
||||
isBoolean: true,
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// FREE METERED (included usage, resets monthly)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Monthly messages - resets each billing cycle
|
||||
* @param includedUsage - Free usage allowance (default: 100)
|
||||
*/
|
||||
const monthlyMessages = ({
|
||||
includedUsage = 100,
|
||||
}: {
|
||||
includedUsage?: number;
|
||||
} = {}) =>
|
||||
constructFeatureItem({ featureId: TestFeature.Messages, includedUsage });
|
||||
|
||||
/**
|
||||
* Monthly words - resets each billing cycle
|
||||
* @param includedUsage - Free usage allowance (default: 100)
|
||||
*/
|
||||
const monthlyWords = ({
|
||||
includedUsage = 100,
|
||||
}: {
|
||||
includedUsage?: number;
|
||||
} = {}) =>
|
||||
constructFeatureItem({ featureId: TestFeature.Words, includedUsage });
|
||||
|
||||
/**
|
||||
* Monthly credits - resets each billing cycle
|
||||
* @param includedUsage - Free usage allowance (default: 100)
|
||||
*/
|
||||
const monthlyCredits = ({
|
||||
includedUsage = 100,
|
||||
}: {
|
||||
includedUsage?: number;
|
||||
} = {}) =>
|
||||
constructFeatureItem({ featureId: TestFeature.Credits, includedUsage });
|
||||
|
||||
/**
|
||||
* Unlimited messages - no usage cap
|
||||
* @returns Unlimited messages feature item
|
||||
*/
|
||||
const unlimitedMessages = () =>
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
unlimited: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* Lifetime messages - never resets (interval: null)
|
||||
* @param includedUsage - One-time usage allowance (default: 100)
|
||||
*/
|
||||
const lifetimeMessages = ({
|
||||
includedUsage = 100,
|
||||
}: {
|
||||
includedUsage?: number;
|
||||
} = {}) =>
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage,
|
||||
interval: null,
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// PREPAID (purchase units upfront)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Prepaid messages - purchase units upfront ($10/unit)
|
||||
* @param includedUsage - Free units before purchase required (default: 0)
|
||||
*/
|
||||
const prepaidMessages = ({
|
||||
includedUsage = 0,
|
||||
}: {
|
||||
includedUsage?: number;
|
||||
} = {}) =>
|
||||
constructPrepaidItem({
|
||||
featureId: TestFeature.Messages,
|
||||
price: 10,
|
||||
billingUnits: 1,
|
||||
includedUsage,
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// CONSUMABLE / PAY-PER-USE (overage pricing)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Consumable messages - pay-per-use overage ($0.10/unit)
|
||||
* @param includedUsage - Free units before overage kicks in (default: 0)
|
||||
*/
|
||||
const consumableMessages = ({
|
||||
includedUsage = 0,
|
||||
}: {
|
||||
includedUsage?: number;
|
||||
} = {}) =>
|
||||
constructArrearItem({
|
||||
featureId: TestFeature.Messages,
|
||||
includedUsage,
|
||||
price: 0.1,
|
||||
billingUnits: 1,
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// ALLOCATED / SEATS (prorated billing)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Allocated users/seats - prorated billing on change ($10/seat)
|
||||
* @param includedUsage - Free seats included (default: 0)
|
||||
*/
|
||||
const allocatedUsers = ({
|
||||
includedUsage = 0,
|
||||
}: {
|
||||
includedUsage?: number;
|
||||
} = {}) =>
|
||||
constructArrearProratedItem({
|
||||
featureId: TestFeature.Users,
|
||||
pricePerUnit: 10,
|
||||
includedUsage,
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// EXPORT
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
export const items = {
|
||||
// Boolean
|
||||
dashboard,
|
||||
|
||||
// Free metered
|
||||
monthlyMessages,
|
||||
monthlyWords,
|
||||
monthlyCredits,
|
||||
unlimitedMessages,
|
||||
lifetimeMessages,
|
||||
|
||||
// Prepaid
|
||||
prepaidMessages,
|
||||
|
||||
// Consumable
|
||||
consumableMessages,
|
||||
|
||||
// Allocated
|
||||
allocatedUsers,
|
||||
} as const;
|
||||
25
server/tests/utils/fixtures/products.ts
Normal file
25
server/tests/utils/fixtures/products.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { ProductItem, ProductV2 } from "@autumn/shared";
|
||||
import { constructRawProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
|
||||
/**
|
||||
* Base product - no base price, customizable defaults
|
||||
* @param items - Product items (features)
|
||||
* @param id - Product ID (default: "base")
|
||||
* @param isDefault - Whether this is a default product (default: false)
|
||||
*/
|
||||
const base = ({
|
||||
items,
|
||||
id = "base",
|
||||
isDefault = false,
|
||||
}: {
|
||||
items: ProductItem[];
|
||||
id?: string;
|
||||
isDefault?: boolean;
|
||||
}): ProductV2 => ({
|
||||
...constructRawProduct({ id, items }),
|
||||
is_default: isDefault,
|
||||
});
|
||||
|
||||
export const products = {
|
||||
base,
|
||||
} as const;
|
||||
@@ -1,8 +0,0 @@
|
||||
export const isValidNumber = (value: any) => {
|
||||
const number = parseFloat(value);
|
||||
return !Number.isNaN(number) && Number.isFinite(number);
|
||||
};
|
||||
|
||||
export const numberWithCommas = (x: number) => {
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
};
|
||||
@@ -1,56 +1,9 @@
|
||||
import type { AppEnv, Organization } from "@autumn/shared";
|
||||
import { expect } from "chai";
|
||||
import type Stripe from "stripe";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { createStripeCli } from "@/external/connect/createStripeCli.js";
|
||||
import { ProductService } from "@/internal/products/ProductService.js";
|
||||
|
||||
export const checkScheduleContainsProducts = async ({
|
||||
db,
|
||||
org,
|
||||
env,
|
||||
scheduleId,
|
||||
schedule,
|
||||
productIds,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
org: Organization;
|
||||
env: AppEnv;
|
||||
scheduleId?: string;
|
||||
schedule?: Stripe.SubscriptionSchedule;
|
||||
productIds: string[];
|
||||
}) => {
|
||||
if (!schedule && !scheduleId) {
|
||||
throw new Error("schedule or scheduleId must be provided");
|
||||
}
|
||||
|
||||
if (scheduleId) {
|
||||
const stripeCli = createStripeCli({ org: org, env: env });
|
||||
schedule = await stripeCli.subscriptionSchedules.retrieve(scheduleId);
|
||||
}
|
||||
|
||||
let priceCount = 0;
|
||||
for (const productId of productIds) {
|
||||
const product = await ProductService.getFull({
|
||||
db,
|
||||
idOrInternalId: productId,
|
||||
orgId: org.id,
|
||||
env: env,
|
||||
});
|
||||
|
||||
for (const price of product.prices) {
|
||||
expect(
|
||||
schedule!.phases[0].items.some(
|
||||
(item) => item.price === price.config!.stripe_price_id,
|
||||
),
|
||||
).to.be.true;
|
||||
priceCount++;
|
||||
}
|
||||
}
|
||||
|
||||
expect(schedule!.phases[0].items.length).to.equal(priceCount);
|
||||
};
|
||||
|
||||
export const checkSubscriptionContainsProducts = async ({
|
||||
db,
|
||||
org,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Re-export functions from setupUtils
|
||||
export { clearOrg } from "./setupUtils/clearOrg.js";
|
||||
export { getAxiosInstance, setupOrg } from "./setupUtils/setupOrg.js";
|
||||
export { clearOrg } from "./setup/clearOrg.js";
|
||||
export { getAxiosInstance, setupOrg } from "./setup/setupOrg.js";
|
||||
|
||||
import axios from "axios";
|
||||
|
||||
|
||||
@@ -24,24 +24,6 @@ export const clearOrg = async ({
|
||||
|
||||
const autumn = new AutumnInt();
|
||||
|
||||
// if (process.env.STRIPE_TEST_KEY) {
|
||||
// console.log(`Reconnecting stripe...`);
|
||||
|
||||
// try {
|
||||
// await autumn.stripe.delete();
|
||||
// } catch (_error) {}
|
||||
// try {
|
||||
// await autumn.stripe.connect({
|
||||
// secret_key: process.env.STRIPE_TEST_KEY!,
|
||||
// success_url: "https://useautumn.com",
|
||||
// default_currency: "usd",
|
||||
// });
|
||||
// } catch (error: any) {
|
||||
// console.error("Error reconnecting stripe", error.message);
|
||||
// process.exit(1);
|
||||
// }
|
||||
// }
|
||||
|
||||
const { db, client } = initDrizzle();
|
||||
const org = await OrgService.getBySlug({ db, slug: orgSlug });
|
||||
|
||||
88
server/tests/utils/testInitUtils/initTestScenario.ts
Normal file
88
server/tests/utils/testInitUtils/initTestScenario.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { ApiVersion, type ProductV2 } from "@autumn/shared";
|
||||
import type { CustomerData } from "autumn-js";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
import ctx from "./createTestContext.js";
|
||||
|
||||
/**
|
||||
* Initialize a complete test scenario with customer, products, and optional attachment.
|
||||
* Combines initCustomerV3, initProductsV0, and attach into a single call.
|
||||
*
|
||||
* @param customerId - Unique identifier used as customerId and product prefix
|
||||
* @param products - Array of products to create (use constructProduct)
|
||||
* @param attachProducts - Array of product IDs to attach to customer
|
||||
* @param customerOptions - Customer initialization options
|
||||
*/
|
||||
export const initTestScenario = async ({
|
||||
customerId,
|
||||
products,
|
||||
attachProducts,
|
||||
customerOptions = {},
|
||||
}: {
|
||||
customerId: string;
|
||||
products: ProductV2[];
|
||||
attachProducts?: string[];
|
||||
customerOptions?: {
|
||||
customerData?: CustomerData;
|
||||
attachPm?: "success" | "fail" | "authenticate";
|
||||
withDefault?: boolean;
|
||||
withTestClock?: boolean;
|
||||
};
|
||||
}) => {
|
||||
const {
|
||||
customerData,
|
||||
attachPm,
|
||||
withDefault = false,
|
||||
withTestClock = true,
|
||||
} = customerOptions;
|
||||
|
||||
// 1. Initialize customer
|
||||
const { testClockId, customer } = await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
customerData,
|
||||
attachPm,
|
||||
withTestClock,
|
||||
withDefault,
|
||||
});
|
||||
|
||||
// 2. Initialize products (prefix = customerId for isolation)
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products,
|
||||
prefix: customerId,
|
||||
});
|
||||
|
||||
// 3. Create autumn clients
|
||||
const autumnV1 = new AutumnInt({
|
||||
version: ApiVersion.V1_2,
|
||||
secretKey: ctx.orgSecretKey,
|
||||
});
|
||||
|
||||
const autumnV2 = new AutumnInt({
|
||||
version: ApiVersion.V2_0,
|
||||
secretKey: ctx.orgSecretKey,
|
||||
});
|
||||
|
||||
// 4. Attach products if requested (prefix IDs to match mutated products)
|
||||
if (attachProducts && attachProducts.length > 0) {
|
||||
for (const productId of attachProducts) {
|
||||
const prefixedId = `${customerId}_${productId}`;
|
||||
await autumnV1.attach({
|
||||
customer_id: customerId,
|
||||
product_id: prefixedId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
customerId,
|
||||
products,
|
||||
autumnV1,
|
||||
autumnV2,
|
||||
testClockId,
|
||||
customer,
|
||||
ctx,
|
||||
};
|
||||
};
|
||||
18
shared/api/billing/common/billingPreviewResponse.ts
Normal file
18
shared/api/billing/common/billingPreviewResponse.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const BillingPreviewResponseSchema = z.object({
|
||||
customer_id: z.string(),
|
||||
line_items: z.array(
|
||||
z.object({
|
||||
description: z.string(),
|
||||
amount: z.number(),
|
||||
}),
|
||||
),
|
||||
|
||||
total: z.number(),
|
||||
currency: z.string(),
|
||||
});
|
||||
|
||||
export type BillingPreviewResponse = z.infer<
|
||||
typeof BillingPreviewResponseSchema
|
||||
>;
|
||||
16
shared/api/billing/index.ts
Normal file
16
shared/api/billing/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// Attach
|
||||
export * from "./attach/attachBodyV1.js";
|
||||
export * from "./attach/prevVersions/attachBodyV0.js";
|
||||
export * from "./attach/prevVersions/attachResponseV1.js";
|
||||
|
||||
// Checkout
|
||||
export * from "./checkout/checkoutParamsV1.js";
|
||||
export * from "./checkout/prevVersions/checkoutParamsV0.js";
|
||||
export * from "./checkout/prevVersions/checkoutResponseV0.js";
|
||||
|
||||
// Common
|
||||
export * from "./common/billingPreviewResponse.js";
|
||||
export * from "./updateSubscription/previewUpdateSubscriptionResponse.js";
|
||||
|
||||
// Update Subscription
|
||||
export * from "./updateSubscription/updateSubscriptionV0Params.js";
|
||||
@@ -1,59 +0,0 @@
|
||||
import type { z } from "zod/v4";
|
||||
import { ApiVersion } from "../../../versionUtils/ApiVersion";
|
||||
import {
|
||||
AffectedResource,
|
||||
defineVersionChange,
|
||||
} from "../../../versionUtils/versionChangeUtils/VersionChange";
|
||||
import { SubscriptionUpdateV1ParamsSchema } from "../subscriptionUpdateV1Params";
|
||||
import { UpdateSubscriptionV0ParamsSchema } from "../UpdateSubscriptionV0Params";
|
||||
|
||||
/**
|
||||
* V2_0_SubscriptionUpdateChange: Transforms subscription update params from V2.0 to V2.1 format
|
||||
*
|
||||
* Applied when: sourceVersion <= V2.0
|
||||
*
|
||||
* Breaking changes introduced in V2.1:
|
||||
*
|
||||
* 1. Renamed field: `product_id` → `plan_id`
|
||||
* 2. Removed fields: `entity_id`, `customer_data`, `entity_data`, `options`, invoice settings
|
||||
* 3. Added field: `plan_override` for customizations
|
||||
*
|
||||
* Input: UpdateSubscriptionV0Params (V2.0 format)
|
||||
* Output: SubscriptionUpdateV1Params (V2.1 format)
|
||||
*/
|
||||
|
||||
export const V2_0_SubscriptionUpdateChange = defineVersionChange({
|
||||
name: "V2.1 Subscription Update Change",
|
||||
newVersion: ApiVersion.V2_1,
|
||||
oldVersion: ApiVersion.V2_0,
|
||||
description: [
|
||||
"Transforms subscription update params from V2.0 to V2.1 format",
|
||||
],
|
||||
affectedResources: [AffectedResource.ApiSubscriptionUpdate],
|
||||
newSchema: SubscriptionUpdateV1ParamsSchema,
|
||||
oldSchema: UpdateSubscriptionV0ParamsSchema,
|
||||
affectsRequest: true,
|
||||
affectsResponse: false,
|
||||
|
||||
// Request: V0 (UpdateSubscriptionV0Params) → V1 (SubscriptionUpdateV1Params)
|
||||
transformRequest: ({
|
||||
input,
|
||||
}: {
|
||||
input: z.infer<typeof UpdateSubscriptionV0ParamsSchema>;
|
||||
}): z.infer<typeof SubscriptionUpdateV1ParamsSchema> => {
|
||||
const planId = input.product_id;
|
||||
|
||||
if (!planId) {
|
||||
throw new Error("product_id is required");
|
||||
}
|
||||
|
||||
// if (input.items) {
|
||||
// const planFeatures = input.items.map((item) => productV2);
|
||||
// }
|
||||
|
||||
return {
|
||||
customer_id: input.customer_id,
|
||||
plan_id: planId,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import { z } from "zod/v4";
|
||||
import { PlanOverrideSchema } from "../common/planOverride";
|
||||
|
||||
export const SubscriptionUpdateV1ParamsSchema = z.object({
|
||||
// Customer / Entity Info
|
||||
customer_id: z.string(),
|
||||
plan_id: z.string(),
|
||||
|
||||
plan_override: PlanOverrideSchema.optional(),
|
||||
|
||||
// customer_data: CustomerDataSchema.optional(),
|
||||
// entity_data: EntityDataSchema.optional(),
|
||||
|
||||
// options: z.array(FeatureOptionsSchema).nullish(),
|
||||
|
||||
// invoice: z.boolean().optional(),
|
||||
// enable_product_immediately: z.boolean().optional(),
|
||||
// finalize_invoice: z.boolean().optional(),
|
||||
|
||||
// // Reset billing cycle anchor?
|
||||
// reset_billing_cycle_anchor: z.boolean().optional(),
|
||||
// new_billing_subscription: z.boolean().optional(),
|
||||
// prorate_billing: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export type SubscriptionUpdateV1Params = z.infer<
|
||||
typeof SubscriptionUpdateV1ParamsSchema
|
||||
>;
|
||||
@@ -0,0 +1,9 @@
|
||||
import { BillingPreviewResponseSchema } from "@api/billing/common/billingPreviewResponse";
|
||||
import type { z } from "zod/v4";
|
||||
|
||||
export const PreviewUpdateSubscriptionResponseSchema =
|
||||
BillingPreviewResponseSchema;
|
||||
|
||||
export type PreviewUpdateSubscriptionResponse = z.infer<
|
||||
typeof PreviewUpdateSubscriptionResponseSchema
|
||||
>;
|
||||
@@ -69,15 +69,8 @@ export * from "./balances/track/trackParams.js";
|
||||
export * from "./balances/track/trackResponseV2.js";
|
||||
export * from "./balances/track/trackTypes/pgDeductionUpdate.js";
|
||||
export * from "./balances/usageModels.js";
|
||||
export * from "./billing/attach/attachBodyV1.js";
|
||||
export * from "./billing/attach/prevVersions/attachBodyV0.js";
|
||||
export * from "./billing/attach/prevVersions/attachResponseV1.js";
|
||||
export * from "./billing/checkout/checkoutParamsV1.js";
|
||||
export * from "./billing/checkout/prevVersions/checkoutParamsV0.js";
|
||||
export * from "./billing/checkout/prevVersions/checkoutParamsV0.js";
|
||||
export * from "./billing/checkout/prevVersions/checkoutResponseV0.js";
|
||||
export * from "./billing/subscriptionUpdate/subscriptionUpdateV1Params.js";
|
||||
export * from "./billing/subscriptionUpdate/updateSubscriptionV0Params.js";
|
||||
// Billing
|
||||
export * from "./billing/index.js";
|
||||
|
||||
export * from "./common/customerData.js";
|
||||
export * from "./common/entityData.js";
|
||||
|
||||
@@ -40,7 +40,7 @@ import { useCusQuery } from "@/views/customers/customer/hooks/useCusQuery";
|
||||
*
|
||||
* This is an isolated test sheet for testing the subscription update flow.
|
||||
* It calls:
|
||||
* - POST /v1/subscriptions/preview/update - to get a billing plan preview
|
||||
* - POST /v1/subscriptions/preview_update - to get a billing plan preview
|
||||
* - POST /v1/subscriptions/update - to execute the update
|
||||
*
|
||||
* Usage: Open this sheet with an itemId (cusProduct id) and optional customizedProduct in data
|
||||
@@ -857,7 +857,7 @@ function useSubscriptionUpdatePreview({
|
||||
queryFn: async () => {
|
||||
if (!debouncedBody) return null;
|
||||
const response = await axiosInstance.post(
|
||||
"/v1/subscriptions/preview/update",
|
||||
"/v1/subscriptions/preview_update",
|
||||
debouncedBody,
|
||||
);
|
||||
return response.data;
|
||||
|
||||
Reference in New Issue
Block a user