added chatResult handling and updated onboarding flow
This commit is contained in:
1430
package-lock.json
generated
1430
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"];
|
||||
|
||||
@@ -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";
|
||||
@@ -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],
|
||||
}),
|
||||
}));
|
||||
@@ -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",
|
||||
}),
|
||||
],
|
||||
);
|
||||
@@ -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),
|
||||
],
|
||||
);
|
||||
51
server/src/external/autumn/autumnUtils.ts
vendored
51
server/src/external/autumn/autumnUtils.ts
vendored
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
64
server/src/external/webhooks/clerkWebhooks.ts
vendored
64
server/src/external/webhooks/clerkWebhooks.ts
vendored
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
100
server/src/internal/features/utils/constructFeatureUtils.ts
Normal file
100
server/src/internal/features/utils/constructFeatureUtils.ts
Normal 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;
|
||||
};
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
52
server/src/internal/orgs/handlers/handlePostOrg.ts
Normal file
52
server/src/internal/orgs/handlers/handlePostOrg.ts
Normal 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,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -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";
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
// }
|
||||
|
||||
@@ -97,7 +97,7 @@ productRouter.get("/counts", async (req: any, res) => {
|
||||
sb,
|
||||
internalProductId: product.internal_id,
|
||||
});
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
// let result: { [key: string]: any } = {};
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -44,7 +44,7 @@ export const mapToProductItems = ({
|
||||
let relatedEnt = getPriceEntitlement(
|
||||
price,
|
||||
entitlements,
|
||||
allowFeatureMatch
|
||||
allowFeatureMatch,
|
||||
);
|
||||
|
||||
if (!relatedEnt) {
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
// }
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
495
shared/db/allTables.ts
Normal 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",
|
||||
// }),
|
||||
// ],
|
||||
// );
|
||||
@@ -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",
|
||||
@@ -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",
|
||||
@@ -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(
|
||||
@@ -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),
|
||||
}));
|
||||
@@ -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
47
shared/db/schema.ts
Normal 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,
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export const InternalErrCode = {
|
||||
UpdateBalanceFailed: "update_balance_failed",
|
||||
};
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
@@ -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";
|
||||
};
|
||||
@@ -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");
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
22
shared/models/featureModels/featureConfig/creditConfig.ts
Normal file
22
shared/models/featureModels/featureConfig/creditConfig.ts
Normal 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>;
|
||||
@@ -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,
|
||||
15
shared/models/featureModels/featureEnums.ts
Normal file
15
shared/models/featureModels/featureEnums.ts
Normal 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",
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
10
shared/models/featureModels/featureRelations.ts
Normal file
10
shared/models/featureModels/featureRelations.ts
Normal 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],
|
||||
}),
|
||||
}));
|
||||
@@ -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");
|
||||
9
shared/models/orgModels/orgRelations.ts
Normal file
9
shared/models/orgModels/orgRelations.ts
Normal 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),
|
||||
}));
|
||||
52
shared/models/orgModels/orgTable.ts
Normal file
52
shared/models/orgModels/orgTable.ts
Normal 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;
|
||||
};
|
||||
11
shared/models/productModels/entModels/entRelations.ts
Normal file
11
shared/models/productModels/entModels/entRelations.ts
Normal 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],
|
||||
}),
|
||||
}));
|
||||
@@ -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);
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
@@ -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} />
|
||||
|
||||
|
||||
31
vite/src/views/onboarding/hooks/useCreateOrg.tsx
Normal file
31
vite/src/views/onboarding/hooks/useCreateOrg.tsx
Normal 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]);
|
||||
};
|
||||
@@ -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={{
|
||||
|
||||
185
vite/src/views/onboarding/onboarding-steps/03_ProductList.tsx
Normal file
185
vite/src/views/onboarding/onboarding-steps/03_ProductList.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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
Reference in New Issue
Block a user