fix: added tests for attach and fixed contUse updates
This commit is contained in:
@@ -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
6
pnpm-lock.yaml
generated
@@ -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
6
run.sh
@@ -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
|
||||
|
||||
@@ -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'
|
||||
@@ -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'
|
||||
|
||||
@@ -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 };
|
||||
|
||||
10
server/src/external/autumn/autumnCli.ts
vendored
10
server/src/external/autumn/autumnCli.ts
vendored
@@ -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;
|
||||
|
||||
4
server/src/external/stripe/stripeSubUtils.ts
vendored
4
server/src/external/stripe/stripeSubUtils.ts
vendored
@@ -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));
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -185,7 +185,7 @@ export const handleAttachErrors = async ({
|
||||
}) => {
|
||||
const { onlyCheckout } = config;
|
||||
|
||||
if (onlyCheckout) {
|
||||
if (onlyCheckout || flags.isPublic) {
|
||||
let upgradeDowngradeFlows = [
|
||||
AttachBranch.Upgrade,
|
||||
AttachBranch.Downgrade,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
@@ -123,3 +123,16 @@ export const constructArrearProratedItem = ({
|
||||
|
||||
return item;
|
||||
};
|
||||
|
||||
export const constructFixedPrice = ({
|
||||
price,
|
||||
interval = ProductItemInterval.Month,
|
||||
}: {
|
||||
price: number;
|
||||
interval?: ProductItemInterval;
|
||||
}) => {
|
||||
return {
|
||||
price,
|
||||
interval,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -113,7 +113,7 @@ export const initCustomer = async ({
|
||||
org,
|
||||
env,
|
||||
db,
|
||||
testClockId,
|
||||
testClockId: testClockId || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
// });
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
// });
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
133
server/tests/attach/others/others6.ts
Normal file
133
server/tests/attach/others/others6.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
80
server/tests/attach/others/others7.ts
Normal file
80
server/tests/attach/others/others7.ts
Normal 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 }));
|
||||
});
|
||||
});
|
||||
0
server/tests/attach/others/others8.ts
Normal file
0
server/tests/attach/others/others8.ts
Normal 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",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
@@ -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 (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user