added chatResult handling and updated onboarding flow

This commit is contained in:
John Yeo
2025-05-23 16:14:50 +01:00
109 changed files with 1838 additions and 3015 deletions

1430
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +1,18 @@
import dotenv from "dotenv";
dotenv.config();
import postgres from "postgres";
import { drizzle } from "drizzle-orm/postgres-js";
import {
customers,
chatResults,
organizations,
apiKeys,
rewards,
} from "./schema/index.js";
import * as relations from "./relations.js";
const client = postgres(process.env.DATABASE_URL!);
import { schemas } from "@autumn/shared";
export const initDrizzle = () => {
const client = postgres(process.env.DATABASE_URL!);
const db = drizzle(client, {
schema: {
customers,
chatResults,
organizations,
apiKeys,
rewards,
...relations,
},
schema: schemas,
});
return db;
return { db, client };
};
export type DrizzleCli = ReturnType<typeof initDrizzle>;
export type DrizzleCli = ReturnType<typeof initDrizzle>["db"];

View File

@@ -1,21 +0,0 @@
import { customers } from "./tables/cusTable.js";
import { chatResults } from "./tables/chatResultsTable.js";
// import { organizations } from "./tables/orgTable.js";
import { apiKeys } from "./tables/apiKeysTable.js";
import { entitlements } from "./tables/entitlementsTable.js";
import { features } from "./tables/featuresTable.js";
import { products } from "./tables/productsTable.js";
import { prices } from "./tables/pricesTable.js";
import { organizations } from "./tables/orgTable.js";
export {
customers,
chatResults,
organizations,
apiKeys,
entitlements,
features,
products,
prices,
};
export * from "./tables/allTables.js";

View File

@@ -1,35 +0,0 @@
import { relations } from "drizzle-orm";
import { features } from "./tables/featuresTable.js";
import { products } from "./tables/productsTable.js";
import { organizations } from "./tables/orgTable.js";
import { apiKeys } from "./tables/apiKeysTable.js";
import { entitlements } from "./tables/entitlementsTable.js";
import { prices } from "./tables/pricesTable.js";
export const organizationsRelations = relations(organizations, ({ many }) => ({
apiKeys: many(apiKeys),
features: many(features),
}));
export const featuresRelations = relations(features, ({ one }) => ({
org: one(organizations, {
fields: [features.org_id],
references: [organizations.id],
}),
}));
export const productsRelations = relations(products, ({ one, many }) => ({
org: one(organizations, {
fields: [products.org_id],
references: [organizations.id],
}),
entitlements: many(entitlements),
prices: many(prices),
}));
export const entitlementsRelations = relations(entitlements, ({ one }) => ({
feature: one(features, {
fields: [entitlements.internal_feature_id],
references: [features.internal_id],
}),
}));

View File

@@ -1,496 +0,0 @@
import {
pgTable,
text,
numeric,
jsonb,
boolean,
foreignKey,
unique,
primaryKey,
} from "drizzle-orm/pg-core";
import { organizations } from "../index.js";
import { features } from "./featuresTable.js";
import { prices } from "../index.js";
export const rewards = pgTable(
"rewards",
{
internalId: text("internal_id").primaryKey().notNull(),
env: text(),
name: text(),
orgId: text("org_id"),
createdAt: numeric("created_at"),
discountConfig: jsonb("discount_config"),
freeProductId: text("free_product_id"),
id: text(),
promoCodes: jsonb("promo_codes").array(),
type: text(),
},
(table) => [
foreignKey({
columns: [table.orgId],
foreignColumns: [organizations.id],
name: "coupons_org_id_fkey",
}).onDelete("cascade"),
],
);
export const rewardPrograms = pgTable(
"reward_programs",
{
internalId: text("internal_id").primaryKey().notNull(),
id: text(),
createdAt: numeric("created_at").notNull(),
internalRewardId: text("internal_reward_id"),
maxRedemptions: numeric("max_redemptions"),
unlimitedRedemptions: boolean("unlimited_redemptions").default(false),
orgId: text("org_id"),
env: text(),
when: text().default("immediately"),
productIds: text("product_ids").array().default([""]),
excludeTrial: boolean("exclude_trial").default(false),
receivedBy: text("received_by"),
},
(table) => [
foreignKey({
columns: [table.internalRewardId],
foreignColumns: [rewards.internalId],
name: "reward_triggers_internal_reward_id_fkey",
}).onDelete("cascade"),
foreignKey({
columns: [table.orgId],
foreignColumns: [organizations.id],
name: "reward_triggers_org_id_fkey",
}).onDelete("cascade"),
],
);
export const customerPrices = pgTable(
"customer_prices",
{
id: text().primaryKey().notNull(),
createdAt: numeric("created_at").notNull(),
customerProductId: text("customer_product_id"),
priceId: text("price_id"),
options: jsonb(),
internalCustomerId: text("internal_customer_id"),
},
(table) => [
foreignKey({
columns: [table.customerProductId],
foreignColumns: [customerProducts.id],
name: "customer_prices_customer_product_id_fkey",
}).onDelete("cascade"),
foreignKey({
columns: [table.internalCustomerId],
foreignColumns: [customers.internalId],
name: "customer_prices_internal_customer_id_fkey",
}).onDelete("cascade"),
foreignKey({
columns: [table.priceId],
foreignColumns: [prices.id],
name: "customer_prices_price_id_fkey",
}),
],
);
export const entities = pgTable(
"entities",
{
internalId: text("internal_id").primaryKey().notNull(),
orgId: text("org_id"),
env: text(),
internalCustomerId: text("internal_customer_id"),
createdAt: numeric("created_at").notNull(),
id: text(),
name: text(),
deleted: boolean().default(false).notNull(),
internalFeatureId: text("internal_feature_id"),
featureId: text("feature_id"),
},
(table) => [
foreignKey({
columns: [table.internalCustomerId],
foreignColumns: [customers.internalId],
name: "entities_internal_customer_id_fkey",
}).onDelete("cascade"),
foreignKey({
columns: [table.internalFeatureId],
foreignColumns: [features.internal_id],
name: "entities_internal_feature_id_fkey",
}).onDelete("cascade"),
foreignKey({
columns: [table.orgId],
foreignColumns: [organizations.id],
name: "entities_org_id_fkey",
}).onDelete("cascade"),
unique("entity_id_constraint").on(
table.orgId,
table.env,
table.internalCustomerId,
table.id,
),
],
);
// export const entitlements = pgTable(
// "entitlements",
// {
// createdAt: numeric("created_at").notNull(),
// internalFeatureId: text("internal_feature_id"),
// orgId: text("org_id"),
// internalProductId: text("internal_product_id"),
// allowanceType: text("allowance_type"),
// allowance: numeric(),
// interval: text(),
// id: text().primaryKey().notNull(),
// featureId: text("feature_id"),
// isCustom: boolean("is_custom").default(false),
// carryFromPrevious: boolean("carry_from_previous").default(false),
// entityFeatureId: text("entity_feature_id"),
// },
// (table) => [
// foreignKey({
// columns: [table.internalFeatureId],
// foreignColumns: [features.internal_id],
// name: "entitlements_internal_feature_id_fkey",
// }).onDelete("cascade"),
// foreignKey({
// columns: [table.internalProductId],
// foreignColumns: [products.internalId],
// name: "entitlements_internal_product_id_fkey",
// })
// .onUpdate("cascade")
// .onDelete("cascade"),
// unique("entitlements_id_key").on(table.id),
// ],
// );
export const events = pgTable(
"events",
{
id: text().primaryKey().notNull(),
orgId: text("org_id").notNull(),
timestamp: numeric().notNull(),
env: text().notNull(),
customerId: text("customer_id").notNull(),
eventName: text("event_name").notNull(),
properties: jsonb(),
idempotencyKey: text("idempotency_key"),
internalCustomerId: text("internal_customer_id"),
value: numeric(),
setUsage: boolean("set_usage").default(false),
entityId: text("entity_id"),
},
(table) => [
foreignKey({
columns: [table.internalCustomerId],
foreignColumns: [customers.internalId],
name: "events_internal_customer_id_fkey",
}).onDelete("cascade"),
unique("unique_event_constraint").on(
table.orgId,
table.env,
table.customerId,
table.eventName,
table.idempotencyKey,
),
],
);
export const freeTrials = pgTable(
"free_trials",
{
id: text().primaryKey().notNull(),
createdAt: numeric("created_at").notNull(),
duration: text().default("day"),
length: numeric(),
internalProductId: text("internal_product_id"),
uniqueFingerprint: boolean("unique_fingerprint"),
isCustom: boolean("is_custom").default(false),
},
(table) => [
foreignKey({
columns: [table.internalProductId],
foreignColumns: [products.internalId],
name: "free_trials_internal_product_id_fkey",
}).onDelete("cascade"),
],
);
export const invoiceItems = pgTable(
"invoice_items",
{
createdAt: numeric("created_at").notNull(),
updatedAt: numeric("updated_at"),
customerPriceId: text("customer_price_id"),
periodStart: numeric("period_start"),
periodEnd: numeric("period_end"),
prorationStart: numeric("proration_start"),
prorationEnd: numeric("proration_end"),
quantity: numeric(),
amount: numeric(),
currency: text(),
id: text().primaryKey().notNull(),
addedToStripe: boolean("added_to_stripe").default(false),
customerId: text("customer_id"),
},
(table) => [
foreignKey({
columns: [table.customerPriceId],
foreignColumns: [customerPrices.id],
name: "invoice_items_customer_price_id_fkey",
}).onDelete("cascade"),
unique("invoice_items_id_key").on(table.id),
],
);
export const customers = pgTable(
"customers",
{
name: text().default(""),
orgId: text("org_id").notNull(),
createdAt: numeric("created_at").notNull(),
internalId: text("internal_id").primaryKey().notNull(),
id: text(),
env: text(),
processor: jsonb(),
email: text().default(""),
fingerprint: text(),
metadata: jsonb().default({}),
},
(table) => [unique("cus_id_constraint").on(table.orgId, table.id, table.env)],
);
export const customerProducts = pgTable(
"customer_products",
{
id: text().primaryKey().notNull(),
internalCustomerId: text("internal_customer_id").notNull(),
customerId: text("customer_id"),
internalProductId: text("internal_product_id"),
createdAt: numeric("created_at"),
status: text(),
processor: jsonb(),
canceledAt: numeric("canceled_at"),
endedAt: numeric("ended_at"),
startsAt: numeric("starts_at"),
options: jsonb().array(),
productId: text("product_id"),
freeTrialId: text("free_trial_id"),
trialEndsAt: numeric("trial_ends_at"),
collectionMethod: text("collection_method").default("charge_automatically"),
subscriptionIds: text("subscription_ids").array(),
scheduledIds: text("scheduled_ids").array(),
isCustom: boolean("is_custom").default(false).notNull(),
quantity: numeric().default("1"),
internalEntityId: text("internal_entity_id"),
entityId: text("entity_id"),
},
(table) => [
foreignKey({
columns: [table.freeTrialId],
foreignColumns: [freeTrials.id],
name: "customer_products_free_trial_id_fkey",
}),
foreignKey({
columns: [table.internalCustomerId],
foreignColumns: [customers.internalId],
name: "customer_products_internal_customer_id_fkey",
})
.onUpdate("cascade")
.onDelete("cascade"),
foreignKey({
columns: [table.internalEntityId],
foreignColumns: [entities.internalId],
name: "customer_products_internal_entity_id_fkey",
}).onDelete("set null"),
foreignKey({
columns: [table.internalProductId],
foreignColumns: [products.internalId],
name: "customer_products_internal_product_id_fkey",
}),
],
);
export const metadata = pgTable("metadata", {
id: text().primaryKey().notNull(),
createdAt: numeric("created_at").notNull(),
expiresAt: numeric("expires_at"),
data: jsonb(),
});
export const products = pgTable(
"products",
{
internalId: text("internal_id").primaryKey().notNull(),
createdAt: numeric("created_at").notNull(),
name: text(),
orgId: text("org_id"),
env: text(),
isAddOn: boolean("is_add_on"),
processor: jsonb(),
isDefault: boolean("is_default").default(false),
id: text(),
group: text().default(""),
version: numeric().default("1"),
},
(table) => [
foreignKey({
columns: [table.orgId],
foreignColumns: [organizations.id],
name: "products_org_id_fkey",
}).onDelete("cascade"),
],
);
export const rewardRedemptions = pgTable(
"reward_redemptions",
{
id: text().primaryKey().notNull(),
createdAt: numeric("created_at").notNull(),
updatedAt: numeric("updated_at"),
internalCustomerId: text("internal_customer_id"),
triggered: boolean(),
internalRewardProgramId: text("internal_reward_program_id"),
applied: boolean().default(false),
referralCodeId: text("referral_code_id"),
},
(table) => [
foreignKey({
columns: [table.internalCustomerId],
foreignColumns: [customers.internalId],
name: "reward_redemptions_internal_customer_id_fkey",
}).onDelete("cascade"),
foreignKey({
columns: [table.internalRewardProgramId],
foreignColumns: [rewardPrograms.internalId],
name: "reward_redemptions_internal_reward_program_id_fkey",
}).onDelete("cascade"),
foreignKey({
columns: [table.referralCodeId],
foreignColumns: [referralCodes.id],
name: "reward_redemptions_referral_code_id_fkey",
}).onDelete("cascade"),
],
);
export const migrationJobs = pgTable(
"migration_jobs",
{
id: text().primaryKey().notNull(),
createdAt: numeric("created_at").notNull(),
updatedAt: numeric("updated_at"),
currentStep: text("current_step"),
fromInternalProductId: text("from_internal_product_id"),
toInternalProductId: text("to_internal_product_id"),
stepDetails: jsonb("step_details"),
orgId: text("org_id"),
env: text(),
},
(table) => [
foreignKey({
columns: [table.fromInternalProductId],
foreignColumns: [products.internalId],
name: "migration_jobs_from_internal_product_id_fkey",
}).onDelete("cascade"),
foreignKey({
columns: [table.orgId],
foreignColumns: [organizations.id],
name: "migration_jobs_org_id_fkey",
}).onDelete("cascade"),
foreignKey({
columns: [table.toInternalProductId],
foreignColumns: [products.internalId],
name: "migration_jobs_to_internal_product_id_fkey",
}).onDelete("cascade"),
],
);
export const subscriptions = pgTable(
"subscriptions",
{
id: text().primaryKey().notNull(),
stripeId: text("stripe_id"),
stripeScheduleId: text("stripe_schedule_id"),
createdAt: numeric("created_at"),
metadata: jsonb().default({}),
usageFeatures: text("usage_features").array().default([""]),
orgId: text("org_id"),
env: text(),
currentPeriodStart: numeric("current_period_start"),
currentPeriodEnd: numeric("current_period_end"),
},
(table) => [
foreignKey({
columns: [table.orgId],
foreignColumns: [organizations.id],
name: "subscriptions_org_id_fkey",
}).onDelete("cascade"),
unique("subscriptions_stripe_id_key").on(table.stripeId),
],
);
export const referralCodes = pgTable(
"referral_codes",
{
code: text().notNull(),
orgId: text("org_id").notNull(),
env: text().notNull(),
internalCustomerId: text("internal_customer_id"),
internalRewardProgramId: text("internal_reward_program_id"),
id: text().notNull(),
createdAt: numeric("created_at"),
},
(table) => [
foreignKey({
columns: [table.internalCustomerId],
foreignColumns: [customers.internalId],
name: "referral_codes_internal_customer_id_fkey",
}).onDelete("cascade"),
foreignKey({
columns: [table.internalRewardProgramId],
foreignColumns: [rewardPrograms.internalId],
name: "referral_codes_internal_reward_program_id_fkey",
}).onDelete("cascade"),
foreignKey({
columns: [table.orgId],
foreignColumns: [organizations.id],
name: "referral_codes_org_id_fkey",
}).onDelete("cascade"),
primaryKey({
columns: [table.code, table.orgId, table.env],
name: "referral_codes_pkey",
}),
unique("referral_codes_id_key").on(table.id),
],
);
export const migrationErrors = pgTable(
"migration_errors",
{
internalCustomerId: text("internal_customer_id").notNull(),
migrationJobId: text("migration_job_id").notNull(),
createdAt: numeric("created_at"),
updatedAt: numeric("updated_at"),
data: jsonb(),
message: text(),
code: text(),
},
(table) => [
foreignKey({
columns: [table.internalCustomerId],
foreignColumns: [customers.internalId],
name: "migration_customers_internal_customer_id_fkey",
}).onDelete("cascade"),
foreignKey({
columns: [table.migrationJobId],
foreignColumns: [migrationJobs.id],
name: "migration_customers_migration_job_id_fkey",
}).onDelete("cascade"),
primaryKey({
columns: [table.internalCustomerId, table.migrationJobId],
name: "migration_errors_pkey",
}),
],
);

View File

@@ -1,31 +0,0 @@
import {
pgTable,
text,
numeric,
jsonb,
boolean,
unique,
} from "drizzle-orm/pg-core";
import { Organization } from "@autumn/shared";
export const organizations = pgTable(
"organizations",
{
id: text().primaryKey().notNull(),
slug: text().notNull(),
default_currency: text("default_currency"),
stripe_connected: boolean("stripe_connected"),
stripe_config:
jsonb("stripe_config").$type<Organization["stripe_config"]>(),
test_pkey: text("test_pkey"),
live_pkey: text("live_pkey"),
svix_config: jsonb("svix_config").$type<Organization["svix_config"]>(),
created_at: numeric("created_at"),
config: jsonb().default({}),
},
(table) => [
unique("organizations_test_pkey_key").on(table.test_pkey),
unique("organizations_live_pkey_key").on(table.live_pkey),
],
);

View File

@@ -1,8 +1,6 @@
import { AppEnv, ErrCode, MinOrg, Organization } from "@autumn/shared";
import { AppEnv, ErrCode, Organization } from "@autumn/shared";
import { Autumn } from "./autumnCli.js";
import RecaseError from "@/utils/errorUtils.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { SupabaseClient } from "@supabase/supabase-js";
export enum FeatureId {
Products = "products",
@@ -10,43 +8,12 @@ export enum FeatureId {
Revenue = "revenue",
}
export const sendFeatureEvent = async ({
minOrg,
env,
incrementBy,
}: {
minOrg: MinOrg;
env: AppEnv;
incrementBy: number;
}) => {
if (env !== AppEnv.Live) {
return;
}
try {
const autumn = new Autumn();
await autumn.sendEvent({
customerId: minOrg.id,
eventName: "feature",
properties: {
value: incrementBy,
},
customer_data: {
name: minOrg.slug,
},
});
} catch (error: any) {
console.log("Failed to send feature event", error?.message || error);
}
};
export const sendProductEvent = async ({
minOrg,
org,
env,
incrementBy,
}: {
minOrg: MinOrg;
org: Organization;
env: AppEnv;
incrementBy: number;
}) => {
@@ -58,13 +25,13 @@ export const sendProductEvent = async ({
const autumn = new Autumn();
await autumn.sendEvent({
customerId: minOrg.id,
customerId: org.id,
eventName: "product",
properties: {
value: incrementBy,
},
customer_data: {
name: minOrg.slug,
name: org.slug,
},
});
console.log("sent product event", incrementBy);
@@ -74,11 +41,11 @@ export const sendProductEvent = async ({
};
export const isEntitled = async ({
minOrg,
org,
env,
featureId,
}: {
minOrg: MinOrg;
org: Organization;
env: AppEnv;
featureId: FeatureId;
}) => {
@@ -91,10 +58,10 @@ export const isEntitled = async ({
let result;
try {
result = await autumn.entitled({
customerId: minOrg.id,
customerId: org.id,
featureId: featureId,
customer_data: {
name: minOrg.slug,
name: org.slug,
},
});
} catch (error: any) {

View File

@@ -89,7 +89,7 @@ export const createStripeMeteredPrice = async ({
...productData,
...priceAmountData,
currency: org.default_currency,
nickname: `Autumn Price (${price.name}) [Placeholder]`,
nickname: `Autumn Price (${feature!.name}) [Placeholder]`,
recurring: {
...(billingIntervalToStripe(price.config!.interval!) as any),
meter: meter!.id,
@@ -102,7 +102,7 @@ export const createStripeMeteredPrice = async ({
export const arrearProratedToStripeTiers = (
price: Price,
entitlement: EntitlementWithFeature
entitlement: EntitlementWithFeature,
) => {
let usageConfig = structuredClone(price.config) as UsagePriceConfig;
@@ -198,7 +198,7 @@ export const createStripeArrearProrated = async ({
recurring: {
...(recurringData as any),
},
nickname: `Autumn Price (${price.name})`,
nickname: `Autumn Price (${relatedEnt.feature.name})`,
});
config.stripe_price_id = stripePrice.id;

View File

@@ -52,7 +52,7 @@ export const searchStripeMeter = async ({
logger.info(`Stripe meter list took ${end - start}ms`);
let stripeMeter = allStripeMeters.find(
(m) => m.event_name == eventName || m.id == meterId
(m) => m.event_name == eventName || m.id == meterId,
);
return stripeMeter;
@@ -86,7 +86,7 @@ export const getStripeMeter = async ({
createNew = true;
} else {
logger.info(
`✅ Found existing meter for ${product.name} - ${feature!.name}`
`✅ Found existing meter for ${product.name} - ${feature!.name}`,
);
return stripeMeter;
}
@@ -106,7 +106,7 @@ export const getStripeMeter = async ({
// IN ARREAR
export const priceToInArrearTiers = (
price: Price,
entitlement: Entitlement
entitlement: Entitlement,
) => {
let usageConfig = structuredClone(price.config) as UsagePriceConfig;
const tiers: any[] = [];
@@ -176,7 +176,7 @@ export const createStripeInArrearPrice = async ({
if (internalEntityId && !useCheckout) {
if (!curStripeProduct) {
logger.info(
`Creating stripe in arrear product for ${relatedEnt.feature.name} (internal entity ID exists!)`
`Creating stripe in arrear product for ${relatedEnt.feature.name} (internal entity ID exists!)`,
);
let stripeProduct = await stripeCli.products.create({
name: `${product.name} - ${feature!.name}`,
@@ -198,7 +198,7 @@ export const createStripeInArrearPrice = async ({
}
logger.info(
`Creating stripe in arrear price for ${relatedEnt.feature.name} (no internal entity ID)`
`Creating stripe in arrear price for ${relatedEnt.feature.name} (no internal entity ID)`,
);
if (!feature) {
@@ -222,7 +222,7 @@ export const createStripeInArrearPrice = async ({
const tiers = priceToInArrearTiers(
price,
getPriceEntitlement(price, entitlements)
getPriceEntitlement(price, entitlements),
);
let priceAmountData = {};
@@ -260,7 +260,7 @@ export const createStripeInArrearPrice = async ({
meter: meter!.id,
usage_type: "metered",
},
nickname: `Autumn Price (${price.name})`,
nickname: `Autumn Price (${relatedEnt.feature.name})`,
});
config.stripe_price_id = stripePrice.id;

View File

@@ -21,7 +21,7 @@ import { Decimal } from "decimal.js";
export const prepaidToStripeTiers = (
price: Price,
entitlement: EntitlementWithFeature
entitlement: EntitlementWithFeature,
) => {
let usageConfig = structuredClone(price.config) as UsagePriceConfig;
@@ -137,7 +137,7 @@ export const createStripePrepaid = async ({
recurring: {
...(recurringData as any),
},
nickname: `Autumn Price (${price.name})`,
nickname: `Autumn Price (${relatedEnt.feature.name})`,
});
config.stripe_price_id = stripePrice.id;

View File

@@ -104,7 +104,7 @@ export const priceToStripeItem = ({
if (!priceId) {
throw new RecaseError({
code: ErrCode.PriceNotFound,
message: `Couldn't find price: ${price.name}, ${price.id} in Stripe`,
message: `Couldn't find Autumn price: ${price.id} in Stripe`,
statusCode: 400,
});
}

View File

@@ -16,10 +16,8 @@ import {
import { DrizzleCli } from "@/db/initDrizzle.js";
import { constructOrg } from "@/internal/orgs/orgUtils.js";
import { createOnboardingProducts } from "@/internal/orgs/onboarding/createOnboardingProducts.js";
import { organizations } from "@/db/schema/tables/orgTable.js";
import { eq } from "drizzle-orm";
import { ErrCode } from "@/errors/errCodes.js";
import { Organization } from "@autumn/shared";
import { Organization, organizations } from "@autumn/shared";
const verifyClerkWebhook = async (req: Request, res: Response) => {
const wh = new Webhook(process.env.CLERK_SIGNING_SECRET!);
@@ -60,6 +58,7 @@ const verifyClerkWebhook = async (req: Request, res: Response) => {
export const handleClerkWebhook = async (req: any, res: any) => {
let event = await verifyClerkWebhook(req, res);
if (!event) {
return;
}
@@ -70,7 +69,11 @@ export const handleClerkWebhook = async (req: any, res: any) => {
try {
switch (eventType) {
case "organization.created":
await handleOrgCreated(req.db, eventData);
await saveOrgToDB({
db: req.db,
id: eventData.id,
slug: eventData.slug,
});
break;
case "organization.deleted":
@@ -99,47 +102,46 @@ export const handleClerkWebhook = async (req: any, res: any) => {
});
};
export const handleOrgCreated = async (
db: DrizzleCli,
eventData: {
id: string;
slug: string;
created_at: number;
},
) => {
console.log(
`Handling organization.created: ${eventData.slug} (${eventData.id})`,
);
export const saveOrgToDB = async ({
db,
id,
slug,
}: {
db: DrizzleCli;
id: string;
slug: string;
}) => {
console.log(`Handling organization.created: ${slug} (${id})`);
try {
// 2. Insert org
await OrgService.insert({
db,
org: constructOrg({
id: eventData.id,
slug: eventData.slug,
id,
slug,
}),
});
// 1. Create svix webhoooks
const { sandboxApp, liveApp } = await initOrgSvixApps({
slug: eventData.slug,
id: eventData.id,
slug,
id,
});
await OrgService.update({
db,
orgId: eventData.id,
orgId: id,
updates: {
svix_config: { sandbox_app_id: sandboxApp.id, live_app_id: liveApp.id },
},
});
console.log(`Created svix webhooks for org ${eventData.id}`);
console.log(`Created svix webhooks for org ${id}`);
} catch (error: any) {
if (error?.data && error.data.code == "23505") {
console.error(
`Org ${eventData.id} already exists in Supabase -- skipping creationg`,
`Org ${id} already exists in Supabase -- skipping creationg`,
);
return;
}
@@ -152,16 +154,16 @@ export const handleOrgCreated = async (
const batch = [];
try {
batch.push(
createOnboardingProducts({
db,
orgId: eventData.id,
}),
);
// batch.push(
// createOnboardingProducts({
// db,
// orgId: eventData.id,
// }),
// );
batch.push(
sendOnboardingEmail({
orgId: eventData.id,
orgId: id,
clerkCli: createClerkCli(),
}),
);
@@ -201,7 +203,7 @@ const handleOrgDeleted = async ({
console.log("1. Deleting svix webhooks");
const batch = [];
if (org.svix_config.sandbox_app_id) {
if (org.svix_config?.sandbox_app_id) {
batch.push(
deleteSvixApp({
appId: org.svix_config.sandbox_app_id,
@@ -209,7 +211,7 @@ const handleOrgDeleted = async ({
);
}
if (org.svix_config.live_app_id) {
if (org.svix_config?.live_app_id) {
batch.push(
deleteSvixApp({
appId: org.svix_config.live_app_id,

View File

@@ -29,21 +29,23 @@ const init = async () => {
server.keepAliveTimeout = 120000; // 120 seconds
server.headersTimeout = 120000; // 120 seconds should be >= keepAliveTimeout
const pgClient = new pg.Client(process.env.DATABASE_URL || "");
await pgClient.connect();
const pgClient = new pg.Client(
process.env.SUPABASE_CONNECTION_STRING || process.env.DATABASE_URL || "",
);
await pgClient.connect();
await QueueManager.getInstance(); // initialize the queue manager
await CacheManager.getInstance();
// await initWorkers();
const supabaseClient = createSupabaseClient();
const logtailAll = createLogtailAll();
const drizzle = initDrizzle();
const { client, db } = initDrizzle();
app.use((req: any, res, next) => {
req.sb = supabaseClient;
req.pg = pgClient;
req.db = drizzle;
req.db = db;
req.logger = logger;
req.logtailAll = logtailAll;

View File

@@ -29,8 +29,8 @@ apiRouter.use(apiAuthMiddleware);
apiRouter.use(pricingMiddleware);
apiRouter.use((req: any, res: any, next: any) => {
const logtailContext: any = {
org_id: req.minOrg?.id,
org_slug: req.minOrg?.slug,
org_id: req.org?.id,
org_slug: req.org?.slug,
method: req.method,
url: req.originalUrl,
body: req.body,
@@ -61,14 +61,14 @@ apiRouter.use((req: any, res: any, next: any) => {
return;
}
req.logtailAll.info(
`[${res.statusCode}] ${req.method} ${req.originalUrl} (${req.minOrg?.slug})`,
`[${res.statusCode}] ${req.method} ${req.originalUrl} (${req.org?.slug})`,
{
req: {
...logtailContext,
},
statusCode: res.statusCode,
res: res.locals.responseBody,
}
},
);
req.logtailAll.flush();
} catch (error) {

View File

@@ -69,14 +69,14 @@ cusRouter.post("/:customer_id", handleUpdateCustomer);
// Update customer entitlement directly
cusRouter.post(
"/customer_entitlements/:customer_entitlement_id",
handleUpdateEntitlement
handleUpdateEntitlement,
);
cusRouter.post("/:customer_id/balances", handleUpdateBalances);
cusRouter.post(
"/customer_products/:customer_product_id",
handleCusProductExpired
handleCusProductExpired,
);
cusRouter.get("/:customer_id/billing_portal", async (req: any, res: any) => {
@@ -115,7 +115,7 @@ cusRouter.get("/:customer_id/billing_portal", async (req: any, res: any) => {
const stripeCli = createStripeCli({ org, env: req.env });
const portal = await stripeCli.billingPortal.sessions.create({
customer: customer.processor.id,
return_url: returnUrl || org.stripe_config.success_url,
return_url: returnUrl || org.stripe_config?.success_url,
});
if (org.api_version >= APIVersion.v1_1) {

View File

@@ -74,7 +74,7 @@ featureApiRouter.post("", async (req: any, res) => {
let data = req.body;
try {
let { sb, orgId, env, logtail: logger } = req;
let { db, orgId, env, logtail: logger } = req;
let parsedFeature = validateFeature(data);
let feature: Feature = {
@@ -87,8 +87,7 @@ featureApiRouter.post("", async (req: any, res) => {
let org = await OrgService.getFromReq(req);
let insertedData = await FeatureService.insert({
db: req.db,
sb,
db,
data: feature,
logger,
});

View File

@@ -16,18 +16,16 @@ export const handleDeleteProduct = (req: any, res: any) =>
const orgId = req.orgId;
const env = req.env;
const [org, product] = await Promise.all([
OrgService.getFullOrg({
sb,
orgId,
}),
ProductService.getProductStrict({
sb,
productId,
orgId,
env,
}),
]);
console.log("Org ID", orgId);
console.log("Product ID", productId);
console.log("Env", env);
const product = await ProductService.getProductStrict({
sb,
productId,
orgId,
env,
});
if (!product) {
throw new RecaseError({
@@ -39,7 +37,7 @@ export const handleDeleteProduct = (req: any, res: any) =>
let cusProducts = await CusProductService.getByInternalProductId(
sb,
product.internal_id
product.internal_id,
);
if (cusProducts.length > 0) {

View File

@@ -16,7 +16,7 @@ export const handleCopyProduct = async (req: any, res: any) =>
const sb = req.sb;
const orgId = req.orgId;
const fromEnv = req.env;
const { env: toEnv, id: toId, name: toName } = req.body;
const { db, env: toEnv, id: toId, name: toName } = req.body;
let { logtail: logger } = req;
if (!toEnv || !toId || !toName) {
@@ -85,8 +85,7 @@ export const handleCopyProduct = async (req: any, res: any) =>
if (!toFeature) {
let res = await FeatureService.insert({
db: req.db,
sb,
db,
data: initNewFeature({
data: CreateFeatureSchema.parse(fromFeature),
orgId,

View File

@@ -1,23 +0,0 @@
import { CreditSystem } from "@autumn/shared";
import { SupabaseClient } from "@supabase/supabase-js";
export class CreditService {
static async getByOrg(sb: SupabaseClient, orgId: string) {
let { data, error } = await sb
.from("credit_systems")
.select("*")
.eq("org_id", orgId);
if (error) throw error;
return data;
}
static async insert(sb: SupabaseClient, creditSystem: CreditSystem) {
let { data, error } = await sb.from("credit_systems").insert(creditSystem);
if (error) throw error;
return data;
}
}

View File

@@ -1,42 +0,0 @@
import express from "express";
import { generateId } from "@/utils/genUtils.js";
import { getFeaturesByOrg } from "../features/featureCrud.js";
import { CreditService } from "./CreditService.js";
import { CreditSystem } from "@autumn/shared";
export const creditsRouter = express.Router();
creditsRouter.get("", async (req: any, res) => {
let orgId = req.orgId;
try {
let features = await getFeaturesByOrg(orgId);
let creditSystems = await CreditService.getByOrg(req.sb, orgId);
res.status(200).json({ features, credit_systems: creditSystems });
} catch (error: any) {
console.log("Error fetching features:", error);
res.status(500).json({ error: error.message });
}
});
creditsRouter.post("/systems", async (req: any, res) => {
let orgId = req.orgId;
let data = req.body;
try {
let creditSystem: CreditSystem = {
internal_id: generateId("cs"),
org_id: orgId,
created_at: Date.now(),
...data,
};
await CreditService.insert(req.sb, creditSystem);
console.log("Successfully inserted credit system into DB");
res.status(200).json({ message: "Credit system created" });
} catch (error: any) {
console.log("Error inserting credit system into DB:", error);
res.status(500).json({ error: error.message });
}
});

View File

@@ -48,14 +48,6 @@ import { SuccessCode } from "@autumn/shared";
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
import { getInvoiceItems } from "../invoices/invoiceUtils.js";
import { sendSvixEvent } from "@/external/svix/svixUtils.js";
import { getCustomerDetails } from "@/internal/api/customers/getCustomerDetails.js";
import { addTaskToQueue } from "@/queue/queueUtils.js";
import { JobName } from "@/queue/JobName.js";
import {
constructProductsUpdatedData,
ProductsUpdatedDataSchema,
} from "@/external/svix/handleProductsUpdatedWebhook.js";
export const handleBillNowPrices = async ({
sb,
@@ -98,7 +90,7 @@ export const handleBillNowPrices = async ({
let mergeCusProduct =
!disableMerge && !freeTrial && org.config.merge_billing_cycles
? cusProducts?.find((cp) =>
products.some((p) => p.group == cp.product.group)
products.some((p) => p.group == cp.product.group),
)
: undefined;
@@ -113,7 +105,7 @@ export const handleBillNowPrices = async ({
}
let mergeWithSub = mergeSubs.find(
(sub) => subToAutumnInterval(sub) == itemSet.interval
(sub) => subToAutumnInterval(sub) == itemSet.interval,
);
let subscription;
@@ -126,7 +118,7 @@ export const handleBillNowPrices = async ({
if (attachParams.billingAnchor) {
billingCycleAnchorUnix = getAlignedIntervalUnix(
attachParams.billingAnchor,
itemSet.interval
itemSet.interval,
);
}
@@ -192,7 +184,7 @@ export const handleBillNowPrices = async ({
: undefined,
carryExistingUsages,
scenario: AttachScenario.New,
})
}),
);
}
await Promise.all(batchInsert);
@@ -250,7 +242,7 @@ export const handleBillNowPrices = async ({
customer_id: customer.id || customer.internal_id,
invoice: invoiceOnly ? invoices?.[0] : undefined,
})
}),
);
} else {
res.status(200).json({
@@ -338,8 +330,8 @@ export const handleOneOffPrices = async ({
billingType == BillingType.UsageInAdvance
? UsageModel.Prepaid
: price.config?.type == PriceType.Usage
? UsageModel.PayPerUse
: null,
? UsageModel.PayPerUse
: null,
feature_name: entitlement?.feature.name,
};
}
@@ -390,7 +382,7 @@ export const handleOneOffPrices = async ({
if (!attachParams.invoiceOnly) {
stripeInvoice = await stripeCli.invoices.finalizeInvoice(
stripeInvoice.id,
getInvoiceExpansion()
getInvoiceExpansion(),
);
logger.info(" 3. Paying invoice");
@@ -428,7 +420,7 @@ export const handleOneOffPrices = async ({
sb,
attachParams: attachToInsertParams(attachParams, product),
lastInvoiceId: stripeInvoice.id,
})
}),
);
}
await Promise.all(batchInsert);
@@ -460,7 +452,7 @@ export const handleOneOffPrices = async ({
product_ids: products.map((p) => p.id),
customer_id: customer.id || customer.internal_id,
scenario: AttachScenario.New,
})
}),
);
}
};
@@ -492,14 +484,14 @@ export const handleAddProduct = async ({
if (product.is_add_on) {
logger.info(
`Adding add-on ${chalk.yellowBright(
product.name
)} to customer ${chalk.yellowBright(customer.id)}`
product.name,
)} to customer ${chalk.yellowBright(customer.id)}`,
);
} else {
logger.info(
`Adding product ${chalk.yellowBright(
product.name
)} to customer ${chalk.yellowBright(customer.id)}`
product.name,
)} to customer ${chalk.yellowBright(customer.id)}`,
);
}
}
@@ -545,7 +537,7 @@ export const handleAddProduct = async ({
billLaterOnly: true,
carryExistingUsages,
keepResetIntervals,
})
}),
);
}
await Promise.all(batchInsert);
@@ -564,7 +556,7 @@ export const handleAddProduct = async ({
.join(", ")}`,
product_ids: products.map((p) => p.id),
customer_id: customer.id,
})
}),
);
} else {
res.status(200).json({

View File

@@ -448,7 +448,7 @@ export const processFullCusProduct = ({
return {
tiers: tiers,
name: price.name,
name: "",
quantity: priceOptions?.quantity,
};
}
@@ -487,6 +487,13 @@ export const processFullCusProduct = ({
}
if (apiVersion >= APIVersion.v1_1) {
if ((!subIds || subIds.length == 0) && trialing) {
stripeSubData = {
current_period_start: cusProduct.starts_at,
current_period_end: cusProduct.trial_ends_at,
};
}
return CusProductResponseSchema.parse({
id: cusProduct.product.id,
name: cusProduct.product.name,

View File

@@ -2,8 +2,9 @@ import { CacheType } from "@/external/caching/cacheActions.js";
import { CacheManager } from "@/external/caching/CacheManager.js";
import { getAPIKeyCache } from "@/external/caching/cacheUtils.js";
import { sbWithRetry } from "@/external/supabaseUtils.js";
import { getApiVersion } from "@/utils/versionUtils.js";
import { ApiKey, AppEnv, ErrCode } from "@autumn/shared";
import { ApiKey, AppEnv, ErrCode, OrgConfigSchema } from "@autumn/shared";
import { SupabaseClient } from "@supabase/supabase-js";
export class ApiKeyService {
@@ -31,8 +32,14 @@ export class ApiKeyService {
}
let org = structuredClone(data.organization);
delete org.features;
// Add org config and api version
org.config = OrgConfigSchema.parse(org.config || {});
org.api_version = getApiVersion({
createdAt: org.created_at,
});
return {
org,
features: data.organization?.features || [],
@@ -133,7 +140,7 @@ export class CachedKeyService {
});
} catch (error) {
console.error(
`(warning) failed to clear cache for verify action: ${error}`
`(warning) failed to clear cache for verify action: ${error}`,
);
}
}

View File

@@ -1,6 +1,12 @@
import { ErrCode } from "@/errors/errCodes.js";
import RecaseError from "@/utils/errorUtils.js";
import { AppEnv, CreditSchemaItem, Feature, FeatureType } from "@autumn/shared";
import {
AppEnv,
CreditSchemaItem,
Feature,
features,
FeatureType,
} from "@autumn/shared";
import { SupabaseClient } from "@supabase/supabase-js";
import { Client } from "pg";
import { creditSystemContainsFeature } from "./creditSystemUtils.js";
@@ -8,6 +14,25 @@ import { clearOrgCache } from "../orgs/orgUtils/clearOrgCache.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
export class FeatureService {
static async list({
db,
orgId,
env,
}: {
db: DrizzleCli;
orgId: string;
env: AppEnv;
}) {
const features = await db.query.features.findMany({
where: (features, { eq, and }) =>
and(eq(features.org_id, orgId), eq(features.env, env)),
orderBy: (features, { desc }) => [desc(features.internal_id)],
});
return features as Feature[];
}
static async getFromReq(req: any) {
if (req.features) return req.features as Feature[];
const features = await FeatureService.getFeatures({
@@ -123,6 +148,7 @@ export class FeatureService {
return data;
}
static async updateStrict({
db,
sb,
@@ -177,23 +203,33 @@ export class FeatureService {
}
static async insert({
sb,
db,
data,
logger,
}: {
sb: SupabaseClient;
db: DrizzleCli;
data: Feature[] | Feature;
logger: any;
}) {
// Insert feature into DB
let { data: insertedData, error } = await sb
.from("features")
.insert(data)
.select();
// let { data: insertedData, error } =
if (error) {
try {
let insertedData = await db
.insert(features)
.values(data as any) // DRIZZLE TYPE REFACTOR
.returning();
if (insertedData && insertedData.length > 0) {
let orgId = insertedData[0].org_id;
await clearOrgCache({
db,
orgId: orgId!,
logger,
});
}
return insertedData;
} catch (error: any) {
if (error.code === "23505") {
let id = Array.isArray(data) ? data.map((f) => f.id) : data.id;
throw new RecaseError({
@@ -202,19 +238,7 @@ export class FeatureService {
statusCode: 400,
});
}
throw error;
}
if (insertedData && insertedData.length > 0) {
let orgId = insertedData[0].org_id;
await clearOrgCache({
db,
orgId,
logger,
});
}
return insertedData;
}
static async deleteStrict({

View File

@@ -1,5 +1,4 @@
import RecaseError from "@/utils/errorUtils.js";
import { CreditSchemaItem, ErrCode, FeatureType } from "@autumn/shared";
import { CreditSchemaItem, FeatureType } from "@autumn/shared";
import { Feature } from "@autumn/shared";
import { Decimal } from "decimal.js";
@@ -36,7 +35,7 @@ export const getCreditSystemsFromFeature = ({
creditSystemContainsFeature({
creditSystem: f,
meteredFeatureId: featureId,
})
}),
);
};

View File

@@ -142,69 +142,6 @@ export const getObjectsUsingFeature = async ({
return { entitlements, prices, creditSystems, linkedEntitlements };
};
export const constructBooleanFeature = ({
featureId,
orgId,
env,
}: {
featureId: string;
orgId: string;
env: AppEnv;
}) => {
let newFeature: Feature = {
internal_id: generateId("fe"),
org_id: orgId,
env,
created_at: Date.now(),
id: featureId,
name: keyToTitle(featureId),
type: FeatureType.Boolean,
config: null,
};
return newFeature;
};
export const constructMeteredFeature = ({
featureId,
orgId,
env,
usageType,
}: {
featureId: string;
orgId: string;
env: AppEnv;
usageType: FeatureUsageType;
}) => {
let newFeature: Feature = {
internal_id: generateId("fe"),
org_id: orgId,
env,
created_at: Date.now(),
id: featureId,
name: keyToTitle(featureId),
type: FeatureType.Metered,
config: {
filters: [
{
property: "event_name",
operator: "eq",
value: [],
},
],
aggregate: {
type: AggregateType.Sum,
property: "value",
},
usage_type: usageType,
},
};
return newFeature;
};
export const runSaveFeatureDisplayTask = async ({
db,
sb,

View File

@@ -0,0 +1,100 @@
import { AggregateType } from "@autumn/shared";
import { AppEnv, FeatureType, FeatureUsageType } from "@autumn/shared";
import { generateId, keyToTitle } from "@/utils/genUtils.js";
import { Feature } from "@autumn/shared";
export const constructFeature = ({
id,
name,
orgId,
type,
env,
config,
display,
}: {
id: string;
name: string;
orgId: string;
type: FeatureType;
env: AppEnv;
config: any;
display: any;
}) => {
let newFeature: Feature = {
internal_id: generateId("fe"),
id,
name,
org_id: orgId,
env,
created_at: Date.now(),
type,
config,
display,
};
return newFeature;
};
export const constructBooleanFeature = ({
featureId,
orgId,
env,
}: {
featureId: string;
orgId: string;
env: AppEnv;
}) => {
let newFeature: Feature = {
internal_id: generateId("fe"),
org_id: orgId,
env,
created_at: Date.now(),
id: featureId,
name: keyToTitle(featureId),
type: FeatureType.Boolean,
config: null,
};
return newFeature;
};
export const constructMeteredFeature = ({
featureId,
orgId,
env,
usageType,
}: {
featureId: string;
orgId: string;
env: AppEnv;
usageType: FeatureUsageType;
}) => {
let newFeature: Feature = {
internal_id: generateId("fe"),
org_id: orgId,
env,
created_at: Date.now(),
id: featureId,
name: keyToTitle(featureId),
type: FeatureType.Metered,
config: {
filters: [
{
property: "event_name",
operator: "eq",
value: [],
},
],
aggregate: {
type: AggregateType.Sum,
property: "value",
},
usage_type: usageType,
},
};
return newFeature;
};

View File

@@ -1,81 +1,32 @@
import dotenv from "dotenv";
dotenv.config();
import { orgRouter } from "./orgs/orgRouter.js";
import { Router } from "express";
import { userRouter } from "./users/userRouter.js";
import { withAuth, withOrgAuth } from "../middleware/authMiddleware.js";
import { featureRouter } from "./features/featureRouter.js";
import { creditsRouter } from "./credits/creditsRouter.js";
import { productRouter } from "./products/internalProductRouter.js";
import { devRouter } from "./dev/devRouter.js";
import { cusRouter } from "./customers/internalCusRouter.js";
import { testRouter } from "./test/testRouter.js";
import { createClerkCli } from "../external/clerkUtils.js";
import { ErrCode } from "@autumn/shared";
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
import { StatusCodes } from "http-status-codes";
import { handleOrgCreated } from "@/external/webhooks/clerkWebhooks.js";
import { autumnHandler } from "autumn-js/express";
import { onboardingRouter } from "./orgs/onboarding/onboardingRouter.js";
import { handlePostOrg } from "./orgs/handlers/handlePostOrg.js";
const mainRouter = Router();
mainRouter.get("", async (req: any, res) => {
res.status(200).json({ message: "Hello World" });
});
mainRouter.use("/onboarding", withAuth, onboardingRouter);
mainRouter.post("/organization", withAuth, async (req: any, res) => {
try {
let { db } = req;
let { orgId } = req.body;
let userId = req.userId;
let clerk = createClerkCli();
let org = await clerk.organizations.getOrganization({
organizationId: orgId,
});
if (!org) {
throw new RecaseError({
message: "Organization not found",
code: ErrCode.OrgNotFound,
statusCode: StatusCodes.NOT_FOUND,
});
}
const memberships = await clerk.users.getOrganizationMembershipList({
userId: userId,
});
let userIsAdmin = memberships.data.some((m) => m.organization.id === orgId);
if (!userIsAdmin) {
throw new RecaseError({
message: "User is not an admin of this organization",
code: ErrCode.OrgNotFound,
statusCode: StatusCodes.NOT_FOUND,
});
}
await handleOrgCreated(db, {
id: orgId,
slug: org.slug || "",
created_at: Date.now(),
}),
res.status(200).json({ message: "Success" });
} catch (error: any) {
handleRequestError({ error, req, res, action: "create org" });
}
});
mainRouter.post("/organization", withAuth, handlePostOrg);
mainRouter.use("/users", withAuth, userRouter);
mainRouter.use("/onboarding", withOrgAuth, onboardingRouter);
mainRouter.use("/organization", withOrgAuth, orgRouter);
mainRouter.use("/features", withOrgAuth, featureRouter);
mainRouter.use("/credits", withOrgAuth, creditsRouter);
mainRouter.use("/products", withOrgAuth, productRouter);
mainRouter.use("/dev", devRouter);
mainRouter.use("/customers", withOrgAuth, cusRouter);
@@ -85,17 +36,15 @@ mainRouter.use(
"/api/autumn",
withOrgAuth,
(req: any, res: any, next: any) => {
console.log("Autumn middleware:", req.originalUrl);
// console.log("Autumn middleware:", req.originalUrl);
next();
},
autumnHandler({
identify: async (req: any) => {
console.log("Org:", req.minOrg);
console.log("User:", req.user);
return {
customerId: req.minOrg?.id,
customerId: req.org?.id,
customerData: {
name: req.minOrg?.slug,
name: req.org?.slug,
email: req.user?.email,
},
};

View File

@@ -4,10 +4,33 @@ import { SupabaseClient } from "@supabase/supabase-js";
import { getApiVersion } from "@/utils/versionUtils.js";
import { clearOrgCache } from "./orgUtils/clearOrgCache.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { organizations, apiKeys } from "@/db/schema/index.js";
import { eq } from "drizzle-orm";
import { organizations, apiKeys } from "@autumn/shared";
export class OrgService {
// Drizzle get
static async get({ db, orgId }: { db: DrizzleCli; orgId: string }) {
const result = await db.query.organizations.findFirst({
where: eq(organizations.id, orgId),
});
if (!result) {
throw new RecaseError({
message: "Organization not found",
code: ErrCode.OrgNotFound,
statusCode: 404,
});
}
return {
...result,
config: OrgConfigSchema.parse(result.config || {}),
api_version: getApiVersion({
createdAt: result.created_at!,
}),
};
}
static async getWithKeys({
db,
orgId,
@@ -96,6 +119,7 @@ export class OrgService {
return data;
}
static async getFromReq(req: any) {
if (req.org) {
let org = structuredClone(req.org);
@@ -194,17 +218,22 @@ export class OrgService {
orgId: string;
updates: any;
}) {
let result = await db
.update(organizations)
.set(updates)
.where(eq(organizations.id, orgId))
.returning();
try {
let result = await db
.update(organizations)
.set(updates)
.where(eq(organizations.id, orgId))
.returning();
await clearOrgCache({
db,
orgId,
});
await clearOrgCache({
db,
orgId,
});
return result.length > 0 ? result[0] : null;
return result.length > 0 ? result[0] : null;
} catch (error) {
console.error(error);
throw error;
}
}
}

View File

@@ -0,0 +1,52 @@
import { createClerkCli } from "@/external/clerkUtils.js";
import { saveOrgToDB } from "@/external/webhooks/clerkWebhooks.js";
import { Request, Response } from "@/utils/models/Request.js";
import { routeHandler } from "@/utils/routerUtils.js";
export const handlePostOrg = async (req: any, res: any) =>
routeHandler({
req,
res,
action: "create org",
handler: async (req: Request, res: Response) => {
const { userId, db } = req;
const clerk = createClerkCli();
const user = await clerk.users.getUser(userId!);
let userMemberships = await clerk.users.getOrganizationMembershipList({
userId: userId!,
});
let org;
if (userMemberships.data.length === 0) {
org = await clerk.organizations.createOrganization({
name: `${user.firstName}'s Org`,
});
// 2. Create org membership for user
await clerk.organizations.createOrganizationMembership({
organizationId: org.id,
userId: userId!,
role: "org:admin",
});
await saveOrgToDB({
db,
id: org.id,
slug: org.slug,
});
console.log(`Created new org: ${org.id} (${org.slug})`);
} else {
org = userMemberships.data[0].organization;
}
res.status(200).json({
id: org.id,
slug: org.slug,
});
},
});

View File

@@ -1,5 +1,4 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import { entitlements, prices, features, products } from "@/db/schema/index.js";
import { generateId, keyToTitle } from "@/utils/genUtils.js";
import {
FeatureType,
@@ -9,6 +8,12 @@ import {
AllowanceType,
PriceType,
BillingInterval,
// DB Models
entitlements,
prices,
features,
products,
} from "@autumn/shared";
import { AppEnv } from "autumn-js";

View File

@@ -1,15 +1,18 @@
import { Router } from "express";
import { Request } from "@/utils/models/Request.js";
import { chatResults } from "@/db/schema/index.js";
import { eq } from "drizzle-orm";
import { routeHandler } from "@/utils/routerUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { createClerkCli } from "@/external/clerkUtils.js";
import { handleOrgCreated } from "@/external/webhooks/clerkWebhooks.js";
import { OrgService } from "../OrgService.js";
import { AppEnv } from "@autumn/shared";
import { parseChatResultFeatures } from "./parseChatFeatures.js";
import { parseChatProducts } from "./parseChatProducts.js";
import { chatResults } from "@autumn/shared";
import { ProductService } from "@/internal/products/ProductService.js";
import { FeatureService } from "@/internal/features/FeatureService.js";
import { EntitlementService } from "@/internal/products/entitlements/EntitlementService.js";
import { PriceService } from "@/internal/prices/PriceService.js";
export const onboardingRouter = Router();
@@ -19,7 +22,7 @@ onboardingRouter.post("", async (req: Request, res: any) =>
res,
action: "onboarding",
handler: async (req: Request, res: any) => {
const { db, userId } = req;
const { db, sb, logtail: logger, org } = req;
const { token } = req.body;
if (!token) {
@@ -30,7 +33,6 @@ onboardingRouter.post("", async (req: Request, res: any) =>
});
}
// 1. Get chat result
let chatResult = await db.query.chatResults.findFirst({
where: eq(chatResults.id, token),
});
@@ -43,49 +45,69 @@ onboardingRouter.post("", async (req: Request, res: any) =>
});
}
let clerk = createClerkCli();
let user = await clerk.users.getUser(userId!);
// console.log(`Creating default org for user ${user.id}`);
// let org = await clerk.organizations.createOrganization({
// name: `${user.firstName}'s Org`,
// });
// // 2. Create org membership for user
// await clerk.organizations.createOrganizationMembership({
// organizationId: org.id,
// userId: userId!,
// role: "org:admin",
// });
// // 3. Create org in db
// await handleOrgCreated(db, {
// id: org.id,
// slug: org.slug,
// created_at: org.createdAt,
// });
// 4. Create new products
let features = chatResult?.data.features;
let products = chatResult?.data.products;
let backendFeatures = parseChatResultFeatures(features);
let backendProducts = parseChatProducts({
features: backendFeatures,
chatProducts: products,
let curProducts = await ProductService.getFullProducts({
sb,
orgId: org.id,
env: AppEnv.Sandbox,
});
console.log(backendFeatures);
let curFeatures = await FeatureService.list({
db,
orgId: org.id,
env: AppEnv.Sandbox,
});
// for (const feature of features) {
// await ProductService.create({
// sb,
// product: feature,
// });
// }
let newProducts = chatResult.data.products.filter((product) => {
return !curProducts.some((p) => p.id === product.id);
});
let newFeatures = chatResult.data.features.filter((feature) => {
return !curFeatures.some((f) => f.id === feature.id);
});
if (newFeatures.length > 0 || newProducts.length > 0) {
let backendFeatures = parseChatResultFeatures({
features: newFeatures,
orgId: org.id,
});
let { products, prices, ents } = await parseChatProducts({
db,
sb,
logger,
orgId: org.id,
features: [...curFeatures, ...backendFeatures],
chatProducts: newProducts,
});
await Promise.all([
FeatureService.insert({
db,
data: backendFeatures,
logger,
}),
(async () => {
for (const product of products) {
await ProductService.create({ sb, product });
}
})(),
]);
await EntitlementService.insert({
sb,
data: ents,
});
await PriceService.insert({
sb,
data: prices,
});
}
res.status(200).json({
message: "Onboarding successful",
org_id: org.id,
feature_ids: chatResult.data.features.map((f) => f.id),
product_ids: chatResult.data.products.map((p) => p.id),
});
},
}),

View File

@@ -1,10 +1,12 @@
import { validateMeteredConfig } from "@/internal/features/featureUtils.js";
import { constructFeature } from "@/internal/features/utils/constructFeatureUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { keyToTitle } from "@/utils/genUtils.js";
import {
AggregateType,
AppEnv,
ChatFeatureCreditSchema,
ChatResultFeature,
Feature,
FeatureType,
FeatureUsageType,
MeteredConfig,
@@ -46,7 +48,13 @@ const validateFeatures = (features: ChatResultFeature[]) => {
});
};
export const parseChatResultFeatures = (features: ChatResultFeature[]) => {
export const parseChatResultFeatures = ({
features,
orgId,
}: {
features: ChatResultFeature[];
orgId: string;
}) => {
validateFeatures(features);
return features.map((feature) => {
@@ -75,13 +83,15 @@ export const parseChatResultFeatures = (features: ChatResultFeature[]) => {
});
}
let backendFeat: Feature = {
let backendFeat = constructFeature({
id: feature.id,
name: feature.name,
type: type,
name: keyToTitle(feature.id),
type,
env: AppEnv.Sandbox,
config,
orgId: orgId,
display: feature.display,
config: config,
};
});
return backendFeat;
});

View File

@@ -1,27 +1,71 @@
import { DrizzleCli } from "@/db/initDrizzle.js";
import { handleNewProductItems } from "@/internal/products/product-items/productItemInitUtils.js";
import { constructProduct } from "@/internal/products/productUtils.js";
import { generateId } from "@/utils/genUtils.js";
import { Feature, Product, ProductV2 } from "@autumn/shared";
import {
AppEnv,
CreateProductSchema,
EntInsertSchema,
Entitlement,
Feature,
Price,
Product,
ProductV2,
} from "@autumn/shared";
import { SupabaseClient } from "@supabase/supabase-js";
import { z } from "zod";
export const parseChatProducts = ({
export const parseChatProducts = async ({
db,
sb,
logger,
features,
orgId,
chatProducts,
}: {
db: DrizzleCli;
sb: SupabaseClient;
logger: any;
features: Feature[];
orgId: string;
chatProducts: ProductV2[];
}) => {
let products: ProductV2[] = [];
let products: Product[] = [];
let allPrices: Price[] = [];
let allEnts: Entitlement[] = [];
for (const product of chatProducts) {
let backendPro: Product = constructProduct({
productData: {
id: product.id,
name: product.name,
is_add_on: product.is_add_on,
is_default: product.is_default,
},
orgId: orgId,
env: env,
let backendProduct: Product = constructProduct({
productData: CreateProductSchema.parse({
...product,
}),
orgId,
env: AppEnv.Sandbox,
});
products.push();
let { prices, entitlements } = await handleNewProductItems({
db,
sb,
curPrices: [],
curEnts: [],
newItems: product.items,
product: backendProduct,
features,
saveToDb: false,
isCustom: false,
logger,
});
products.push(backendProduct);
allPrices.push(...prices);
allEnts.push(
...entitlements.map((ent) => {
return EntInsertSchema.parse(ent) as unknown as Entitlement;
}),
);
}
return { products, prices: allPrices, ents: allEnts };
};

View File

@@ -3,7 +3,6 @@ import { OrgService } from "../OrgService.js";
import { CacheManager } from "@/external/caching/CacheManager.js";
import { CacheType } from "@/external/caching/cacheActions.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { ApiKey } from "@/db/schema/tables/apiKeysTable.js";
export const clearOrgCache = async ({
// sb,
@@ -30,7 +29,7 @@ export const clearOrgCache = async ({
return;
}
let secretKeys = org.api_keys.map((key: ApiKey) => key.hashed_key);
let secretKeys = org.api_keys.map((key: any) => key.hashed_key);
let publicKeys = [org.test_pkey, org.live_pkey];
let batchDelete = [];

View File

@@ -65,7 +65,7 @@ export const constructPrice = ({
// GET PRICES
const validatePrice = (
price: Price,
relatedEnt?: Entitlement | undefined | null
relatedEnt?: Entitlement | undefined | null,
) => {
if (!price.config?.type) {
throw new RecaseError({
@@ -138,7 +138,7 @@ export const tiersAreSame = (tiers1: any[], tiers2: any[]) => {
export const pricesAreSame = (
price1: Price,
price2: Price,
logDifferences = false
logDifferences = false,
) => {
// if (price1.name !== price2.name) return false;
@@ -170,7 +170,7 @@ export const pricesAreSame = (
"Differences:",
Object.values(diffs)
.filter((d) => d.condition)
.map((d) => d.message)
.map((d) => d.message),
);
}
@@ -208,10 +208,10 @@ export const pricesAreSame = (
usage_tiers: {
condition: !tiersAreSame(
usageConfig1.usage_tiers,
usageConfig2.usage_tiers
usageConfig2.usage_tiers,
),
message: `Usage tiers different: ${usageConfig1.usage_tiers.map(
(t) => `${t.to} (${t.amount})`
(t) => `${t.to} (${t.amount})`,
)} !== ${usageConfig2.usage_tiers.map((t) => `${t.to} (${t.amount})`)}`,
},
// stripe_price_id: {
@@ -245,7 +245,7 @@ export const pricesAreSame = (
"Differences:",
Object.values(diffs)
.filter((d) => d.condition)
.map((d) => d.message)
.map((d) => d.message),
);
}
@@ -328,7 +328,7 @@ export const handleNewPrices = async ({
const feature = features.find((f) => f.id === config.feature_id);
if (!feature) {
throw new RecaseError({
message: `Feature ${config.feature_id} not found for price ${price.name}`,
message: `Feature ${config.feature_id} not found for price (autumn id: ${price.id})`,
code: ErrCode.FeatureNotFound,
statusCode: 400,
});
@@ -345,7 +345,7 @@ export const handleNewPrices = async ({
// 1. Deleted entitlements: filter out entitlements that are not in newEnts
const removedPrices: Price[] = curPrices.filter(
(price) => !newPrices.some((p: Price) => p.id === price.id)
(price) => !newPrices.some((p: Price) => p.id === price.id),
);
const createdPrices: Price[] = [];
@@ -356,7 +356,7 @@ export const handleNewPrices = async ({
const relatedEnt = getPriceEntitlement(
newPrice,
entitlements as EntitlementWithFeature[]
entitlements as EntitlementWithFeature[],
);
validatePrice(newPrice, relatedEnt);
@@ -370,7 +370,7 @@ export const handleNewPrices = async ({
orgId,
internalProductId,
isCustom,
})
}),
);
}
@@ -391,7 +391,7 @@ export const handleNewPrices = async ({
internalProductId,
isCustom,
keepStripePrice: newVersion && pricesAreSame(curPrice, newPrice),
})
}),
);
removedPrices.push(curPrice);
}
@@ -421,7 +421,7 @@ export const handleNewPrices = async ({
if (product.is_default && !isCustom) {
if (
createdPrices.some(
(p) => getBillingType(p.config!) == BillingType.UsageInAdvance
(p) => getBillingType(p.config!) == BillingType.UsageInAdvance,
)
) {
throw new RecaseError({
@@ -455,6 +455,6 @@ export const handleNewPrices = async ({
}
console.log(
`Successfully handled new prices. Created ${createdPrices.length}, updated ${updatedPrices.length}, removed ${removedPrices.length}`
`Successfully handled new prices. Created ${createdPrices.length}, updated ${updatedPrices.length}, removed ${removedPrices.length}`,
);
};

View File

@@ -82,7 +82,7 @@ export class ProductService {
const { data, error } = await sb
.from("products")
.select(
"*, prices(*), entitlements(*, feature:features(*)), free_trial:free_trials(*)"
"*, prices(*), entitlements(*, feature:features(*)), free_trial:free_trials(*)",
)
.eq("org_id", orgId)
.eq("env", env)
@@ -198,7 +198,7 @@ export class ProductService {
),
prices(*),
free_trial:free_trials(*)
`
`,
)
.eq("org_id", orgId)
.eq("env", env)
@@ -271,7 +271,7 @@ export class ProductService {
feature:features (id, name, type)
),
prices (*)
`
`,
);
if (productId) {
@@ -448,7 +448,7 @@ export class ProductService {
if (!data) {
console.log(
"ProductService.getByFeature returning no data, error:",
error
error,
);
return [];
}
@@ -458,41 +458,3 @@ export class ProductService {
return data;
}
}
// static async deleteProduct({
// sb,
// productId,
// orgId,
// env,
// }: {
// sb: SupabaseClient;
// productId: string;
// orgId: string;
// env: AppEnv;
// }) {
// const { error } = await sb
// .from("products")
// .delete()
// .eq("id", productId)
// .eq("org_id", orgId)
// .eq("env", env);
// if (error) {
// throw error;
// }
// }
// UNUSED
// static async getProducts(sb: SupabaseClient, orgId: string, env: AppEnv) {
// const { data, error } = await sb
// .from("products")
// .select("*")
// .eq("org_id", orgId)
// .eq("env", env);
// if (error) {
// throw error;
// }
// return data;
// }

View File

@@ -97,7 +97,7 @@ productRouter.get("/counts", async (req: any, res) => {
sb,
internalProductId: product.internal_id,
});
})
}),
);
// let result: { [key: string]: any } = {};

View File

@@ -9,11 +9,12 @@ import {
} from "@autumn/shared";
import { SupabaseClient } from "@supabase/supabase-js";
import { isBooleanFeatureItem } from "./getItemType.js";
import { validateFeatureId } from "@/internal/features/featureUtils.js";
import {
constructBooleanFeature,
constructMeteredFeature,
validateFeatureId,
} from "@/internal/features/featureUtils.js";
} from "@/internal/features/utils/constructFeatureUtils.js";
import RecaseError from "@/utils/errorUtils.js";
import { nullish } from "@/utils/genUtils.js";

View File

@@ -56,7 +56,6 @@ export const toPrice = ({
org_id: orgId,
internal_product_id: internalProductId,
is_custom: isCustom,
name: "",
config,
};
@@ -106,8 +105,8 @@ export const toFeature = ({
allowance_type: isBoolean
? null
: item.included_usage == Infinite
? AllowanceType.Unlimited
: AllowanceType.Fixed,
? AllowanceType.Unlimited
: AllowanceType.Fixed,
interval: isBoolean ? null : (itemToEntInterval(item) as EntInterval),
@@ -206,8 +205,6 @@ export const toFeatureAndPrice = ({
org_id: orgId,
internal_product_id: internalProductId,
is_custom: isCustom,
name: "",
config,
entitlement_id: ent.id,
};

View File

@@ -153,6 +153,7 @@ export const handleNewProductItems = async ({
logger,
isCustom,
newVersion,
saveToDb = true,
}: {
db: DrizzleCli;
sb: SupabaseClient;
@@ -164,9 +165,9 @@ export const handleNewProductItems = async ({
logger: any;
isCustom: boolean;
newVersion?: boolean;
saveToDb?: boolean;
}) => {
// Create features if not exist...
if (!newItems) {
return {
prices: [],
@@ -175,7 +176,6 @@ export const handleNewProductItems = async ({
}
// Validate product items...
let { allFeatures, newFeatures } = validateProductItems({
newItems,
features,
@@ -258,16 +258,15 @@ export const handleNewProductItems = async ({
`Ents: new(${newEnts.length}), updated(${updatedEnts.length}), deleted(${deletedEnts.length})`,
);
if (newFeatures.length > 0) {
if (newFeatures.length > 0 && saveToDb) {
await FeatureService.insert({
db,
sb,
data: newFeatures,
logger,
});
}
if (isCustom || newVersion) {
if ((isCustom || newVersion) && saveToDb) {
return handleCustomProductItems({
sb,
newPrices,
@@ -280,15 +279,17 @@ export const handleNewProductItems = async ({
});
}
await updateDbPricesAndEnts({
sb,
newPrices,
newEnts,
updatedPrices,
updatedEnts,
deletedPrices,
deletedEnts,
});
if (saveToDb) {
await updateDbPricesAndEnts({
sb,
newPrices,
newEnts,
updatedPrices,
updatedEnts,
deletedPrices,
deletedEnts,
});
}
return {
prices: [...newPrices, ...updatedPrices],

View File

@@ -44,7 +44,7 @@ export const mapToProductItems = ({
let relatedEnt = getPriceEntitlement(
price,
entitlements,
allowFeatureMatch
allowFeatureMatch,
);
if (!relatedEnt) {

View File

@@ -15,7 +15,7 @@ export const pricingMiddleware = async (req: any, res: any, next: any) => {
try {
if (path == "/products" && method == "POST") {
await isEntitled({
minOrg: req.minOrg,
org: req.org,
env: req.env,
featureId: FeatureId.Products,
});
@@ -27,7 +27,7 @@ export const pricingMiddleware = async (req: any, res: any, next: any) => {
if (path == "/products" && method === "POST") {
console.log("sending product create event");
await sendProductEvent({
minOrg: req.minOrg,
org: req.org,
env: req.env,
incrementBy: 1,
});
@@ -36,7 +36,7 @@ export const pricingMiddleware = async (req: any, res: any, next: any) => {
if (path.match(/^\/products\/[^\/]+$/) && method === "DELETE") {
console.log("sending product delete event");
await sendProductEvent({
minOrg: req.minOrg,
org: req.org,
env: req.env,
incrementBy: -1,
});

View File

@@ -1,15 +1,12 @@
import { AppEnv, Feature, MinOrg, Organization } from "@autumn/shared";
import { Logtail } from "@logtail/node";
import { SupabaseClient } from "@supabase/supabase-js";
import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
import type {
Request as ExpressRequest,
Response as ExpressResponse,
} from "express";
import { Client } from "pg";
import * as schema from "@/db/schema/index.js";
import postgres from "postgres";
import { DrizzleCli } from "@/db/initDrizzle.js";
export interface Request extends ExpressRequest {

View File

@@ -5,6 +5,7 @@ import {
Feature,
FeatureType,
FullProduct,
organizations,
Price,
PriceType,
RewardProgram,
@@ -25,6 +26,7 @@ import { CacheManager } from "@/external/caching/CacheManager.js";
import { CacheType } from "@/external/caching/cacheActions.js";
import { hashApiKey } from "@/internal/dev/api-keys/apiKeyUtils.js";
import { initDrizzle } from "@/db/initDrizzle.js";
import { eq } from "drizzle-orm";
export const getAxiosInstance = (
apiKey: string = process.env.UNIT_TEST_AUTUMN_SECRET_KEY!,
@@ -242,7 +244,8 @@ export const setupOrg = async ({
}) => {
const axiosInstance = getAxiosInstance();
const sb = createSupabaseClient();
const db = initDrizzle();
const { client, db } = initDrizzle();
const autumn = new Autumn(process.env.UNIT_TEST_AUTUMN_SECRET_KEY!);
let insertFeatures = [];
@@ -251,7 +254,7 @@ export const setupOrg = async ({
}
await Promise.all(insertFeatures);
const org = await OrgService.getFullOrg({ sb, orgId });
const org = await OrgService.get({ db, orgId });
await OrgService.update({
db,
orgId,
@@ -263,6 +266,8 @@ export const setupOrg = async ({
},
});
await client.end();
const { data: newFeatures } = await sb
.from("features")
.select("*")
@@ -456,14 +461,4 @@ export const setupOrg = async ({
}
await Promise.all(insertRewardTriggers);
console.log("✅ Inserted reward triggers");
// Initialize stripe products
// // How to check if mocha is in parallel mode?
// if (process.env.MOCHA_PARALLEL) {
// console.log("MOCHA RUNNING IN PARALLEL");
// await AutumnCli.initStripeProducts();
// console.log("✅ Initialized stripe products / prices");
// } else {
// console.log("MOCHA RUNNING IN SERIAL");
// }
};

View File

@@ -25,7 +25,22 @@
"@shared/*": ["../shared/*"]
}
},
"include": ["src", "tests", "tests-old/utils.ts", "tests-old/global.ts"],
"include": [
"src",
"tests",
"tests-old/utils.ts",
"tests-old/global.ts",
"../shared/db/allTables.ts",
"../shared/db/apiKeysTable.ts",
"../shared/db/chatResultsTable.ts",
"../shared/db/cusTable.ts",
"../shared/db/entitlementsTable.ts",
"../shared/db/featuresTable.ts",
"../shared/db/invoicesTable.ts",
"../shared/db/orgTable.ts",
"../shared/db/pricesTable.ts",
"../shared/db/productsTable.ts"
],
"references": [
{
"path": "../shared"

View File

@@ -1,6 +1,8 @@
// Old File
import { z } from "zod";
import { OrgConfigSchema } from "./orgConfigModels.js";
import { APIVersion } from "../apiVersionEnum.js";
import { OrgConfigSchema } from "../models/orgModels/orgConfig.js";
import { APIVersion } from "../models/apiVersionEnum.js";
export const MinOrgSchema = z.object({
id: z.string(),
@@ -20,20 +22,13 @@ export const SvixConfigSchema = z.object({
live_app_id: z.string(),
});
export const FrontendOrganizationSchema = z.object({
id: z.string(),
slug: z.string(),
default_currency: z.string(),
stripe_connected: z.boolean().default(false),
created_at: z.number(),
});
export const OrganizationSchema = z.object({
id: z.string(),
slug: z.string(),
default_currency: z.string(),
default_currency: z.string().default("usd"),
stripe_connected: z.boolean().default(false),
stripe_config: StripeConfigSchema.optional().nullable(),
test_pkey: z.string(),
live_pkey: z.string(),
created_at: z.number(),
@@ -47,8 +42,7 @@ export const OrganizationSchema = z.object({
api_version: z.number().nullish(),
});
export type MinOrg = z.infer<typeof MinOrgSchema>;
export type Organization = z.infer<typeof OrganizationSchema>;
export type StripeConfig = z.infer<typeof StripeConfigSchema>;
export type SvixConfig = z.infer<typeof SvixConfigSchema>;
export type MinOrg = z.infer<typeof MinOrgSchema>;
export type FrontendOrganization = z.infer<typeof FrontendOrganizationSchema>;

495
shared/db/allTables.ts Normal file
View File

@@ -0,0 +1,495 @@
// import {
// pgTable,
// text,
// numeric,
// jsonb,
// boolean,
// foreignKey,
// unique,
// primaryKey,
// } from "drizzle-orm/pg-core";
// import { organizations } from "./orgTable.js";
// export const rewards = pgTable(
// "rewards",
// {
// internalId: text("internal_id").primaryKey().notNull(),
// env: text(),
// name: text(),
// orgId: text("org_id"),
// createdAt: numeric("created_at"),
// discountConfig: jsonb("discount_config"),
// freeProductId: text("free_product_id"),
// id: text(),
// promoCodes: jsonb("promo_codes").array(),
// type: text(),
// },
// (table) => [
// foreignKey({
// columns: [table.orgId],
// foreignColumns: [organizations.id],
// name: "coupons_org_id_fkey",
// }).onDelete("cascade"),
// ],
// );
// export const rewardPrograms = pgTable(
// "reward_programs",
// {
// internalId: text("internal_id").primaryKey().notNull(),
// id: text(),
// createdAt: numeric("created_at").notNull(),
// internalRewardId: text("internal_reward_id"),
// maxRedemptions: numeric("max_redemptions"),
// unlimitedRedemptions: boolean("unlimited_redemptions").default(false),
// orgId: text("org_id"),
// env: text(),
// when: text().default("immediately"),
// productIds: text("product_ids").array().default([""]),
// excludeTrial: boolean("exclude_trial").default(false),
// receivedBy: text("received_by"),
// },
// (table) => [
// foreignKey({
// columns: [table.internalRewardId],
// foreignColumns: [rewards.internalId],
// name: "reward_triggers_internal_reward_id_fkey",
// }).onDelete("cascade"),
// foreignKey({
// columns: [table.orgId],
// foreignColumns: [organizations.id],
// name: "reward_triggers_org_id_fkey",
// }).onDelete("cascade"),
// ],
// );
// export const customerPrices = pgTable(
// "customer_prices",
// {
// id: text().primaryKey().notNull(),
// createdAt: numeric("created_at").notNull(),
// customerProductId: text("customer_product_id"),
// priceId: text("price_id"),
// options: jsonb(),
// internalCustomerId: text("internal_customer_id"),
// },
// (table) => [
// foreignKey({
// columns: [table.customerProductId],
// foreignColumns: [customerProducts.id],
// name: "customer_prices_customer_product_id_fkey",
// }).onDelete("cascade"),
// foreignKey({
// columns: [table.internalCustomerId],
// foreignColumns: [customers.internalId],
// name: "customer_prices_internal_customer_id_fkey",
// }).onDelete("cascade"),
// foreignKey({
// columns: [table.priceId],
// foreignColumns: [prices.id],
// name: "customer_prices_price_id_fkey",
// }),
// ],
// );
// export const entities = pgTable(
// "entities",
// {
// internalId: text("internal_id").primaryKey().notNull(),
// orgId: text("org_id"),
// env: text(),
// internalCustomerId: text("internal_customer_id"),
// createdAt: numeric("created_at").notNull(),
// id: text(),
// name: text(),
// deleted: boolean().default(false).notNull(),
// internalFeatureId: text("internal_feature_id"),
// featureId: text("feature_id"),
// },
// (table) => [
// foreignKey({
// columns: [table.internalCustomerId],
// foreignColumns: [customers.internalId],
// name: "entities_internal_customer_id_fkey",
// }).onDelete("cascade"),
// foreignKey({
// columns: [table.internalFeatureId],
// foreignColumns: [features.internal_id],
// name: "entities_internal_feature_id_fkey",
// }).onDelete("cascade"),
// foreignKey({
// columns: [table.orgId],
// foreignColumns: [organizations.id],
// name: "entities_org_id_fkey",
// }).onDelete("cascade"),
// unique("entity_id_constraint").on(
// table.orgId,
// table.env,
// table.internalCustomerId,
// table.id,
// ),
// ],
// );
// // export const entitlements = pgTable(
// // "entitlements",
// // {
// // createdAt: numeric("created_at").notNull(),
// // internalFeatureId: text("internal_feature_id"),
// // orgId: text("org_id"),
// // internalProductId: text("internal_product_id"),
// // allowanceType: text("allowance_type"),
// // allowance: numeric(),
// // interval: text(),
// // id: text().primaryKey().notNull(),
// // featureId: text("feature_id"),
// // isCustom: boolean("is_custom").default(false),
// // carryFromPrevious: boolean("carry_from_previous").default(false),
// // entityFeatureId: text("entity_feature_id"),
// // },
// // (table) => [
// // foreignKey({
// // columns: [table.internalFeatureId],
// // foreignColumns: [features.internal_id],
// // name: "entitlements_internal_feature_id_fkey",
// // }).onDelete("cascade"),
// // foreignKey({
// // columns: [table.internalProductId],
// // foreignColumns: [products.internalId],
// // name: "entitlements_internal_product_id_fkey",
// // })
// // .onUpdate("cascade")
// // .onDelete("cascade"),
// // unique("entitlements_id_key").on(table.id),
// // ],
// // );
// export const events = pgTable(
// "events",
// {
// id: text().primaryKey().notNull(),
// orgId: text("org_id").notNull(),
// timestamp: numeric().notNull(),
// env: text().notNull(),
// customerId: text("customer_id").notNull(),
// eventName: text("event_name").notNull(),
// properties: jsonb(),
// idempotencyKey: text("idempotency_key"),
// internalCustomerId: text("internal_customer_id"),
// value: numeric(),
// setUsage: boolean("set_usage").default(false),
// entityId: text("entity_id"),
// },
// (table) => [
// foreignKey({
// columns: [table.internalCustomerId],
// foreignColumns: [customers.internalId],
// name: "events_internal_customer_id_fkey",
// }).onDelete("cascade"),
// unique("unique_event_constraint").on(
// table.orgId,
// table.env,
// table.customerId,
// table.eventName,
// table.idempotencyKey,
// ),
// ],
// );
// export const freeTrials = pgTable(
// "free_trials",
// {
// id: text().primaryKey().notNull(),
// createdAt: numeric("created_at").notNull(),
// duration: text().default("day"),
// length: numeric(),
// internalProductId: text("internal_product_id"),
// uniqueFingerprint: boolean("unique_fingerprint"),
// isCustom: boolean("is_custom").default(false),
// },
// (table) => [
// foreignKey({
// columns: [table.internalProductId],
// foreignColumns: [products.internalId],
// name: "free_trials_internal_product_id_fkey",
// }).onDelete("cascade"),
// ],
// );
// export const invoiceItems = pgTable(
// "invoice_items",
// {
// createdAt: numeric("created_at").notNull(),
// updatedAt: numeric("updated_at"),
// customerPriceId: text("customer_price_id"),
// periodStart: numeric("period_start"),
// periodEnd: numeric("period_end"),
// prorationStart: numeric("proration_start"),
// prorationEnd: numeric("proration_end"),
// quantity: numeric(),
// amount: numeric(),
// currency: text(),
// id: text().primaryKey().notNull(),
// addedToStripe: boolean("added_to_stripe").default(false),
// customerId: text("customer_id"),
// },
// (table) => [
// foreignKey({
// columns: [table.customerPriceId],
// foreignColumns: [customerPrices.id],
// name: "invoice_items_customer_price_id_fkey",
// }).onDelete("cascade"),
// unique("invoice_items_id_key").on(table.id),
// ],
// );
// export const customers = pgTable(
// "customers",
// {
// name: text().default(""),
// orgId: text("org_id").notNull(),
// createdAt: numeric("created_at").notNull(),
// internalId: text("internal_id").primaryKey().notNull(),
// id: text(),
// env: text(),
// processor: jsonb(),
// email: text().default(""),
// fingerprint: text(),
// metadata: jsonb().default({}),
// },
// (table) => [unique("cus_id_constraint").on(table.orgId, table.id, table.env)],
// );
// export const customerProducts = pgTable(
// "customer_products",
// {
// id: text().primaryKey().notNull(),
// internalCustomerId: text("internal_customer_id").notNull(),
// customerId: text("customer_id"),
// internalProductId: text("internal_product_id"),
// createdAt: numeric("created_at"),
// status: text(),
// processor: jsonb(),
// canceledAt: numeric("canceled_at"),
// endedAt: numeric("ended_at"),
// startsAt: numeric("starts_at"),
// options: jsonb().array(),
// productId: text("product_id"),
// freeTrialId: text("free_trial_id"),
// trialEndsAt: numeric("trial_ends_at"),
// collectionMethod: text("collection_method").default("charge_automatically"),
// subscriptionIds: text("subscription_ids").array(),
// scheduledIds: text("scheduled_ids").array(),
// isCustom: boolean("is_custom").default(false).notNull(),
// quantity: numeric().default("1"),
// internalEntityId: text("internal_entity_id"),
// entityId: text("entity_id"),
// },
// (table) => [
// foreignKey({
// columns: [table.freeTrialId],
// foreignColumns: [freeTrials.id],
// name: "customer_products_free_trial_id_fkey",
// }),
// foreignKey({
// columns: [table.internalCustomerId],
// foreignColumns: [customers.internalId],
// name: "customer_products_internal_customer_id_fkey",
// })
// .onUpdate("cascade")
// .onDelete("cascade"),
// foreignKey({
// columns: [table.internalEntityId],
// foreignColumns: [entities.internalId],
// name: "customer_products_internal_entity_id_fkey",
// }).onDelete("set null"),
// foreignKey({
// columns: [table.internalProductId],
// foreignColumns: [products.internalId],
// name: "customer_products_internal_product_id_fkey",
// }),
// ],
// );
// export const metadata = pgTable("metadata", {
// id: text().primaryKey().notNull(),
// createdAt: numeric("created_at").notNull(),
// expiresAt: numeric("expires_at"),
// data: jsonb(),
// });
// export const products = pgTable(
// "products",
// {
// internalId: text("internal_id").primaryKey().notNull(),
// createdAt: numeric("created_at").notNull(),
// name: text(),
// orgId: text("org_id"),
// env: text(),
// isAddOn: boolean("is_add_on"),
// processor: jsonb(),
// isDefault: boolean("is_default").default(false),
// id: text(),
// group: text().default(""),
// version: numeric().default("1"),
// },
// (table) => [
// foreignKey({
// columns: [table.orgId],
// foreignColumns: [organizations.id],
// name: "products_org_id_fkey",
// }).onDelete("cascade"),
// ],
// );
// export const rewardRedemptions = pgTable(
// "reward_redemptions",
// {
// id: text().primaryKey().notNull(),
// createdAt: numeric("created_at").notNull(),
// updatedAt: numeric("updated_at"),
// internalCustomerId: text("internal_customer_id"),
// triggered: boolean(),
// internalRewardProgramId: text("internal_reward_program_id"),
// applied: boolean().default(false),
// referralCodeId: text("referral_code_id"),
// },
// (table) => [
// foreignKey({
// columns: [table.internalCustomerId],
// foreignColumns: [customers.internalId],
// name: "reward_redemptions_internal_customer_id_fkey",
// }).onDelete("cascade"),
// foreignKey({
// columns: [table.internalRewardProgramId],
// foreignColumns: [rewardPrograms.internalId],
// name: "reward_redemptions_internal_reward_program_id_fkey",
// }).onDelete("cascade"),
// foreignKey({
// columns: [table.referralCodeId],
// foreignColumns: [referralCodes.id],
// name: "reward_redemptions_referral_code_id_fkey",
// }).onDelete("cascade"),
// ],
// );
// export const migrationJobs = pgTable(
// "migration_jobs",
// {
// id: text().primaryKey().notNull(),
// createdAt: numeric("created_at").notNull(),
// updatedAt: numeric("updated_at"),
// currentStep: text("current_step"),
// fromInternalProductId: text("from_internal_product_id"),
// toInternalProductId: text("to_internal_product_id"),
// stepDetails: jsonb("step_details"),
// orgId: text("org_id"),
// env: text(),
// },
// (table) => [
// foreignKey({
// columns: [table.fromInternalProductId],
// foreignColumns: [products.internalId],
// name: "migration_jobs_from_internal_product_id_fkey",
// }).onDelete("cascade"),
// foreignKey({
// columns: [table.orgId],
// foreignColumns: [organizations.id],
// name: "migration_jobs_org_id_fkey",
// }).onDelete("cascade"),
// foreignKey({
// columns: [table.toInternalProductId],
// foreignColumns: [products.internalId],
// name: "migration_jobs_to_internal_product_id_fkey",
// }).onDelete("cascade"),
// ],
// );
// export const subscriptions = pgTable(
// "subscriptions",
// {
// id: text().primaryKey().notNull(),
// stripeId: text("stripe_id"),
// stripeScheduleId: text("stripe_schedule_id"),
// createdAt: numeric("created_at"),
// metadata: jsonb().default({}),
// usageFeatures: text("usage_features").array().default([""]),
// orgId: text("org_id"),
// env: text(),
// currentPeriodStart: numeric("current_period_start"),
// currentPeriodEnd: numeric("current_period_end"),
// },
// (table) => [
// foreignKey({
// columns: [table.orgId],
// foreignColumns: [organizations.id],
// name: "subscriptions_org_id_fkey",
// }).onDelete("cascade"),
// unique("subscriptions_stripe_id_key").on(table.stripeId),
// ],
// );
// export const referralCodes = pgTable(
// "referral_codes",
// {
// code: text().notNull(),
// orgId: text("org_id").notNull(),
// env: text().notNull(),
// internalCustomerId: text("internal_customer_id"),
// internalRewardProgramId: text("internal_reward_program_id"),
// id: text().notNull(),
// createdAt: numeric("created_at"),
// },
// (table) => [
// foreignKey({
// columns: [table.internalCustomerId],
// foreignColumns: [customers.internalId],
// name: "referral_codes_internal_customer_id_fkey",
// }).onDelete("cascade"),
// foreignKey({
// columns: [table.internalRewardProgramId],
// foreignColumns: [rewardPrograms.internalId],
// name: "referral_codes_internal_reward_program_id_fkey",
// }).onDelete("cascade"),
// foreignKey({
// columns: [table.orgId],
// foreignColumns: [organizations.id],
// name: "referral_codes_org_id_fkey",
// }).onDelete("cascade"),
// primaryKey({
// columns: [table.code, table.orgId, table.env],
// name: "referral_codes_pkey",
// }),
// unique("referral_codes_id_key").on(table.id),
// ],
// );
// export const migrationErrors = pgTable(
// "migration_errors",
// {
// internalCustomerId: text("internal_customer_id").notNull(),
// migrationJobId: text("migration_job_id").notNull(),
// createdAt: numeric("created_at"),
// updatedAt: numeric("updated_at"),
// data: jsonb(),
// message: text(),
// code: text(),
// },
// (table) => [
// foreignKey({
// columns: [table.internalCustomerId],
// foreignColumns: [customers.internalId],
// name: "migration_customers_internal_customer_id_fkey",
// }).onDelete("cascade"),
// foreignKey({
// columns: [table.migrationJobId],
// foreignColumns: [migrationJobs.id],
// name: "migration_customers_migration_job_id_fkey",
// }).onDelete("cascade"),
// primaryKey({
// columns: [table.internalCustomerId, table.migrationJobId],
// name: "migration_errors_pkey",
// }),
// ],
// );

View File

@@ -7,7 +7,7 @@ import {
unique,
} from "drizzle-orm/pg-core";
import { organizations } from "../index.js";
import { organizations } from "../models/orgModels/orgTable.js";
export const apiKeys = pgTable(
"api_keys",

View File

@@ -1,5 +1,5 @@
import { pgTable, text, numeric, jsonb, unique } from "drizzle-orm/pg-core";
import { collatePgColumn } from "../schemaUtils.js";
import { collatePgColumn } from "./utils.js";
export const customers = pgTable(
"customers",

View File

@@ -7,7 +7,7 @@ import {
jsonb,
text,
} from "drizzle-orm/pg-core";
import { entitlements } from "./entitlementsTable.js";
import { entitlements } from "../models/productModels/entModels/entTable.js";
import { products } from "./productsTable.js";
export const prices = pgTable(

View File

@@ -6,9 +6,11 @@ import {
pgTable,
text,
} from "drizzle-orm/pg-core";
import { organizations } from "../index.js";
import { sql } from "drizzle-orm";
import { sqlNow } from "../schemaUtils.js";
import { sqlNow } from "./utils.js";
import { relations } from "drizzle-orm";
import { organizations } from "../models/orgModels/orgTable.js";
import { entitlements } from "../models/productModels/entModels/entTable.js";
import { prices } from "./pricesTable.js";
export const products = pgTable(
"products",
@@ -33,3 +35,12 @@ export const products = pgTable(
}).onDelete("cascade"),
],
);
export const productsRelations = relations(products, ({ one, many }) => ({
org: one(organizations, {
fields: [products.org_id],
references: [organizations.id],
}),
entitlements: many(entitlements),
prices: many(prices),
}));

View File

@@ -1,9 +1,7 @@
import { relations } from "drizzle-orm";
import { apiKeys, organizations } from "./schema/index.js";
import { apiKeys } from "./apiKeysTable.js";
export const organizationsRelations = relations(organizations, ({ many }) => ({
api_keys: many(apiKeys),
}));
import { organizations } from "../models/orgModels/orgTable.js";
import { relations } from "drizzle-orm";
export const apiKeysRelations = relations(apiKeys, ({ one }) => ({
organization: one(organizations, {

47
shared/db/schema.ts Normal file
View File

@@ -0,0 +1,47 @@
import { organizations } from "../models/orgModels/orgTable.js";
import { chatResults } from "../models/chatResultModels/chatResultTable.js";
import { entitlements } from "../models/productModels/entModels/entTable.js";
import { features } from "../models/featureModels/featureTable.js";
import { apiKeys } from "./apiKeysTable.js";
import { customers } from "./cusTable.js";
import { prices } from "./pricesTable.js";
import { products } from "./productsTable.js";
// import * as relations from "./relations.js";
// Relations
import { organizationsRelations } from "../models/orgModels/orgRelations.js";
import { apiKeysRelations } from "./relations.js";
import { entitlementsRelations } from "../models/productModels/entModels/entRelations.js";
import { featureRelations } from "../models/featureModels/featureRelations.js";
const relations = {
organizationsRelations,
apiKeysRelations,
entitlementsRelations,
featureRelations,
};
export const schemas = {
apiKeys,
customers,
chatResults,
prices,
organizations,
entitlements,
features,
products,
...relations,
};
export {
apiKeys,
organizations,
customers,
chatResults,
prices,
entitlements,
features,
products,
};

View File

@@ -1,3 +0,0 @@
export const InternalErrCode = {
UpdateBalanceFailed: "update_balance_failed",
};

View File

@@ -1,9 +1,23 @@
// Feature Models
// Schemas
export * from "./db/schema.js";
// 1. Org Models
export * from "./models/orgModels/orgTable.js";
export * from "./models/orgModels/orgConfig.js";
// 2. Feature Models
export * from "./models/featureModels/featureTable.js";
export * from "./models/featureModels/featureEnums.js";
export * from "./models/featureModels/featureModels.js";
export * from "./models/featureModels/meteredModels.js";
export * from "./models/featureModels/dbConnModels.js";
export * from "./models/featureModels/creditModels.js";
export * from "./models/featureModels/featureConfig/meteredConfig.js";
export * from "./models/featureModels/featureConfig/creditConfig.js";
// 3. Entitlement Models
export * from "./models/productModels/entModels/entTable.js";
// 3. Chat Result Models
export * from "./models/chatResultModels/chatResultTable.js";
export * from "./models/chatResultModels/chatResultFeature.js";
// Gen Models
export * from "./models/genModels.js";
@@ -26,11 +40,6 @@ export * from "./models/cusModels/cusPriceModels/cusPriceModels.js";
export * from "./models/cusModels/invoiceModels/invoiceModels.js";
export * from "./models/cusModels/cusResponseModels.js";
// Error Codes
export * from "./errors/errCode.js";
export * from "./errors/internalErrCode.js";
export * from "./logger/LoggerAction.js";
// Entity Models
export * from "./models/cusModels/entityModels/entityModels.js";
@@ -51,7 +60,6 @@ export * from "./models/productModels/productItemModels.js";
export * from "./models/productModels/productV2Models.js";
export * from "./models/productModels/productResponseModels.js";
export * from "./models/apiVersionEnum.js";
export * from "./errors/SuccessCode.js";
export * from "./models/productModels/productItemModels/prodItemResponseModels.js";
export * from "./models/cusModels/fullCusModel.js";
@@ -72,6 +80,11 @@ export * from "./models/checkModels/checkPreviewModels.js";
export * from "./models/rewardModels/rewardResponseModels.js";
// Org Models
export * from "./models/orgModels/orgModels.js";
export * from "./models/orgModels/orgConfigModels.js";
export * from "./models/orgModels/chatResultModels.js";
// export * from "./models/orgModels/orgModels.js";
export * from "./models/chatResultModels/chatResultFeature.js";
// ENUMS
export * from "./enums/SuccessCode.js";
export * from "./enums/ErrCode.js";
export * from "./enums/LoggerAction.js";

View File

@@ -1,29 +0,0 @@
export type LogEntry = {
level: "info" | "error" | "warn" | "debug";
name: string;
msg: string;
timestamp: number;
};
export type LogGroup = {
name: string;
id: string;
logs: LogEntry[];
};
export type WorkflowRun = {
id: string;
created_at: number;
workspace_id: string;
logs: LogGroup;
inputs: Record<string, any>;
status: "running" | "completed" | "failed";
workflow: {
id: string;
external_id: string;
};
// status: "pending" | "running" | "completed" | "failed";
};

View File

@@ -1,22 +0,0 @@
export type Workflow = {
id?: string;
workspace_id?: string;
created_at?: number;
last_updated?: number;
external_id?: string;
inputs?: Record<string, unknown>;
file_contents?: string;
env?: string | null;
package_json?: string | null;
typescript_contents?: string;
};
// export enum WorkflowInputType {
// String = "String",
// }
export type WorkflowInput = {
name: string;
type: "string" | "number" | "boolean" | "object";
};

View File

@@ -1,6 +1,7 @@
import { pgTable, text, numeric, jsonb, boolean } from "drizzle-orm/pg-core";
import { collatePgColumn } from "../schemaUtils.js";
import { ChatResultFeature, ProductV2 } from "@autumn/shared";
import { collatePgColumn } from "../../db/utils.js";
export const chatResults = pgTable("chat_results", {
id: text("id").primaryKey(),
@@ -11,7 +12,6 @@ export const chatResults = pgTable("chat_results", {
products: ProductV2[];
}>()
.notNull(),
processed: boolean("processed").default(false),
}).enableRLS();
collatePgColumn(chatResults.id, "C");

View File

@@ -1,25 +0,0 @@
import { FeatureUsageType } from "./meteredModels.js";
export type CreditSchemaItem = {
metered_feature_id: string;
feature_amount: number;
credit_amount: number;
};
// export type CreditSchema = {
// items: CreditSchemaItem[];
// };
export type CreditSystemConfig = {
schema: CreditSchemaItem[];
usage_type: FeatureUsageType;
};
export type CreditSystem = {
internal_id: string;
org_id: string;
id: string;
name: string;
created_at: number;
config: CreditSystemConfig;
};

View File

@@ -1,12 +0,0 @@
export enum DBConnectionProvider {
Postgres = "postgres",
}
export interface DBConnection {
id: string;
org_id: string;
provider: DBConnectionProvider;
display_name: string;
connection_string: string;
created_at: number;
}

View File

@@ -0,0 +1,22 @@
import { FeatureUsageType } from "../featureEnums.js";
import { z } from "zod";
export const CreditSchemaItemSchema = z.object({
metered_feature_id: z.string(),
feature_amount: z.number(),
credit_amount: z.number(),
});
export const CreditSystemConfigSchema = z.object({
schema: z.array(
z.object({
metered_feature_id: z.string(),
feature_amount: z.number(),
credit_amount: z.number(),
}),
),
usage_type: z.nativeEnum(FeatureUsageType),
});
export type CreditSystemConfig = z.infer<typeof CreditSystemConfigSchema>;
export type CreditSchemaItem = z.infer<typeof CreditSchemaItemSchema>;

View File

@@ -1,4 +1,5 @@
import { z } from "zod";
import { FeatureUsageType } from "../featureEnums.js";
export interface Expression {
property: string;
@@ -22,15 +23,6 @@ export const AggregateSchema = z.object({
property: z.string().nullable(),
});
export const GroupBySchema = z.object({
property: z.string(),
});
export enum FeatureUsageType {
Single = "single_use",
Continuous = "continuous_use",
}
export const MeteredConfigSchema = z.object({
filters: z.array(ExpressionSchema),
aggregate: AggregateSchema,

View File

@@ -0,0 +1,15 @@
export enum FeatureType {
Boolean = "boolean",
Metered = "metered",
CreditSystem = "credit_system",
}
export enum AggregateType {
Count = "count",
Sum = "sum",
}
export enum FeatureUsageType {
Single = "single_use",
Continuous = "continuous_use",
}

View File

@@ -1,16 +1,6 @@
import { z } from "zod";
import { AppEnv } from "../genModels.js";
export enum FeatureType {
Boolean = "boolean",
Metered = "metered",
CreditSystem = "credit_system",
}
export enum AggregateType {
Count = "count",
Sum = "sum",
}
import { FeatureType } from "./featureEnums.js";
export const FeatureSchema = z.object({
internal_id: z.string().optional(),

View File

@@ -0,0 +1,10 @@
import { relations } from "drizzle-orm";
import { organizations } from "../orgModels/orgTable.js";
import { features } from "./featureTable.js";
export const featureRelations = relations(features, ({ one }) => ({
org: one(organizations, {
fields: [features.org_id],
references: [organizations.id],
}),
}));

View File

@@ -6,8 +6,12 @@ import {
text,
unique,
} from "drizzle-orm/pg-core";
import { organizations } from "../index.js";
import { relations } from "drizzle-orm";
import { organizations } from "../orgModels/orgTable.js";
import { MeteredConfig } from "./featureConfig/meteredConfig.js";
import { CreditSystemConfig } from "./featureConfig/creditConfig.js";
import { collatePgColumn } from "../../db/utils.js";
export const features = pgTable(
"features",
@@ -17,8 +21,8 @@ export const features = pgTable(
id: text().notNull(),
name: text(),
type: text(),
created_at: numeric("created_at"),
config: jsonb(),
created_at: numeric({ mode: "number" }),
config: jsonb().$type<MeteredConfig | CreditSystemConfig>(),
env: text().default("live"),
display: jsonb(),
},
@@ -31,3 +35,5 @@ export const features = pgTable(
unique("feature_id_constraint").on(table.org_id, table.id, table.env),
],
);
collatePgColumn(features.internal_id, "C");

View File

@@ -0,0 +1,9 @@
import { relations } from "drizzle-orm";
import { apiKeys } from "../../db/apiKeysTable.js";
import { features } from "../featureModels/featureTable.js";
import { organizations } from "./orgTable.js";
export const organizationsRelations = relations(organizations, ({ many }) => ({
api_keys: many(apiKeys),
features: many(features),
}));

View File

@@ -0,0 +1,52 @@
import {
pgTable,
text,
numeric,
jsonb,
boolean,
unique,
} from "drizzle-orm/pg-core";
import { OrgConfig } from "./orgConfig.js";
export type MinOrg = {
id: string;
slug: string;
};
export type SvixConfig = {
sandbox_app_id: string;
live_app_id: string;
};
export type StripeConfig = {
test_api_key: string;
live_api_key: string;
test_webhook_secret: string;
live_webhook_secret: string;
success_url: string;
};
export const organizations = pgTable(
"organizations",
{
id: text().primaryKey().notNull(),
slug: text().notNull(),
default_currency: text("default_currency").notNull().default("usd"),
stripe_connected: boolean("stripe_connected").default(false),
stripe_config: jsonb("stripe_config").$type<StripeConfig>(),
test_pkey: text("test_pkey"),
live_pkey: text("live_pkey"),
svix_config: jsonb("svix_config").notNull().$type<SvixConfig>(),
created_at: numeric({ mode: "number" }),
config: jsonb().default({}).notNull().$type<OrgConfig>(),
},
(table) => [
unique("organizations_test_pkey_key").on(table.test_pkey),
unique("organizations_live_pkey_key").on(table.live_pkey),
],
);
export type Organization = typeof organizations.$inferSelect & {
api_version: number;
};

View File

@@ -0,0 +1,11 @@
import { relations } from "drizzle-orm";
import { features } from "../../featureModels/featureTable.js";
import { entitlements } from "./entTable.js";
export const entitlementsRelations = relations(entitlements, ({ one }) => ({
feature: one(features, {
fields: [entitlements.internal_feature_id],
references: [features.internal_id],
}),
}));

View File

@@ -5,24 +5,28 @@ import {
foreignKey,
unique,
text,
integer,
} from "drizzle-orm/pg-core";
import { products, features } from "../index.js";
import { features } from "../../featureModels/featureTable.js";
import { products } from "../../../db/productsTable.js";
import { createInsertSchema } from "drizzle-zod";
export const entitlements = pgTable(
"entitlements",
{
created_at: numeric("created_at").notNull(),
internal_feature_id: text("internal_feature_id"),
org_id: text("org_id"),
internal_product_id: text("internal_product_id"),
allowance_type: text("allowance_type"),
allowance: numeric(),
allowance: numeric({ mode: "number" }),
interval: text(),
id: text().primaryKey().notNull(),
feature_id: text("feature_id"),
is_custom: boolean("is_custom").default(false),
carry_from_previous: boolean("carry_from_previous").default(false),
entity_feature_id: text("entity_feature_id"),
created_at: numeric("created_at", { mode: "number" }).notNull(),
},
(table) => [
foreignKey({
@@ -40,3 +44,5 @@ export const entitlements = pgTable(
unique("entitlements_id_key").on(table.id),
],
);
export const EntInsertSchema = createInsertSchema(entitlements);

View File

@@ -1,5 +1,6 @@
import { z } from "zod";
import { FeatureSchema, FeatureType } from "../featureModels/featureModels.js";
import { FeatureSchema } from "../featureModels/featureModels.js";
import { FeatureType } from "../featureModels/featureEnums.js";
import { EntInterval } from "../genModels.js";
import { UsagePriceConfigSchema } from "./usagePriceModels.js";

View File

@@ -25,7 +25,7 @@ export const PriceSchema = z.object({
billing_type: z.nativeEnum(BillingType).nullish(),
is_custom: z.boolean().optional(),
name: z.string().optional(),
// name: z.string().optional(),
config: FixedPriceConfigSchema.or(UsagePriceConfigSchema).optional(),
entitlement_id: z.string().nullish(),
});
@@ -33,7 +33,7 @@ export const PriceSchema = z.object({
export type Price = z.infer<typeof PriceSchema>;
export const CreatePriceSchema = z.object({
name: z.string().nonempty(),
// name: z.string().nonempty(),
config: FixedPriceConfigSchema.or(UsagePriceConfigSchema),
});

View File

@@ -16,7 +16,7 @@
"dependencies": {
"drizzle-orm": "^0.43.1",
"drizzle-zod": "^0.8.1",
"zod": "^3.x"
"zod": "^3.25.23"
},
"devDependencies": {
"typescript": "^5.7.2"

View File

@@ -19,6 +19,7 @@ import { Button } from "@/components/ui/button";
import { ArrowUpRightFromSquare } from "lucide-react";
import { AutumnProvider } from "autumn-js/react";
import { useAuth } from "@clerk/clerk-react";
export function MainLayout() {
const { isLoaded: isUserLoaded, user } = useUser();
const { organization: org } = useOrganization();
@@ -157,7 +158,7 @@ const MainContent = () => {
)}
<div
className={cn(
"w-full h-full overflow-auto flex justify-center bg-stone-50"
"w-full h-full overflow-auto flex justify-center bg-stone-50",
)}
>
<div className="hidden md:flex w-full h-full justify-center">

View File

@@ -9,19 +9,21 @@ import { useState } from "react";
export const AdminHover = ({
children,
texts,
hide = false,
}: {
children: React.ReactNode;
texts: (string | { key: string; value: string } | undefined | null)[];
hide?: boolean;
}) => {
const { isLoaded, user } = useUser();
const email = user?.primaryEmailAddress?.emailAddress;
let isAdmin =
const isAdmin =
email === "johnyeocx@gmail.com" ||
email === "ayush@recaseai.com" ||
email === "johnyeo10@gmail.com" ||
email == "npmrundemo@gmail.com";
if (!isAdmin) return children;
if (!isAdmin || hide) return children;
return (
<TooltipProvider>

View File

@@ -15,7 +15,7 @@ function Step({ title, children, className, description, number }: StepProps) {
<div
className={cn(
"relative pl-8 pb-8 border-l border-stone-200 gap-4 flex flex-col",
className
className,
)}
>
<div className="absolute -left-[17px] -top-1 flex items-center justify-center w-8 h-8 rounded-full bg-stone-50 border">
@@ -28,7 +28,7 @@ function Step({ title, children, className, description, number }: StepProps) {
<div className="flex flex-col gap-4 w-full lg:w-1/3">
<h1 className="text-t1 text-md font-medium">{title}</h1>
{description && (
<div className="text-t2 flex flex-col gap-4 w-full">
<div className="text-t2/70 flex flex-col gap-4 w-full">
{description}
</div>
)}

View File

@@ -1,9 +1,28 @@
export const PageSectionHeader = ({ title }: { title: string }) => {
import { cn } from "@/lib/utils";
export const PageSectionHeader = ({
title,
isOnboarding = false,
addButton,
className,
}: {
title: string;
isOnboarding?: boolean;
addButton?: React.ReactNode;
className?: string;
}) => {
return (
<div className="sticky top-0 z-10 border-y bg-stone-100 pl-10 pr-7 h-10 flex justify-between items-center">
<div
className={cn(
"sticky top-0 z-10 border-y bg-stone-100 pl-10 pr-7 h-10 flex justify-between items-center",
isOnboarding && "px-2",
className,
)}
>
<div className="flex items-center gap-2">
<h2 className="text-sm text-t2 font-medium">{title}</h2>
</div>
{addButton && <div className="flex items-center">{addButton}</div>}
</div>
);
};

View File

@@ -5,12 +5,14 @@ export const Row = ({
className,
type,
onClick,
isOnboarding = false,
...props
}: {
children?: React.ReactNode;
className?: string;
type?: "header" | "body";
onClick?: () => void;
isOnboarding?: boolean;
props?: React.ComponentProps<"div">;
}) => {
return (
@@ -19,7 +21,8 @@ export const Row = ({
"grid grid-cols-[repeat(auto-fit,_minmax(0,_1fr))] gap-2 w-full px-10 h-8 items-center hover:bg-primary/5 whitespace-nowrap",
type === "header" &&
"text-xs text-t3 h-8 -mb-1 items-center hover:bg-primary/0",
className
isOnboarding && "px-2",
className,
)}
onClick={onClick}
{...props}
@@ -40,7 +43,7 @@ export const Item = ({
<div
className={cn(
"col-span-1 flex h-full w-full items-center gap-2 truncate",
className
className,
)}
>
{children}

View File

@@ -3,13 +3,13 @@ import axios, { AxiosInstance } from "axios";
export class ProductService {
static async createProduct(axiosInstance: AxiosInstance, data: any) {
const response = await axiosInstance.post("/v1/products", data);
return response.data.id;
return response.data;
}
static async updateProduct(
axiosInstance: AxiosInstance,
productId: string,
data: any
data: any,
) {
await axiosInstance.post(`/v1/products/${productId}`, data);
}
@@ -25,7 +25,7 @@ export class ProductService {
static async createPrice(
axiosInstance: AxiosInstance,
productId: string,
data: any
data: any,
) {
await axiosInstance.post(`/products/${productId}/prices`, data);
}
@@ -39,7 +39,7 @@ export class ProductService {
static async copyProduct(
axiosInstance: AxiosInstance,
productId: string,
data: any
data: any,
) {
await axiosInstance.post(`/v1/products/${productId}/copy`, data);
}

View File

@@ -3,14 +3,16 @@ import { endpoint } from "@/utils/constants";
import { useAuth } from "@clerk/clerk-react";
import { AppEnv } from "@autumn/shared";
import { useEnv } from "@/utils/envUtils";
const defaultParams = {
isAuth: true,
};
export function useAxiosInstance(params?: { env?: AppEnv; isAuth?: boolean }) {
const finalParams: any = {
...defaultParams,
...(params || {}),
};
export function useAxiosInstance({
env,
isAuth = true,
}: {
env?: AppEnv;
isAuth?: boolean;
}) {
const trueEnv = useEnv();
const axiosInstance = axios.create({
@@ -19,7 +21,7 @@ export function useAxiosInstance({
const { getToken } = useAuth();
if (isAuth) {
if (finalParams.isAuth) {
axiosInstance.interceptors.request.use(
async (config: any) => {
const token = await getToken({
@@ -35,7 +37,7 @@ export function useAxiosInstance({
},
(error: any) => {
return Promise.reject(error);
}
},
);
}

View File

@@ -1,15 +1,11 @@
"use client";
import React, { useContext } from "react";
import { AppEnv, DBConnection, Feature, FeatureType } from "@autumn/shared";
import { AppEnv } from "@autumn/shared";
import CreateCreditSystem from "./CreateCreditSystem";
import { useFeaturesContext } from "../features/FeaturesContext";
import { useAxiosSWR } from "@/services/useAxiosSwr";
import LoadingScreen from "../general/LoadingScreen";
import { FeaturesContext } from "../features/FeaturesContext";
import { CreditSystemsTable } from "./CreditSystemsTable";
import { CustomToaster } from "@/components/general/CustomToaster";
function CreditSystemsView({ env }: { env: AppEnv }) {
const { data, isLoading, error, mutate } = useAxiosSWR({

View File

@@ -20,13 +20,8 @@ import { useEffect, useState } from "react";
import ErrorScreen from "@/views/general/ErrorScreen";
import { InvoicesTable } from "./InvoicesTable";
import { AdminHover } from "@/components/general/AdminHover";
import { CustomerSidebar } from "./customer-sidebar/customer-sidebar";
import { CustomerBreadcrumbs } from "./customer-breadcrumbs";
import { Building2, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { XMarkIcon } from "@heroicons/react/24/outline";
import { EntityHeader } from "./components/entity-header";
import { SelectEntity } from "./customer-sidebar/select-entity";
export default function CustomerView({ env }: { env: AppEnv }) {

View File

@@ -17,12 +17,7 @@ import {
import { ProductActionState } from "@/utils/models";
import { File, ShoppingCart, Upload } from "lucide-react";
export const AddProductButton = ({}: // setUseInvoice,
{
// setUseInvoice?: (useInvoice: boolean) => void;
// handleCreateProduct: (useInvoice?: boolean) => Promise<void>;
// actionState: any;
}) => {
export const AddProductButton = () => {
const [checkoutLoading, setCheckoutLoading] = useState(false);
const [invoiceLoading, setInvoiceLoading] = useState(false);
const [open, setOpen] = useState(false);

View File

@@ -14,7 +14,7 @@ export const CustomerEntities = () => {
const entity = entities.find((entity: Entity) => entity.id === entityId);
const feature = features.find(
(feature: Feature) => entity?.internal_feature_id === feature.internal_id
(feature: Feature) => entity?.internal_feature_id === feature.internal_id,
);
const featureName = getFeatureName({

View File

@@ -108,8 +108,8 @@ export default function CustomerProductView() {
}
}, [entityIdParam]);
let version = searchParams.get("version");
let customer_product_id = searchParams.get("id");
const version = searchParams.get("version");
const customer_product_id = searchParams.get("id");
const { data, isLoading, mutate, error } = useAxiosSWR({
url: `/customers/${customer_id}/product/${product_id}${getProductUrlParams({
version,
@@ -143,7 +143,7 @@ export default function CustomerProductView() {
useEffect(() => {
if (!data?.product || !data?.customer) return;
let product = data.product;
const product = data.product;
initialProductRef.current = structuredClone(product);
if (product.options) {
@@ -192,7 +192,7 @@ export default function CustomerProductView() {
if (isLoading) return <LoadingScreen />;
const oneTimePurchase = pricesOnlyOneOff(
product?.items || [],
product?.is_add_on || false
product?.is_add_on || false,
);
if (!customer_id || !product_id) {
@@ -220,7 +220,7 @@ export default function CustomerProductView() {
// Continue with product creation if no required options
await createProduct(
useInvoiceLatest !== undefined ? useInvoiceLatest : useInvoice
useInvoiceLatest !== undefined ? useInvoiceLatest : useInvoice,
);
} catch (error) {
toast.error(getBackendErr(error, "Error checking required options"));
@@ -229,7 +229,7 @@ export default function CustomerProductView() {
const createProduct = async (useInvoiceLatest?: boolean) => {
try {
let isCustom = hasChanges;
const isCustom = hasChanges;
const { data } = await CusService.addProduct(axiosInstance, customer_id, {
product_id,
@@ -255,7 +255,7 @@ export default function CustomerProductView() {
entity_id: entityId,
})}`,
navigation,
env
env,
);
toast.success(data.message || "Successfully attached product");
@@ -281,7 +281,7 @@ export default function CustomerProductView() {
navigateTo(
`/integrations/stripe?redirect=${redirectUrl}`,
navigation,
env
env,
);
} else {
toast.error(getBackendErr(error, "Error creating product"));
@@ -422,15 +422,15 @@ export default function CustomerProductView() {
entityId ? `?entity_id=${entityId}` : ""
}`,
navigation,
env
env,
)
}
>
{customer.name
? customer.name
: customer.id
? customer.id
: customer.email}
? customer.id
: customer.email}
</BreadcrumbLink>
<BreadcrumbSeparator />
<BreadcrumbItem>{product.name}</BreadcrumbItem>
@@ -438,14 +438,7 @@ export default function CustomerProductView() {
</Breadcrumb>
<div className="flex">
<div className="flex-1 w-full min-w-sm">
{product && (
<ManageProduct
customerData={data}
showFreeTrial={false}
setShowFreeTrial={() => {}}
version={version ? parseInt(version) : product.version}
/>
)}
{product && <ManageProduct />}
{options.length > 0 && (
<ProductOptions
options={options}

View File

@@ -1,33 +0,0 @@
import React from "react";
import CreateDBConnection from "./CreateDBConnection";
import { useFeaturesContext } from "../FeaturesContext";
import { DBConnection, Feature, FeatureType } from "@autumn/shared";
function MeteredView() {
const { features, dbConns } = useFeaturesContext();
const createMeteredFeature = () => {
console.log("Create Metered Feature");
};
return (
<div className="flex flex-col gap-2 w-fit text-sm">
<p>DB Connections</p>
{dbConns?.map((dbConn: DBConnection) => (
<p key={dbConn.id}>- {dbConn.display_name}</p>
))}
<p>Features</p>
{features
?.filter((feature: Feature) => feature.type === FeatureType.Metered)
.map((feature: Feature) => (
<p key={feature.id}>- {feature.name}</p>
))}
<CreateDBConnection />
</div>
);
}
export default MeteredView;

View File

@@ -2,7 +2,7 @@
import { useState, useEffect, useRef } from "react";
import { toast } from "sonner";
import { useOrganization } from "@clerk/clerk-react";
import { useOrganization, useOrganizationList } from "@clerk/clerk-react";
import { useSearchParams } from "react-router";
import Step from "@/components/general/OnboardingStep";
import { AppEnv } from "@autumn/shared";
@@ -18,11 +18,15 @@ import AttachProduct from "./onboarding-steps/04_AttachProduct";
import CheckAccessStep from "./onboarding-steps/05_CheckAccess";
import Install from "./onboarding-steps/Install";
import LoadingScreen from "../general/LoadingScreen";
import { ProductList } from "./onboarding-steps/03_ProductList";
import { useCreateOrg } from "./hooks/useCreateOrg";
function OnboardingView() {
const env = useEnv();
const { organization: org } = useOrganization();
const { setActive } = useOrganizationList();
const [searchParams] = useSearchParams();
const [orgCreated, setOrgCreated] = useState(org ? true : false);
@@ -31,30 +35,9 @@ function OnboardingView() {
const hasHandledOrg = useRef(false);
const hasHandledToken = useRef(false);
const axiosInstance = useAxiosInstance({ env });
const [loading, setLoading] = useState(
searchParams.get("token") ? true : false,
);
const handleToken = async () => {
if (searchParams.get("token")) {
console.log("Token: ", searchParams.get("token"));
axiosInstance.post("/onboarding", {
token: searchParams.get("token"),
});
// await new Promise((resolve) => setTimeout(resolve, 3000));
setLoading(false);
}
};
useEffect(() => {
if (searchParams.get("token") && !hasHandledToken.current) {
hasHandledToken.current = true;
handleToken();
}
}, [searchParams]);
const axiosInstance = useAxiosInstance();
const token = searchParams.get("token");
const [loading, setLoading] = useState(true);
const {
data: productData,
@@ -66,69 +49,55 @@ function OnboardingView() {
withAuth: true,
});
const pollForOrg = async () => {
for (let i = 0; i < 10; i++) {
console.log("polling for org, attempt", i);
const requiredProdLength = env == AppEnv.Sandbox ? 2 : 0;
useEffect(() => {
const handleToken = async () => {
try {
const response = await axiosInstance.get("/products/data");
const pollingData = response.data;
const { data } = await axiosInstance.post("/onboarding", {
token,
});
if (pollingData?.products.length != requiredProdLength) {
throw new Error("Products not created");
}
setOrgCreated(true);
await productMutate();
return;
} catch (error) {
console.log("error", error);
console.error(error);
} finally {
setLoading(false);
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
};
window.location.reload();
};
if (org && token && !hasHandledToken.current) {
hasHandledToken.current = true;
handleToken();
}
}, [org, searchParams, token, axiosInstance, productMutate]);
useEffect(() => {
const toastMessage = searchParams.get("toast");
if (toastMessage) {
toast.error(toastMessage);
if (org && !token) {
setLoading(false);
}
}, [searchParams]);
}, [org, token]);
useEffect(() => {
if (org && !orgCreated && !hasHandledOrg.current) {
hasHandledOrg.current = true;
pollForOrg();
}
}, [org, orgCreated]);
useCreateOrg({ productMutate });
if (loading) {
if (loading || productLoading) {
return <LoadingScreen />;
}
return (
<div className="text-sm w-full flex justify-start">
<div className="flex flex-col p-8 px-14">
<CreateOrgStep pollForOrg={pollForOrg} number={1} />
{orgCreated && (
{productData && (
<>
<CreateProductStep
productId={productId}
setProductId={setProductId}
number={2}
/>
<CreateSecretKey apiKey={apiKey} setApiKey={setApiKey} number={3} />
<ProductList data={productData} mutate={productMutate} />
<CreateSecretKey apiKey={apiKey} setApiKey={setApiKey} number={2} />
<ConnectStripeStep
mutate={productMutate}
productData={productData}
number={4}
number={3}
/>
<Install number={5} />
<Install number={4} />
<AttachProduct productId={productId} apiKey={apiKey} number={6} />
<AttachProduct productId={productId} apiKey={apiKey} number={5} />
<CheckAccessStep apiKey={apiKey} number={6} />

View File

@@ -0,0 +1,31 @@
import { useOrganization } from "@clerk/clerk-react";
import { useAxiosInstance } from "@/services/useAxiosInstance";
import { useOrganizationList } from "@clerk/clerk-react";
import { useEffect, useRef } from "react";
export const useCreateOrg = ({
productMutate,
}: {
productMutate: () => Promise<void>;
}) => {
const axiosInstance = useAxiosInstance();
const { organization: org } = useOrganization();
const { setActive } = useOrganizationList();
const hasCreatedOrg = useRef(false);
useEffect(() => {
const createDefaultOrg = async () => {
if (hasCreatedOrg.current) return;
hasCreatedOrg.current = true;
const { data } = await axiosInstance.post("/organization");
await setActive?.({ organization: data.id });
await productMutate();
};
if (!org) {
createDefaultOrg();
}
}, [org]);
};

View File

@@ -11,9 +11,7 @@ import { useEnv } from "@/utils/envUtils";
import { getBackendErr } from "@/utils/genUtils";
import { toast } from "sonner";
import { ProductContext } from "@/views/products/product/ProductContext";
import { ManageProduct } from "@/views/products/product/ManageProduct";
import { ProductItemTable } from "@/views/products/product/product-item/ProductItemTable";
import { ArrowUp, CopyIcon, PlusIcon } from "lucide-react";
import { Input } from "@/components/ui/input";
import { slugify } from "@/utils/formatUtils/formatTextUtils";
@@ -26,12 +24,12 @@ export const CreateProductStep = ({
setProductId: (productId: string) => void;
number: number;
}) => {
let [newProduct, setNewProduct] = useState<any>(defaultProduct);
let [createClicked, setCreateClicked] = useState(false);
let [createProductLoading, setCreateProductLoading] = useState(false);
const env = useEnv();
const axiosInstance = useAxiosInstance({ env });
let env = useEnv();
let axiosInstance = useAxiosInstance({ env });
const [newProduct, setNewProduct] = useState<any>(defaultProduct);
const [createClicked, setCreateClicked] = useState(false);
const [createProductLoading, setCreateProductLoading] = useState(false);
const { data, isLoading, mutate } = useAxiosSWR({
url: `/products/${newProduct.id}/data`,
@@ -60,7 +58,7 @@ export const CreateProductStep = ({
const res = await ProductService.updateProduct(
axiosInstance,
productId,
product
product,
);
toast.success("Product items successfully created");
await mutate();
@@ -91,12 +89,6 @@ export const CreateProductStep = ({
</p>
}
>
{/* <p>
Then, create your <span className="font-bold">Products</span>, which
are the pricing plans that grant access to those features.
</p>
<p>Some examples have been created for you.</p> */}
{product ? (
<FeaturesContext.Provider
value={{

View File

@@ -0,0 +1,185 @@
import { Button } from "@/components/ui/button";
import Step from "@/components/general/OnboardingStep";
import { FeaturesContext } from "@/views/features/FeaturesContext";
import { PageSectionHeader } from "@/components/general/PageSectionHeader";
import { useEnv } from "@/utils/envUtils";
import { ManageProduct } from "@/views/products/product/ManageProduct";
import { ProductContext } from "@/views/products/product/ProductContext";
import { ProductsContext } from "@/views/products/ProductsContext";
import { ProductsTable } from "@/views/products/ProductsTable";
import { Product, products, ProductV2 } from "@autumn/shared";
import {
DialogHeader,
DialogContent,
DialogTrigger,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { useState } from "react";
import { Dialog } from "@/components/ui/dialog";
import { AddProductButton } from "@/views/customers/customer/add-product/AddProductButton";
import { ProductService } from "@/services/products/ProductService";
import { useAxiosInstance } from "@/services/useAxiosInstance";
import { getBackendErr } from "@/utils/genUtils";
import { toast } from "sonner";
import CreateProduct from "@/views/products/CreateProduct";
import { useSearchParams } from "react-router";
export const ProductList = ({
data,
mutate,
}: {
data: any;
mutate: () => Promise<void>;
}) => {
const env = useEnv();
const [searchParams] = useSearchParams();
const token = searchParams.get("token");
const [product, setProduct] = useState<any>(data.products[0]);
const [features, setFeatures] = useState<any[]>(data.features);
const [open, setOpen] = useState(false);
if (!data.products) return null;
return (
<Step
title={token ? "Your products" : "Create your products"}
number={1}
description={
<p>
Products define the features your customers can access and how much
they cost. Create your first product to get started .
</p>
}
>
<EditProductDialog
product={product}
setProduct={setProduct}
features={features}
setFeatures={setFeatures}
mutate={mutate}
open={open}
setOpen={setOpen}
/>
<ProductsContext.Provider
value={{
products,
env,
onboarding: true,
mutate,
}}
>
<PageSectionHeader
title="Products"
isOnboarding={true}
addButton={
<>
{/* <Button variant="add">Test Data</Button> */}
<CreateProduct
onSuccess={async (newProduct: ProductV2) => {
await mutate();
setProduct(newProduct);
setOpen(true);
}}
/>
</>
}
className="pr-0"
/>
<ProductsTable
products={data.products}
onRowClick={(id) => {
setProduct(data.products.find((p: ProductV2) => p.id === id));
setOpen(true);
}}
/>
</ProductsContext.Provider>
</Step>
);
};
const EditProductDialog = ({
product,
features,
setProduct,
setFeatures,
mutate,
open,
setOpen,
}: {
product: any;
setProduct: (product: any) => void;
features: any[];
setFeatures: (features: any[]) => void;
mutate: () => Promise<void>;
open: boolean;
setOpen: (open: boolean) => void;
}) => {
const env = useEnv();
const axiosInstance = useAxiosInstance();
const [createProductLoading, setCreateProductLoading] = useState(false);
const updateProduct = async () => {
setCreateProductLoading(true);
setCreateProductLoading(true);
try {
const res = await ProductService.updateProduct(
axiosInstance,
product.id,
product,
);
toast.success("Product items successfully created");
await mutate();
setOpen(false);
} catch (error) {
toast.error(getBackendErr(error, "Failed to update product"));
}
setCreateProductLoading(false);
setCreateProductLoading(false);
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="p-0 py-8 min-w-[500px] min-h-[300px] flex flex-col justify-between">
<DialogTitle className="text-t2 font-semibold px-10 hidden">
{/* Edit Product */}
</DialogTitle>
<div>
<FeaturesContext.Provider
value={{
env,
mutate,
}}
>
<ProductContext.Provider
value={{
product,
setProduct,
mutate,
env,
features,
setFeatures,
}}
>
<ManageProduct hideAdminHover={true} />
</ProductContext.Provider>
</FeaturesContext.Provider>
</div>
<DialogFooter>
<div className="flex justify-end gap-2 px-10">
<Button
isLoading={createProductLoading}
variant="gradientPrimary"
onClick={updateProduct}
className="min-w-44 w-44 max-w-44"
>
Update Product
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
);
};

View File

@@ -11,48 +11,63 @@ import { ProductService } from "@/services/products/ProductService";
import { useNavigate } from "react-router";
import { useAxiosInstance } from "@/services/useAxiosInstance";
import { useState } from "react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { useProductsContext } from "./ProductsContext";
import { PlusIcon } from "lucide-react";
import { getBackendErr, navigateTo } from "@/utils/genUtils";
import { ProductConfig } from "./ProductConfig";
import { ProductV2 } from "@autumn/shared";
export let defaultProduct = {
export const defaultProduct = {
name: "",
id: "",
group: "",
is_add_on: false,
is_default: false,
};
function CreateProduct() {
const { env, mutate } = useProductsContext();
const axiosInstance = useAxiosInstance({ env });
const navigate = useNavigate();
function CreateProduct({
onSuccess,
}: {
onSuccess?: (newProduct: ProductV2) => Promise<void>;
}) {
const { env, mutate } = useProductsContext();
const [loading, setLoading] = useState(false);
const [product, setProduct] = useState(defaultProduct);
const [idChanged, setIdChanged] = useState(false);
const [open, setOpen] = useState(false);
const axiosInstance = useAxiosInstance({ env });
const navigate = useNavigate();
const handleCreateClicked = async () => {
setLoading(true);
try {
const productId = await ProductService.createProduct(
const newProduct = await ProductService.createProduct(
axiosInstance,
product
product,
);
await mutate();
navigateTo(`/products/${productId}`, navigate, env);
if (onSuccess) {
await onSuccess(newProduct);
} else {
navigateTo(`/products/${newProduct.id}`, navigate, env);
}
setOpen(false);
} catch (error) {
toast.error(getBackendErr(error, "Failed to create product"));
}
setLoading(false);
};
useEffect(() => {
if (open) {
setProduct(defaultProduct);
}
}, [open]);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>

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