fix: added tests for attach and fixed contUse updates

This commit is contained in:
John Yeo
2025-06-17 12:26:58 +01:00
parent b248cf6be9
commit 943baf9ff8
41 changed files with 519 additions and 820 deletions

View File

@@ -1,4 +1,5 @@
services:
# Run valkey
valkey:
image: docker.io/bitnami/valkey:8.0
environment:
@@ -11,8 +12,8 @@ services:
interval: 10s
timeout: 5s
retries: 5
# ports:
# - "6379:6379"
ports:
- "6379:6379"
restart: unless-stopped
shared:

6
pnpm-lock.yaml generated
View File

@@ -244,6 +244,12 @@ importers:
decimal.js:
specifier: ^10.5.0
version: 10.5.0
dotenv:
specifier: ^16.5.0
version: 16.5.0
drizzle-kit:
specifier: ^0.31.1
version: 0.31.1
drizzle-orm:
specifier: ^0.43.1
version: 0.43.1(@opentelemetry/api@1.9.0)(@types/pg@8.15.4)(kysely@0.28.2)(pg@8.16.0)(postgres@3.4.7)

6
run.sh
View File

@@ -1,3 +1,7 @@
if [[ $1 == *"docker-compose"* ]]; then
docker compose -f "$1" up
if [[ $2 == "down" ]]; then
docker compose -f "$1" down $3
else
docker compose -f "$1" up $3
fi
fi

View File

@@ -10,3 +10,5 @@ $MOCHA_CMD 'tests/contUse/entities/*.ts'
$MOCHA_CMD 'tests/contUse/update/*.ts'
$MOCHA_CMD 'tests/contUse/track/*.ts'
$MOCHA_CMD 'tests/contUse/roles/*.ts'

View File

@@ -10,7 +10,7 @@ $MOCHA_CMD 'tests/advanced/multiFeature/*.ts' \
'tests/attach/updateQuantity/*.ts' \
'tests/advanced/referrals/*.ts'
# $MOCHA_CMD 'tests/attach/multiProduct/*.ts'
$MOCHA_CMD 'tests/attach/multiProduct/*.ts'
# $MOCHA_CMD 'tests/advanced/usage/*.ts'
$MOCHA_CMD 'tests/advanced/usage/*.ts'

View File

@@ -16,7 +16,7 @@ export const initDrizzle = (params?: { maxConnections?: number }) => {
const db = drizzle(client, {
schema,
logger: process.env.NODE_ENV === "development",
// logger: process.env.NODE_ENV === "development",
});
return { db, client };

View File

@@ -238,8 +238,8 @@ export class AutumnInt {
return data;
},
create: async (customer: { id: string; email: string; name: string }) => {
const data = await this.post(`/customers`, customer);
create: async (customer: { id: string; email: string; name?: string }) => {
const data = await this.post(`/customers?with_autumn_id=true`, customer);
return data;
},
delete: async (
@@ -269,10 +269,10 @@ export class AutumnInt {
customerId: string,
entity: CreateEntity | CreateEntity[],
) => {
let entities = Array.isArray(entity) ? entity : [entity];
// let entities = Array.isArray(entity) ? entity : [entity];
const data = await this.post(
`/customers/${customerId}/entities`,
entities,
`/customers/${customerId}/entities?with_autumn_id=true`,
entity,
);
return data;

View File

@@ -206,6 +206,10 @@ export const getStripeSchedules = async ({
const schedule =
await stripeCli.subscriptionSchedules.retrieve(scheduleId);
if (schedule.status == "canceled") {
return null;
}
const batchPricesGet = [];
for (const item of schedule.phases[0].items) {
batchPricesGet.push(stripeCli.prices.retrieve(item.price as string));

View File

@@ -4,6 +4,7 @@ import { createStripeCli } from "../utils.js";
import Stripe from "stripe";
import { notNullish, timeout } from "@/utils/genUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { RewardService } from "@/internal/rewards/RewardService.js";
export async function handleCusDiscountDeleted({
db,
@@ -35,61 +36,68 @@ export async function handleCusDiscountDeleted({
return;
}
res.status(200).json({ message: "OK" });
// Check if any redemptions available, and apply to customer if so
let redemptions = await RewardRedemptionService.getUnappliedRedemptions({
db,
internalCustomerId: customer.internal_id,
});
logger.info(`discount.deleted: found ${redemptions.length} redemptions`);
if (redemptions.length == 0) {
return;
}
// // Check if any redemptions available, and apply to customer if so
// let redemptions = await RewardRedemptionService.getUnappliedRedemptions({
// db,
// internalCustomerId: customer.internal_id,
// });
let redemption = redemptions[0];
// if (redemptions.length == 0) {
// return;
// }
// Apply redemption to customer
let stripeCli = createStripeCli({
org,
env,
});
// let redemption = redemptions[0];
// let reward = redemption.reward_program.reward;
let stripeCus = (await stripeCli.customers.retrieve(
discount.customer,
)) as Stripe.Customer;
// // Apply redemption to customer
// let stripeCli = createStripeCli({
// org,
// env,
// });
if (stripeCus && notNullish(stripeCus.discount)) {
logger.info(
`discount.deleted: stripe customer ${discount.customer} already has a discount`,
);
return;
}
// let stripeCus = (await stripeCli.customers.retrieve(
// discount.customer,
// )) as Stripe.Customer;
// Send response first...?
res.status(200).json({ message: "OK" });
// if (stripeCus && notNullish(stripeCus.discount)) {
// logger.info(
// `discount.deleted: stripe customer ${discount.customer} already has a discount`,
// );
// return;
// }
const reward = await RewardService.get({
db,
orgId: org.id,
env,
idOrInternalId: redemption.reward_program.internal_reward_id!,
});
// // Send response first...?
// res.status(200).json({ message: "OK" });
if (!reward) {
logger.warn(
`discount.deleted: reward ${redemption.reward_program.internal_id} not found`,
);
return;
}
// if (notNullish(stripeCus.test_clock)) {
// // Time out for test clock to complete
// await timeout(5000);
// }
await stripeCli.customers.update(discount.customer, {
coupon: reward.internal_id,
});
// await stripeCli.customers.update(discount.customer, {
// coupon: reward.internal_id,
// });
await RewardRedemptionService.update({
db,
id: redemption.id,
updates: {
applied: true,
},
});
// await RewardRedemptionService.update({
// db,
// id: redemption.id,
// updates: {
// applied: true,
// },
// });
// logger.info(
// `discount.deleted: applied reward ${reward.name} on customer ${customer.name} (${customer.id})`,
// );
// logger.info(`Redemption ID: ${redemption.id}`);
logger.info(
`discount.deleted: applied reward ${reward.name} on customer ${customer.name} (${customer.id})`,
);
logger.info(`Redemption ID: ${redemption.id}`);
}

View File

@@ -119,7 +119,11 @@ export const handleProductsUpdated = async ({
if (!req) {
logger.warn("products.updated, no req object found, skipping", {
data,
...data,
org: {
id: org.id,
slug: org.slug,
},
});
return;
}

View File

@@ -120,6 +120,8 @@ export const handleNewScheduleForItemSet = async ({
let otherSub = otherSubObj?.otherSub || null;
let otherSubItems = otherSubObj?.otherSubItems || [];
// console.log("Interval:", itemSet.interval, "Other sub: ", otherSub?.id);
// 2. Add to item set to create new schedule
itemSet.items.push(
...otherSubItems.map((sub: any) => ({

View File

@@ -8,7 +8,6 @@ import { ProrationBehavior } from "@autumn/shared";
import { attachParamsToProduct } from "./convertAttachParams.js";
import { attachParamToCusProducts } from "./convertAttachParams.js";
import { cusProductToPrices } from "../../cusProducts/cusProductUtils/convertCusProduct.js";
import { insertInvoiceFromAttach } from "@/internal/invoices/invoiceUtils.js";
export const intervalsAreSame = ({
attachParams,
@@ -85,8 +84,10 @@ export const getAttachConfig = async ({
branch == AttachBranch.MainIsTrial ||
org.config.merge_billing_cycles === false;
const onlyCheckout =
isPublic || forceCheckout || (noPaymentMethod && !invoiceOnly && !isFree);
const checkoutFlow =
isPublic || forceCheckout || (noPaymentMethod && !invoiceOnly);
const onlyCheckout = !isFree && checkoutFlow;
let config: AttachConfig = {
branch,

View File

@@ -3,12 +3,7 @@ import {
AttachParams,
AttachResultSchema,
} from "../../cusProducts/AttachParams.js";
import {
AttachBranch,
AttachFunction,
CusProductStatus,
ProrationBehavior,
} from "@autumn/shared";
import { AttachBranch, AttachFunction, CusProductStatus } from "@autumn/shared";
import { handleUpgradeDiffInterval } from "../attachFunctions/upgradeDiffIntFlow/handleUpgradeDiffInt.js";
import { handleCreateCheckout } from "../../add-product/handleCreateCheckout.js";
import { handleAddProduct } from "../attachFunctions/addProductFlow/handleAddProduct.js";

View File

@@ -185,7 +185,7 @@ export const handleAttachErrors = async ({
}) => {
const { onlyCheckout } = config;
if (onlyCheckout) {
if (onlyCheckout || flags.isPublic) {
let upgradeDowngradeFlows = [
AttachBranch.Upgrade,
AttachBranch.Downgrade,

View File

@@ -178,10 +178,6 @@ export class RewardRedemptionService {
rewardPrograms.internal_id,
),
)
.innerJoin(
rewards,
eq(rewards.internal_id, rewardPrograms.internal_reward_id),
)
.where(
and(
eq(referralCodes.internal_customer_id, internalCustomerId),
@@ -197,7 +193,7 @@ export class RewardRedemptionService {
referral_code: d.referral_codes,
reward_program: {
...d.reward_programs,
reward: d.rewards,
// reward: d.rewards,
},
}));

View File

@@ -123,3 +123,16 @@ export const constructArrearProratedItem = ({
return item;
};
export const constructFixedPrice = ({
price,
interval = ProductItemInterval.Month,
}: {
price: number;
interval?: ProductItemInterval;
}) => {
return {
price,
interval,
};
};

View File

@@ -113,7 +113,7 @@ export const initCustomer = async ({
org,
env,
db,
testClockId,
testClockId: testClockId || undefined,
});
}

View File

@@ -199,34 +199,34 @@ describe(`${chalk.yellowBright(
assert.equal(invoices[0].total, 0);
});
it("customer should have discount for second purchase", async function () {
// 2. Check that customer has another discount
let stripeCus = (await stripeCli.customers.retrieve(
mainCustomer.processor?.id,
)) as Stripe.Customer;
// it("customer should have discount for second purchase", async function () {
// // 2. Check that customer has another discount
// let stripeCus = (await stripeCli.customers.retrieve(
// mainCustomer.processor?.id,
// )) as Stripe.Customer;
assert.notEqual(stripeCus.discount, null);
// assert.notEqual(stripeCus.discount, null);
// 2. Advance test clock to 1 month from start (trigger discount.deleted event)
curTime = addHours(addMonths(new Date(), 1), 2);
await advanceTestClock({
testClockId,
advanceTo: curTime.getTime(),
stripeCli,
});
// // 2. Advance test clock to 1 month from start (trigger discount.deleted event)
// curTime = addHours(addMonths(new Date(), 1), 2);
// await advanceTestClock({
// testClockId,
// advanceTo: curTime.getTime(),
// stripeCli,
// });
// 3. Advance test clock to 1 month + 12 days from start (trigger new invoice)
curTime = addDays(curTime, 12);
await advanceTestClock({
testClockId,
advanceTo: curTime.getTime(),
stripeCli,
});
// // 3. Advance test clock to 1 month + 12 days from start (trigger new invoice)
// curTime = addDays(curTime, 12);
// await advanceTestClock({
// testClockId,
// advanceTo: curTime.getTime(),
// stripeCli,
// });
// // 3. Get invoice again
let { invoices: invoices2 } = await autumn.customers.get(mainCustomerId);
// // // 3. Get invoice again
// let { invoices: invoices2 } = await autumn.customers.get(mainCustomerId);
assert.equal(invoices2.length, 3);
assert.equal(invoices2[0].total, 0);
});
// assert.equal(invoices2.length, 3);
// assert.equal(invoices2[0].total, 0);
// });
});

View File

@@ -137,34 +137,34 @@ describe(`${chalk.yellowBright(
assert.equal(invoices![0].total, 0);
});
it("customer should have discount for second purchase", async function () {
// 2. Check that customer has another discount
let stripeCus = (await stripeCli.customers.retrieve(
mainCustomer.processor?.id,
)) as Stripe.Customer;
// it("customer should have discount for second purchase", async function () {
// // 2. Check that customer has another discount
// let stripeCus = (await stripeCli.customers.retrieve(
// mainCustomer.processor?.id,
// )) as Stripe.Customer;
assert.notEqual(stripeCus.discount, null);
// assert.notEqual(stripeCus.discount, null);
// 2. Advance test clock to 1 month from start (trigger discount.deleted event)
curTime = addHours(addMonths(new Date(), 1), 2);
await advanceTestClock({
testClockId,
advanceTo: curTime.getTime(),
stripeCli,
});
// // 2. Advance test clock to 1 month from start (trigger discount.deleted event)
// curTime = addHours(addMonths(new Date(), 1), 2);
// await advanceTestClock({
// testClockId,
// advanceTo: curTime.getTime(),
// stripeCli,
// });
// 3. Advance test clock to 1 month + 7 days from start (trigger new invoice)
curTime = addDays(curTime, 8);
await advanceTestClock({
testClockId,
advanceTo: curTime.getTime(),
stripeCli,
});
// // 3. Advance test clock to 1 month + 7 days from start (trigger new invoice)
// curTime = addDays(curTime, 8);
// await advanceTestClock({
// testClockId,
// advanceTo: curTime.getTime(),
// stripeCli,
// });
// // 3. Get invoice again
let { invoices: invoices2 } = await autumn.customers.get(mainCustomerId);
// // // 3. Get invoice again
// let { invoices: invoices2 } = await autumn.customers.get(mainCustomerId);
assert.equal(invoices2!.length, 3);
assert.equal(invoices2![0].total, 0);
});
// assert.equal(invoices2!.length, 3);
// assert.equal(invoices2![0].total, 0);
// });
});

View File

@@ -1,26 +1,43 @@
import chalk from "chalk";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import {
constructFeatureItem,
constructFixedPrice,
} from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { assert, expect } from "chai";
import chalk from "chalk";
import { setupBefore } from "tests/before.js";
import { AutumnCli } from "tests/cli/AutumnCli.js";
import { features, products } from "tests/global.js";
import { compareMainProduct } from "tests/utils/compare.js";
import { completeCheckoutForm } from "tests/utils/stripeUtils.js";
import { timeout } from "tests/utils/genUtils.js";
import { createProducts } from "tests/utils/productUtils.js";
import { addPrefixToProducts } from "../utils.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { APIVersion } from "@autumn/shared";
const oneTimeQuantity = 2;
const oneTimePurchaseCount = 2;
const oneTimeOverrideQuantity = 4;
const monthlyQuantity = 2;
const freeProd = constructProduct({
type: "free",
isDefault: false,
items: [
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 1000,
}),
// constructFixedPrice({
// price: 0,
// }),
],
});
// UNCOMMENT FROM HERE
const testCase = "basic1";
describe(`${chalk.yellowBright(
"basic1: Testing attach -- free, pro & one-time / monthly add on",
)}`, () => {
describe(`${chalk.yellowBright("basic1: Testing attach free product")}`, () => {
let customerId = testCase;
let autumn: AutumnInt = new AutumnInt();
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_2 });
let db, org, env;
before(async function () {
@@ -36,11 +53,25 @@ describe(`${chalk.yellowBright(
org,
env,
fingerprint: "test",
withTestClock: false,
});
addPrefixToProducts({
products: [freeProd],
prefix: testCase,
});
await createProducts({
db,
orgId: org.id,
env,
autumn,
products: [freeProd],
});
});
it("should create customer and have default free active", async function () {
const data = await autumn.customers.get(customerId);
const data = await AutumnCli.getCustomer(customerId);
compareMainProduct({
sent: products.free,
@@ -49,13 +80,12 @@ describe(`${chalk.yellowBright(
});
it("should have correct entitlements", async function () {
const data = await autumn.customers.get(customerId);
const expectedEntitlement = products.free.entitlements.metered1;
const entitled = (await autumn.check({
customer_id: customerId,
feature_id: features.metered1.id,
})) as any;
const entitled = (await AutumnCli.entitled(
customerId,
features.metered1.id,
)) as any;
const metered1Balance = entitled.balances.find(
(balance: any) => balance.feature_id === features.metered1.id,
@@ -71,262 +101,24 @@ describe(`${chalk.yellowBright(
const entitled = await AutumnCli.entitled(customerId, features.boolean1.id);
expect(entitled!.allowed).to.be.false;
});
it("should attach free (with $0 price) and force checkout and succeed", async function () {
await autumn.attach({
customer_id: customerId,
product_id: freeProd.id,
force_checkout: true,
});
const customer = await autumn.customers.get(customerId);
expectProductAttached({
customer,
product: freeProd,
});
expectFeaturesCorrect({
customer,
product: freeProd,
});
});
});
// describe("Attach pro -- check products & entitlements", () => {
// it("POST /attach -- attaching pro (force checkout)", async function () {
// const { checkout_url } = await AutumnCli.attach({
// customerId: customerId,
// productId: products.pro.id,
// });
// await completeCheckoutForm(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,
// });
// expect(res.invoices.length).to.be.greaterThan(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;
// }
// });
// });

View File

@@ -38,7 +38,12 @@ let pro = constructProduct({
type: "pro",
});
describe(`${chalk.yellowBright(`${testCase}: Testing downgrade: premium -> pro -> free -> pro -> premium`)}`, () => {
let premium = constructProduct({
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "premium",
});
describe(`${chalk.yellowBright(`${testCase}: Testing downgrade: pro-quarter -> premium -> pro`)}`, () => {
let customerId = testCase;
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
let customer: Customer;
@@ -58,13 +63,13 @@ describe(`${chalk.yellowBright(`${testCase}: Testing downgrade: premium -> pro -
stripeCli = this.stripeCli;
addPrefixToProducts({
products: [proQuarter, pro],
products: [proQuarter, pro, premium],
prefix: testCase,
});
await createProducts({
autumn,
products: [proQuarter, pro],
products: [proQuarter, pro, premium],
customerId,
db,
orgId: org.id,
@@ -97,6 +102,19 @@ describe(`${chalk.yellowBright(`${testCase}: Testing downgrade: premium -> pro -
});
});
it("should downgrade to premium", async function () {
await expectDowngradeCorrect({
autumn,
customerId,
curProduct: proQuarter,
newProduct: premium,
stripeCli,
db,
org,
env,
});
});
let preview = null;
it("should downgrade to pro", async function () {

View File

@@ -16,9 +16,7 @@ import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
const testCase = "downgrade5";
describe(`${chalk.yellowBright(
"downgrade5: testing basic downgrade (paid to paid)",
)}`, () => {
describe(`${chalk.yellowBright(`${testCase}: testing basic downgrade (paid to paid)`)}`, () => {
let customerId = testCase;
let autumn: AutumnInt = new AutumnInt();
let testClockId: string;

View File

@@ -9,7 +9,7 @@ import { setupBefore } from "tests/before.js";
import { getMainCusProduct } from "@/internal/customers/cusProducts/cusProductUtils.js";
const testCase = "downgrade6";
describe(`${chalk.yellowBright("downgrade6: testing expire button")}`, () => {
describe(`${chalk.yellowBright(`${testCase}: testing expire button`)}`, () => {
let customerId = testCase;
let testClockId: string;
let autumn: AutumnInt = new AutumnInt();

View File

@@ -1,17 +1,16 @@
import chalk from "chalk";
import { Customer } from "@autumn/shared";
import { AutumnCli } from "tests/cli/AutumnCli.js";
import { products } from "tests/global.js";
import chalk from "chalk";
import { compareMainProduct } from "tests/utils/compare.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { setupBefore } from "tests/before.js";
import { findCusProductById } from "@/internal/customers/cusProducts/cusProductUtils/findCusProduct.js";
import { expect } from "chai";
import { getSubsFromCusId } from "tests/utils/expectUtils/expectSubUtils.js";
const testCase = "downgrade7";
describe(`${chalk.yellowBright("downgrade7: testing expire scheduled product")}`, () => {
describe(`${chalk.yellowBright(`${testCase}: testing expire scheduled product`)}`, () => {
let customerId = testCase;
let testClockId: string;
let customer: Customer;

View File

@@ -1,6 +1,4 @@
import { createStripeCli } from "@/external/stripe/utils.js";
import chalk from "chalk";
import Stripe from "stripe";
import { AutumnCli } from "tests/cli/AutumnCli.js";
import { advanceProducts } from "tests/global.js";
import {

View File

@@ -0,0 +1,133 @@
import { expect } from "chai";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { APIVersion, 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 { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { expectAttachCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { CusService } from "@/internal/customers/CusService.js";
export let pro = constructProduct({
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "pro",
});
const testCase = "others6";
describe(`${chalk.yellowBright(`${testCase}: Testing attach with customer ID and entity ID null`)}`, () => {
let customerId = testCase;
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let stripeCli: Stripe;
const email = `${customerId}@test.com`;
before(async function () {
await setupBefore(this);
const { autumnJs } = this;
db = this.db;
org = this.org;
env = this.env;
stripeCli = this.stripeCli;
let customer = await CusService.getByEmail({
db,
orgId: org.id,
env,
email,
});
if (customer.length > 0) {
await autumn.customers.delete(customer[0].internal_id);
}
addPrefixToProducts({
products: [pro],
prefix: testCase,
});
await createProducts({
db,
orgId: org.id,
env,
autumn,
products: [pro],
});
});
let internalCustomerId = "";
let internalEntityId = "";
let entityId = "1";
it("should attach create customer with no ID", async function () {
let customer = await autumn.customers.create({
// @ts-ignore
id: null,
email: `${customerId}@test.com`,
name: customerId,
});
expect(customer.autumn_id, "Customer ID should exist").to.exist;
internalCustomerId = customer.autumn_id;
let data = await autumn.entities.create(internalCustomerId, {
// @ts-ignore
id: null,
feature_id: TestFeature.Users,
});
internalEntityId = data.autumn_id;
expect(internalEntityId, "Entity ID should exist").to.exist;
});
it("should be able to attach pro product, invoice only", async function () {
await autumn.attach({
customer_id: internalCustomerId,
entity_id: internalEntityId,
product_id: pro.id,
invoice_only: true,
});
const customer = await autumn.customers.get(internalCustomerId);
expectAttachCorrect({
customer,
product: pro,
});
expect(customer.invoices.length).to.equal(1);
expect(customer.invoices[0].status).to.equal("draft");
});
it("should create customer with ID, and attach pro product", async function () {
let customer = await autumn.customers.create({
id: customerId,
email: `${customerId}@test.com`,
});
expect(customer.autumn_id).to.equal(internalCustomerId);
let entity = await autumn.entities.create(customer.autumn_id, {
id: entityId,
feature_id: TestFeature.Users,
});
internalEntityId = entity.autumn_id;
const customer2 = await autumn.customers.get(customerId);
expectAttachCorrect({
customer: customer2,
product: pro,
entityId,
});
});
});

View File

@@ -0,0 +1,80 @@
import chalk from "chalk";
import Stripe from "stripe";
import { expect } from "chai";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { APIVersion, AppEnv, Organization } from "@autumn/shared";
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 { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { expectAttachCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js";
export let pro = constructProduct({
items: [constructArrearItem({ featureId: TestFeature.Words })],
type: "pro",
trial: true,
});
const testCase = "others7";
describe(`${chalk.yellowBright(`${testCase}: Testing attach with free_trial=False`)}`, () => {
let customerId = testCase;
let autumn: AutumnInt = new AutumnInt({ version: APIVersion.v1_4 });
let db: DrizzleCli, org: Organization, env: AppEnv;
let stripeCli: Stripe;
before(async function () {
await setupBefore(this);
db = this.db;
org = this.org;
env = this.env;
stripeCli = this.stripeCli;
await initCustomer({
db,
org,
env,
autumn: this.autumnJs,
customerId,
fingerprint: "test",
attachPm: "success",
});
addPrefixToProducts({
products: [pro],
prefix: testCase,
});
await createProducts({
db,
orgId: org.id,
env,
autumn,
products: [pro],
});
});
it("should attach pro product with free_trial=False", async function () {
await autumn.attach({
customer_id: customerId,
product_id: pro.id,
free_trial: false,
});
const customer = await autumn.customers.get(customerId);
expectAttachCorrect({
customer,
product: pro,
});
expect(customer.invoices.length).to.equal(1);
expect(customer.invoices[0].total).to.equal(getBasePrice({ product: pro }));
});
});

View File

View File

@@ -7,7 +7,6 @@ import {
CreateEntity,
LimitedItem,
Organization,
ProductItem,
} from "@autumn/shared";
import { TestFeature } from "tests/setup/v2Features.js";
@@ -21,7 +20,6 @@ import { createProducts } from "tests/utils/productUtils.js";
import { addPrefixToProducts, runAttachTest } from "../../attach/utils.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { Decimal } from "decimal.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
import { addHours, addMonths } from "date-fns";
@@ -37,13 +35,6 @@ let userMessages = constructArrearItem({
entityFeatureId: user,
}) as LimitedItem;
// let adminMessages = constructArrearItem({
// featureId: TestFeature.Messages,
// includedUsage: 0,
// price: 0.1,
// entityFeatureId: admin,
// }) as LimitedItem;
export let pro = constructProduct({
items: [userMessages],
type: "pro",

View File

@@ -87,17 +87,17 @@ describe(`${chalk.yellowBright(`attach/entities/${testCase}: Testing update cont
{
id: "1",
name: "test",
featureId: TestFeature.Users,
feature_id: TestFeature.Users,
},
{
id: "2",
name: "test2",
featureId: TestFeature.Users,
feature_id: TestFeature.Users,
},
{
id: "3",
name: "test3",
featureId: TestFeature.Users,
feature_id: TestFeature.Users,
},
];
@@ -167,12 +167,12 @@ describe(`${chalk.yellowBright(`attach/entities/${testCase}: Testing update cont
{
id: "4",
name: "test4",
featureId: TestFeature.Users,
feature_id: TestFeature.Users,
},
{
id: "5",
name: "test5",
featureId: TestFeature.Users,
feature_id: TestFeature.Users,
},
];

View File

@@ -87,17 +87,17 @@ describe(`${chalk.yellowBright(`contUse/update/${testCase}: Testing update cont
{
id: "1",
name: "test",
featureId: TestFeature.Users,
feature_id: TestFeature.Users,
},
{
id: "2",
name: "test2",
featureId: TestFeature.Users,
feature_id: TestFeature.Users,
},
{
id: "3",
name: "test3",
featureId: TestFeature.Users,
feature_id: TestFeature.Users,
},
];

View File

@@ -88,12 +88,12 @@ describe(`${chalk.yellowBright(`contUse/${testCase}: Testing update contUse incl
{
id: "1",
name: "entity1",
featureId: TestFeature.Users,
feature_id: TestFeature.Users,
},
{
id: "2",
name: "entity2",
featureId: TestFeature.Users,
feature_id: TestFeature.Users,
},
];

View File

@@ -99,17 +99,17 @@ describe(`${chalk.yellowBright(`contUse/${testCase}: Testing update contUse incl
{
id: "1",
name: "entity1",
featureId: TestFeature.Users,
feature_id: TestFeature.Users,
},
{
id: "2",
name: "entity2",
featureId: TestFeature.Users,
feature_id: TestFeature.Users,
},
{
id: "3",
name: "entity3",
featureId: TestFeature.Users,
feature_id: TestFeature.Users,
},
];

View File

@@ -18,6 +18,7 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { expect } from "chai";
import { completeCheckoutForm } from "../stripeUtils.js";
import { Customer } from "autumn-js";
export const attachAndExpectCorrect = async ({
autumn,
@@ -158,3 +159,24 @@ export const attachAndExpectCorrect = async ({
);
}
};
export const expectAttachCorrect = async ({
customer,
product,
entityId,
}: {
customer: Customer;
product: ProductV2;
entityId?: string;
}) => {
expectProductAttached({
customer,
product,
entityId,
});
expectFeaturesCorrect({
customer,
product,
});
};

View File

@@ -71,7 +71,7 @@ export const getExpectedInvoiceTotal = async ({
const usageAmount = usage.find(
(u) =>
u.featureId == featureId &&
(u.featureId ? u.entityFeatureId == ent.entity_feature_id : true),
(u.entityFeatureId ? u.entityFeatureId == ent.entity_feature_id : true),
)?.value;
const overage =

View File

@@ -2,8 +2,6 @@ import { config } from "dotenv";
config({ path: "../server/.env" });
import { defineConfig } from "drizzle-kit";
console.log("DATABASE_URL", process.env.DATABASE_URL);
export default defineConfig({
dialect: "postgresql",
out: "./drizzle",

View File

@@ -21,6 +21,8 @@
},
"dependencies": {
"decimal.js": "^10.5.0",
"dotenv": "^16.5.0",
"drizzle-kit": "^0.31.1",
"drizzle-orm": "^0.43.1",
"drizzle-zod": "^0.8.2",
"zod": "^3.25.23"

View File

@@ -25,11 +25,6 @@ export const CreateEntity = ({
const cusContext = useCustomerContext();
const navigate = useNavigate();
if (!cusContext) {
return null;
}
const { customer, cusMutate } = cusContext;
const [isLoading, setIsLoading] = useState(false);
const [entity, setEntity] = useState<any>({
id: "",
@@ -39,6 +34,11 @@ export const CreateEntity = ({
const env = useEnv();
const axiosInstance = useAxiosInstance({ env });
if (!cusContext) {
return null;
}
const { customer, cusMutate } = cusContext;
const handleCreateClicked = async () => {
setIsLoading(true);
try {
@@ -51,7 +51,7 @@ export const CreateEntity = ({
name: entity.name || null,
feature_id: entity.feature_id,
customer_id: customer.id,
}
},
);
await cusMutate();

View File

@@ -9,7 +9,7 @@ import {
import { Entity } from "@autumn/shared";
import { useLocation, useNavigate } from "react-router";
import { CreateEntity } from "./create-entity/create-entity";
import { CreateEntity } from "./create-entity/CreateEntity";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { PlusIcon } from "lucide-react";
@@ -46,7 +46,8 @@ export const SelectEntity = ({
}
const entity = entities.find(
(entity: Entity) => entity.id === entityId || entity.internal_id == entityId
(entity: Entity) =>
entity.id === entityId || entity.internal_id == entityId,
);
return (

View File

@@ -132,7 +132,7 @@ export const AttachModal = ({
const redirectUrl = getRedirectUrl(`/customers/${cusId}`, env);
const attachBody = getAttachBody({
customerId: customer.id,
customerId: customer.id || customer.internal_id,
entityId,
product,
optionsInput: options,

View File

@@ -1,369 +0,0 @@
"use client";
import axios from "axios";
import { toast } from "sonner";
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { LoaderCircle } from "lucide-react";
import { useDemoSWR } from "@/services/useAxiosSwr";
import CustomerBalances from "./CustomerBalances";
import { Input } from "@/components/ui/input";
import { APIPlayground } from "./APIPlayground";
const endpoint = "https://api.useautumn.com";
// const endpoint = "http://localhost:8080";
export default function MintDemo({
publishableKey,
secretKey,
}: {
publishableKey: string;
secretKey: string;
}) {
const [eventName, setEventName] = useState("chat-responses");
const customerId = "hahnbee";
const axiosInstance = axios.create({
baseURL: endpoint + "/v1",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${secretKey}`,
},
});
const {
data: customer,
error,
isLoading: customerLoading,
mutate: cusMutate,
} = useDemoSWR({
url: `/public/customers/${customerId}`,
publishableKey: publishableKey || "",
});
const hasAccessRequest = {
customer_id: customerId,
feature_id: eventName,
};
const sendEventRequestChat = {
customer_id: customerId,
event_name: "chat-responses",
};
const sendEventRequestEditorsPlus = {
customer_id: customerId,
event_name: "editors",
properties: {
value: 1,
},
};
const sendEventRequestEditorsMinus = {
customer_id: customerId,
event_name: "editors",
properties: {
value: -1,
},
};
const getCustomerRequest = {
customer_id: customerId,
};
const [hasAccessLoading, setHasAccessLoading] = useState(false);
const [hasAccessResponse, setHasAccessResponse] = useState(null);
const [sendEventRequest, setSendEventRequest] =
useState(sendEventRequestChat);
const [getCustomerResponse, setGetCustomerResponse] = useState(null);
const [sendEventLoading, setSendEventLoading] = useState(false);
const [sendEventResponse, setSendEventResponse] = useState(null);
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState("");
useEffect(() => {
setGetCustomerResponse(customer?.entitlements);
}, [customer]);
//Check access to Pro features and email balance
const checkAccess = async (featureId: string) => {
const { data } = await axiosInstance.post("/entitled", {
customer_id: customerId,
feature_id: featureId,
});
return data;
};
//Send usage event for
const sendUsage = async (featureId: string) => {
const { data } = await axiosInstance.post("/events", {
customer_id: customerId,
event_name: featureId,
properties: {
value: 1,
},
});
return data;
};
const handleClicked = async () => {
setHasAccessLoading(true);
const data = await checkAccess("chat-responses");
setHasAccessResponse(data);
await setSendEventRequest(sendEventRequestChat);
setHasAccessLoading(false);
if (!data.allowed) {
toast.error("You're out of AI chat responses");
return;
}
setSendEventLoading(true);
const eventData = await sendUsage("chat-responses");
setSendEventResponse(eventData);
setSendEventLoading(false);
await cusMutate();
};
return (
<div className="w-full h-fit bg-white flex absolute top-0 left-0 flex-nowrap">
<div className="flex w-full gap-32 relative">
<div className="w-[150px] bg-stone-100 border-r flex flex-col h-screen fixed left-0 top-0">
<div className="p-4 flex items-center gap-2 border-b">
<div className="w-6 h-6 rounded-full bg-gray-900"></div>
<span className="font-medium">autumn</span>
</div>
<div className="flex flex-col h-full p-2 space-y-1">
<SidebarItem icon="📊" text="Overview" active />
<SidebarItem icon="📝" text="Editor" />
<SidebarItem icon="📈" text="Analytics" />
<SidebarItem icon="⚙️" text="Settings" />
<div className="flex flex-col h-full justify-between">
<div>
<div className="text-xs text-gray-500 px-3 pt-4 pb-2">
Products
</div>
<SidebarItem icon="💬" text="Chat" />
<SidebarItem icon="🤖" text="Assistant" />
<SidebarItem icon="🔒" text="Authentication" />
<SidebarItem icon="🧩" text="Add-ons" />
</div>
<div className="">
<SidebarItem icon="📚" text="Documentation" />
<SidebarItem icon="👥" text="Invite Members" />
<SidebarItem icon="❓" text="Support" />
</div>
</div>
</div>
</div>
<div className="flex flex-col gap-4 flex-2 w-full ml-60">
{loading ? (
<div className="flex justify-center items-center h-[500px]">
<LoaderCircle className="animate-spin text-primary" size={30} />
</div>
) : (
<>
<div className="flex gap-4 items-center">
<div className="text-xl font-extrabold mt-6 -mb-2">
Mintlify
</div>
</div>
<p className="text-lg">
Start building modern documentation in under five minutes
</p>
<div className="flex gap-2">
<Input
placeholder="Search or ask..."
className="w-full"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<Button
isLoading={hasAccessLoading}
onClick={async () => handleClicked()}
className="font-semibold bg-gradient-to-b from-emerald-400 to-emerald-500 hover:bg-gradient-to-b hover:from-emerald-500 hover:to-emerald-600 border-t border-emerald-500"
>
Use AI Chat
</Button>
<Button
variant="gradientSecondary"
onClick={async () => {
setEventName("editors");
setSendEventRequest(sendEventRequestEditorsPlus);
const { data } = await axiosInstance.post("/entitled", {
customer_id: customerId,
feature_id: "editors",
event_data: {
event_name: "editors",
properties: {
value: 1,
},
},
});
setHasAccessResponse(data);
if (!data.allowed) {
toast.error("You're out of editors");
}
await cusMutate();
}}
>
Add Editor
</Button>
<Button
variant="gradientSecondary"
onClick={async () => {
setEventName("editors");
setSendEventRequest(sendEventRequestEditorsMinus);
const { data } = await axiosInstance.post("/entitled", {
customer_id: customerId,
feature_id: "editors",
required_quantity: -1,
event_data: {
event_name: "editors",
properties: {
value: -1,
},
},
});
setHasAccessResponse(data);
await cusMutate();
}}
>
Remove Editor
</Button>
{/* <Button
variant="gradientSecondary"
onClick={async () => {
const { data } = await axiosInstance.get(
`/customers/${customerId}/billing_portal`
);
window.open(data.url, "_blank");
}}
>
Manage Subscription
</Button> */}
</div>
<div className="py-6 border-b border-t h-fit">
<h1 className="text-lg font-medium">Good evening, Autumn</h1>
<p className="text-t3">
Welcome back to your documentation portal
</p>
<div className="flex gap-6 mt-6 rounded-lg pb-4 pt-2 h-fit">
{/* <Image
src="/demo-assets/Dashboard.png"
alt="Dashboard Preview"
width={600}
height={375}
className="w-[300px] h-[200px] rounded-md border shadow-sm"
/> */}
<div className="mt-4 flex flex-col h-[170px] justify-between">
<div className="flex flex-col">
<div className="px-2 py-1 rounded-full w-fit bg-green-100 text-green-600 text-sm mb-2">
Live
</div>
<span className="text-sm text-gray-600">
Last updated 1 week ago by Ayush
</span>
</div>
<div className="flex items-center gap-2">
<button className="p-2 hover:bg-gray-100 rounded">
<span className="text-gray-600">📋</span>
</button>
<button className="p-2 hover:bg-gray-100 rounded">
<span className="text-gray-600">🔄</span>
</button>
<button
className="px-3 py-1 bg-black text-white rounded-md text-sm min-w-fit"
onClick={() => {
window.open("https://docs.useautumn.com", "_blank");
}}
>
Visit docs
</button>
</div>
</div>
</div>
</div>
<div className="text-lg font-medium mt-4 -mb-3">Billing</div>
<p className="text-sm text-t3">
{customer?.name ? customer?.name : customerId}, you have access
to:
</p>
<CustomerBalances customer={customer} />
<div className="text-lg font-medium mt-2">Pricing</div>
<div className="max-w-[600px]"></div>
<p className="text-xs text-t3">
Make a test purchase to see how Autumn handles it. Use the
Stripe test card{" "}
<span className="font-bold">4242 4242 4242 4242</span> with any
expiration date, CVC and cardholder details.
</p>
</>
)}
</div>
<div className="w-full space-y-4 flex flex-col gap-4 max-w-[400px] bg-gray-900 p-4 rounded-sm">
{/* <Image
src="/demo-assets/autumn-logo.png"
alt="Autumn Logo"
width={100}
height={100}
/> */}
<APIPlayground
title="Check Feature Access"
endpoint="GET /entitled"
request={hasAccessRequest}
response={hasAccessResponse}
loading={hasAccessLoading}
/>
<APIPlayground
title="Send Usage Event"
endpoint="POST /events"
request={sendEventRequest}
response={sendEventResponse}
loading={sendEventLoading}
/>
<APIPlayground
title="Get Customer"
endpoint="GET /customers/:customer_id"
request={getCustomerRequest}
response={getCustomerResponse}
loading={customerLoading}
/>
</div>
</div>
</div>
);
}
function SidebarItem({
icon,
text,
active = false,
}: {
icon: string;
text: string;
active?: boolean;
}) {
return (
<div
className={`flex items-center gap-2 px-3 py-2 rounded-md cursor-pointer hover:bg-gray-100 ${
active ? "bg-gray-100" : ""
}`}
>
<span>{icon}</span>
<span className="text-sm">{text}</span>
</div>
);
}