From af4bd753160e9e814b42c951152fe4c99882ea8e Mon Sep 17 00:00:00 2001 From: John Yeo Date: Sun, 25 May 2025 14:58:25 +0100 Subject: [PATCH] rewrote product queries in drizzle --- server/drizzle/relations.ts | 310 ++--- server/drizzle/schema.ts | 1023 +++++++---------- server/src/external/stripe/stripeWebhooks.ts | 2 + .../webhookHandlers/handleSubDeleted.ts | 8 +- .../webhookHandlers/handleSubUpdated.ts | 35 +- .../api/components/componentRouter.ts | 4 +- server/src/internal/api/customers/cusUtils.ts | 49 +- .../handlers/handleCreateCustomer.ts | 13 +- .../handlers/handleCusProductExpired.ts | 13 +- .../handlers/handleUpdateBalances.ts | 34 +- .../handlers/handleUpdateEntitlement.ts | 13 +- .../api/customers/products/attachRouter.ts | 3 +- .../api/customers/products/expireRouter.ts | 7 +- .../api/entities/handleCreateEntity.ts | 107 +- .../api/entities/handleDeleteEntity.ts | 15 +- .../src/internal/api/entitled/checkUtils.ts | 9 +- .../internal/api/entitled/entitledRouter.ts | 6 +- .../internal/api/entitled/getCheckPreview.ts | 5 +- .../entitled/handlers/handleProductCheck.ts | 6 +- .../features/handlers/handleUpdateFeature.ts | 2 +- .../api/migrations/migrationRouter.ts | 14 +- .../api/products/handleDeleteProduct.ts | 16 +- .../internal/api/products/handleGetProduct.ts | 40 +- .../api/products/handleUpdateProduct.ts | 90 +- .../api/products/handleVersionProduct.ts | 2 +- .../products/handlers/handleCopyProduct.ts | 12 +- .../products/handlers/handleCreateProduct.ts | 10 +- .../products/handlers/handleListProducts.ts | 15 +- .../internal/api/products/productRouter.ts | 14 +- .../change-product/handleChangeProduct.ts | 13 +- .../customers/cusUtils/getOrCreateCustomer.ts | 1 + .../customers/entitlements/cusEntUtils.ts | 6 +- .../customers/entitlements/groupByUtils.ts | 341 +----- .../internal/customers/internalCusRouter.ts | 32 +- .../customers/previews/getUpgradePreview.ts | 2 +- .../customers/prices/CusPriceService.ts | 78 +- .../customers/products/attachUtils.ts | 23 +- .../customers/products/cusProductUtils.ts | 13 +- server/src/internal/features/featureUtils.ts | 8 +- server/src/internal/metadata/metadataUtils.ts | 16 +- .../internal/migrations/runMigrationTask.ts | 12 +- .../orgs/onboarding/onboardingRouter.ts | 6 +- .../src/internal/products/ProductService.ts | 520 ++++----- .../products/internalProductRouter.ts | 61 +- .../internal/products/prices/priceUtils.ts | 10 +- server/src/internal/products/productUtils.ts | 25 +- server/src/internal/rewards/referralUtils.ts | 15 +- .../internal/rewards/triggerCheckoutReward.ts | 10 +- server/src/middleware/analyticsMiddleware.ts | 1 + server/src/middleware/apiMiddleware.ts | 3 +- server/src/middleware/authMiddleware.ts | 4 +- server/src/middleware/publicAuthMiddleware.ts | 5 +- server/src/queue/queue.ts | 9 +- server/src/trigger/adjustAllowance.ts | 7 +- server/src/trigger/updateBalanceTask.ts | 58 +- server/src/trigger/updateUsageTask.ts | 27 +- .../arrear_prorated/arrear_prorated2.ts | 5 +- .../arrear_prorated/arrear_prorated3.ts | 13 +- server/tests/advanced/usage/usage3.ts | 1 + server/tests/attach/01_multi_product3.ts | 4 +- server/tests/utils/scheduleCheckUtils.ts | 29 +- shared/db/cusTable.ts | 23 - shared/db/schema.ts | 35 +- shared/index.ts | 10 +- .../cusModels/cusEntModels/customEntModels.ts | 18 - .../cusPriceModels/pricesInputModel.ts | 19 - shared/models/cusModels/cusRelations.ts | 14 + shared/models/cusModels/cusResponseModels.ts | 2 +- shared/models/cusModels/cusTable.ts | 43 + .../cusModels/entityModels/entityRelations.ts | 20 + .../cusModels/entityModels/entityTable.ts | 53 + shared/models/cusModels/fullCusModel.ts | 2 +- .../cusEntModels/cusEntModels.ts} | 26 +- .../cusEntModels/cusEntRelations.ts | 29 + .../cusEntModels/cusEntTable.ts | 55 + .../cusPriceModels/cusPriceModels.ts | 0 .../cusPriceModels/cusPriceRelations.ts | 24 + .../cusPriceModels/cusPriceTable.ts | 35 + .../cusProductModels.ts | 12 +- .../cusProductModels/cusProductRelations.ts | 25 + .../cusProductModels/cusProductTable.ts | 77 ++ shared/models/genModels/genEnums.ts | 6 + .../freeTrialModels/freeTrialTable.ts | 2 +- .../productModels/priceModels/priceModels.ts | 2 +- shared/models/productModels/productModels.ts | 3 +- .../models/productModels/productRelations.ts | 5 +- 86 files changed, 1662 insertions(+), 2113 deletions(-) delete mode 100644 shared/db/cusTable.ts delete mode 100644 shared/models/cusModels/cusEntModels/customEntModels.ts delete mode 100644 shared/models/cusModels/cusPriceModels/pricesInputModel.ts create mode 100644 shared/models/cusModels/cusRelations.ts create mode 100644 shared/models/cusModels/cusTable.ts create mode 100644 shared/models/cusModels/entityModels/entityRelations.ts create mode 100644 shared/models/cusModels/entityModels/entityTable.ts rename shared/models/{cusModels/cusEntModels/cusEntitlementModels.ts => cusProductModels/cusEntModels/cusEntModels.ts} (60%) create mode 100644 shared/models/cusProductModels/cusEntModels/cusEntRelations.ts create mode 100644 shared/models/cusProductModels/cusEntModels/cusEntTable.ts rename shared/models/{cusModels => cusProductModels}/cusPriceModels/cusPriceModels.ts (100%) create mode 100644 shared/models/cusProductModels/cusPriceModels/cusPriceRelations.ts create mode 100644 shared/models/cusProductModels/cusPriceModels/cusPriceTable.ts rename shared/models/{cusModels => cusProductModels}/cusProductModels.ts (93%) create mode 100644 shared/models/cusProductModels/cusProductRelations.ts create mode 100644 shared/models/cusProductModels/cusProductTable.ts diff --git a/server/drizzle/relations.ts b/server/drizzle/relations.ts index 124c45c77..1348cb1d6 100644 --- a/server/drizzle/relations.ts +++ b/server/drizzle/relations.ts @@ -1,5 +1,32 @@ import { relations } from "drizzle-orm/relations"; -import { rewards, rewardPrograms, organizations, customers, referralCodes, rewardRedemptions, customerPrices, invoiceItems, apiKeys, freeTrials, customerProducts, entities, products, prices, features, entitlements, events, migrationJobs, subscriptions, migrationErrors } from "./schema"; +import { organizations, apiKeys, rewards, rewardPrograms, customerProducts, customerPrices, customers, prices, entities, features, entitlements, products, events, freeTrials, invoiceItems, rewardRedemptions, referralCodes, migrationJobs, subscriptions, migrationErrors } from "./schema"; + +export const apiKeysRelations = relations(apiKeys, ({one}) => ({ + organization: one(organizations, { + fields: [apiKeys.orgId], + references: [organizations.id] + }), +})); + +export const organizationsRelations = relations(organizations, ({many}) => ({ + apiKeys: many(apiKeys), + rewards: many(rewards), + rewardPrograms: many(rewardPrograms), + entities: many(entities), + features: many(features), + products: many(products), + migrationJobs: many(migrationJobs), + subscriptions: many(subscriptions), + referralCodes: many(referralCodes), +})); + +export const rewardsRelations = relations(rewards, ({one, many}) => ({ + organization: one(organizations, { + fields: [rewards.orgId], + references: [organizations.id] + }), + rewardPrograms: many(rewardPrograms), +})); export const rewardProgramsRelations = relations(rewardPrograms, ({one, many}) => ({ reward: one(rewards, { @@ -10,56 +37,144 @@ export const rewardProgramsRelations = relations(rewardPrograms, ({one, many}) = fields: [rewardPrograms.orgId], references: [organizations.id] }), - referralCodes: many(referralCodes), rewardRedemptions: many(rewardRedemptions), -})); - -export const rewardsRelations = relations(rewards, ({one, many}) => ({ - rewardPrograms: many(rewardPrograms), - organization: one(organizations, { - fields: [rewards.orgId], - references: [organizations.id] - }), -})); - -export const organizationsRelations = relations(organizations, ({many}) => ({ - rewardPrograms: many(rewardPrograms), referralCodes: many(referralCodes), - rewards: many(rewards), - apiKeys: many(apiKeys), - features: many(features), - entities: many(entities), - products: many(products), - migrationJobs: many(migrationJobs), - subscriptions: many(subscriptions), })); -export const referralCodesRelations = relations(referralCodes, ({one, many}) => ({ +export const customerPricesRelations = relations(customerPrices, ({one, many}) => ({ + customerProduct: one(customerProducts, { + fields: [customerPrices.customerProductId], + references: [customerProducts.id] + }), customer: one(customers, { - fields: [referralCodes.internalCustomerId], + fields: [customerPrices.internalCustomerId], references: [customers.internalId] }), - rewardProgram: one(rewardPrograms, { - fields: [referralCodes.internalRewardProgramId], - references: [rewardPrograms.internalId] + price: one(prices, { + fields: [customerPrices.priceId], + references: [prices.id] }), - organization: one(organizations, { - fields: [referralCodes.orgId], - references: [organizations.id] + invoiceItems: many(invoiceItems), +})); + +export const customerProductsRelations = relations(customerProducts, ({one, many}) => ({ + customerPrices: many(customerPrices), + freeTrial: one(freeTrials, { + fields: [customerProducts.freeTrialId], + references: [freeTrials.id] + }), + customer: one(customers, { + fields: [customerProducts.internalCustomerId], + references: [customers.internalId] + }), + entity: one(entities, { + fields: [customerProducts.internalEntityId], + references: [entities.internalId] + }), + product: one(products, { + fields: [customerProducts.internalProductId], + references: [products.internalId] }), - rewardRedemptions: many(rewardRedemptions), })); export const customersRelations = relations(customers, ({many}) => ({ - referralCodes: many(referralCodes), - rewardRedemptions: many(rewardRedemptions), - customerProducts: many(customerProducts), customerPrices: many(customerPrices), entities: many(entities), events: many(events), + customerProducts: many(customerProducts), + rewardRedemptions: many(rewardRedemptions), + referralCodes: many(referralCodes), migrationErrors: many(migrationErrors), })); +export const pricesRelations = relations(prices, ({one, many}) => ({ + customerPrices: many(customerPrices), + entitlement: one(entitlements, { + fields: [prices.entitlementId], + references: [entitlements.id] + }), + product: one(products, { + fields: [prices.internalProductId], + references: [products.internalId] + }), +})); + +export const entitiesRelations = relations(entities, ({one, many}) => ({ + customer: one(customers, { + fields: [entities.internalCustomerId], + references: [customers.internalId] + }), + feature: one(features, { + fields: [entities.internalFeatureId], + references: [features.internalId] + }), + organization: one(organizations, { + fields: [entities.orgId], + references: [organizations.id] + }), + customerProducts: many(customerProducts), +})); + +export const featuresRelations = relations(features, ({one, many}) => ({ + entities: many(entities), + entitlements: many(entitlements), + organization: one(organizations, { + fields: [features.orgId], + references: [organizations.id] + }), +})); + +export const entitlementsRelations = relations(entitlements, ({one, many}) => ({ + feature: one(features, { + fields: [entitlements.internalFeatureId], + references: [features.internalId] + }), + product: one(products, { + fields: [entitlements.internalProductId], + references: [products.internalId] + }), + prices: many(prices), +})); + +export const productsRelations = relations(products, ({one, many}) => ({ + entitlements: many(entitlements), + freeTrials: many(freeTrials), + customerProducts: many(customerProducts), + prices: many(prices), + organization: one(organizations, { + fields: [products.orgId], + references: [organizations.id] + }), + migrationJobs_fromInternalProductId: many(migrationJobs, { + relationName: "migrationJobs_fromInternalProductId_products_internalId" + }), + migrationJobs_toInternalProductId: many(migrationJobs, { + relationName: "migrationJobs_toInternalProductId_products_internalId" + }), +})); + +export const eventsRelations = relations(events, ({one}) => ({ + customer: one(customers, { + fields: [events.internalCustomerId], + references: [customers.internalId] + }), +})); + +export const freeTrialsRelations = relations(freeTrials, ({one, many}) => ({ + product: one(products, { + fields: [freeTrials.internalProductId], + references: [products.internalId] + }), + customerProducts: many(customerProducts), +})); + +export const invoiceItemsRelations = relations(invoiceItems, ({one}) => ({ + customerPrice: one(customerPrices, { + fields: [invoiceItems.customerPriceId], + references: [customerPrices.id] + }), +})); + export const rewardRedemptionsRelations = relations(rewardRedemptions, ({one}) => ({ customer: one(customers, { fields: [rewardRedemptions.internalCustomerId], @@ -75,137 +190,22 @@ export const rewardRedemptionsRelations = relations(rewardRedemptions, ({one}) = }), })); -export const invoiceItemsRelations = relations(invoiceItems, ({one}) => ({ - customerPrice: one(customerPrices, { - fields: [invoiceItems.customerPriceId], - references: [customerPrices.id] - }), -})); - -export const customerPricesRelations = relations(customerPrices, ({one, many}) => ({ - invoiceItems: many(invoiceItems), - customerProduct: one(customerProducts, { - fields: [customerPrices.customerProductId], - references: [customerProducts.id] - }), +export const referralCodesRelations = relations(referralCodes, ({one, many}) => ({ + rewardRedemptions: many(rewardRedemptions), customer: one(customers, { - fields: [customerPrices.internalCustomerId], + fields: [referralCodes.internalCustomerId], references: [customers.internalId] }), - price: one(prices, { - fields: [customerPrices.priceId], - references: [prices.id] - }), -})); - -export const apiKeysRelations = relations(apiKeys, ({one}) => ({ - organization: one(organizations, { - fields: [apiKeys.orgId], - references: [organizations.id] - }), -})); - -export const customerProductsRelations = relations(customerProducts, ({one, many}) => ({ - freeTrial: one(freeTrials, { - fields: [customerProducts.freeTrialId], - references: [freeTrials.id] - }), - customer: one(customers, { - fields: [customerProducts.internalCustomerId], - references: [customers.internalId] - }), - entity: one(entities, { - fields: [customerProducts.internalEntityId], - references: [entities.internalId] - }), - product: one(products, { - fields: [customerProducts.internalProductId], - references: [products.internalId] - }), - customerPrices: many(customerPrices), -})); - -export const freeTrialsRelations = relations(freeTrials, ({one, many}) => ({ - customerProducts: many(customerProducts), - product: one(products, { - fields: [freeTrials.internalProductId], - references: [products.internalId] - }), -})); - -export const entitiesRelations = relations(entities, ({one, many}) => ({ - customerProducts: many(customerProducts), - customer: one(customers, { - fields: [entities.internalCustomerId], - references: [customers.internalId] - }), - feature: one(features, { - fields: [entities.internalFeatureId], - references: [features.internalId] + rewardProgram: one(rewardPrograms, { + fields: [referralCodes.internalRewardProgramId], + references: [rewardPrograms.internalId] }), organization: one(organizations, { - fields: [entities.orgId], + fields: [referralCodes.orgId], references: [organizations.id] }), })); -export const productsRelations = relations(products, ({one, many}) => ({ - customerProducts: many(customerProducts), - entitlements: many(entitlements), - prices: many(prices), - organization: one(organizations, { - fields: [products.orgId], - references: [organizations.id] - }), - migrationJobs_fromInternalProductId: many(migrationJobs, { - relationName: "migrationJobs_fromInternalProductId_products_internalId" - }), - migrationJobs_toInternalProductId: many(migrationJobs, { - relationName: "migrationJobs_toInternalProductId_products_internalId" - }), - freeTrials: many(freeTrials), -})); - -export const pricesRelations = relations(prices, ({one, many}) => ({ - customerPrices: many(customerPrices), - entitlement: one(entitlements, { - fields: [prices.entitlementId], - references: [entitlements.id] - }), - product: one(products, { - fields: [prices.internalProductId], - references: [products.internalId] - }), -})); - -export const featuresRelations = relations(features, ({one, many}) => ({ - organization: one(organizations, { - fields: [features.orgId], - references: [organizations.id] - }), - entities: many(entities), - entitlements: many(entitlements), -})); - -export const entitlementsRelations = relations(entitlements, ({one, many}) => ({ - feature: one(features, { - fields: [entitlements.internalFeatureId], - references: [features.internalId] - }), - product: one(products, { - fields: [entitlements.internalProductId], - references: [products.internalId] - }), - prices: many(prices), -})); - -export const eventsRelations = relations(events, ({one}) => ({ - customer: one(customers, { - fields: [events.internalCustomerId], - references: [customers.internalId] - }), -})); - export const migrationJobsRelations = relations(migrationJobs, ({one, many}) => ({ product_fromInternalProductId: one(products, { fields: [migrationJobs.fromInternalProductId], diff --git a/server/drizzle/schema.ts b/server/drizzle/schema.ts index 0900fb72b..41107c2f8 100644 --- a/server/drizzle/schema.ts +++ b/server/drizzle/schema.ts @@ -1,608 +1,459 @@ -import { - pgTable, - foreignKey, - text, - numeric, - boolean, - unique, - jsonb, - index, - primaryKey, -} from "drizzle-orm/pg-core"; -import { sql } from "drizzle-orm"; +import { pgTable, foreignKey, unique, text, numeric, jsonb, boolean, primaryKey } from "drizzle-orm/pg-core" +import { sql } from "drizzle-orm" -export const rewardPrograms = pgTable( - "reward_programs", - { - internalId: text("internal_id").primaryKey().notNull(), - id: text().notNull(), - createdAt: numeric("created_at"), - orgId: text("org_id"), - env: text(), - internalRewardId: text("internal_reward_id"), - maxRedemptions: numeric("max_redemptions"), - unlimitedRedemptions: boolean("unlimited_redemptions").default(false), - when: text(), - productIds: text("product_ids").array().default([""]), - excludeTrial: boolean("exclude_trial").default(false), - receivedBy: text("received_by").default("referrer"), - }, - (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 referralCodes = pgTable( - "referral_codes", - { - code: text().notNull(), - orgId: text("org_id").notNull(), - env: text(), - id: text().primaryKey().notNull(), - internalRewardProgramId: text("internal_reward_program_id"), - createdAt: numeric("created_at"), - internalCustomerId: text("internal_customer_id"), - }, - (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"), - unique("unique_code_constraint").on(table.code, table.orgId, table.env), - ], -); -export const rewardRedemptions = pgTable( - "reward_redemptions", - { - id: text().primaryKey().notNull(), - createdAt: numeric("created_at").notNull(), - updatedAt: numeric("updated_at"), - internalCustomerId: text("internal_customer_id"), - internalRewardProgramId: text("internal_reward_program_id"), - triggered: boolean().default(false), - 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 apiKeys = pgTable("api_keys", { + id: text().primaryKey().notNull(), + createdAt: numeric("created_at").notNull(), + name: text(), + prefix: text(), + orgId: text("org_id"), + userId: text("user_id"), + env: text(), + hashedKey: text("hashed_key").default(gen_random_uuid()), + meta: jsonb(), +}, (table) => [ + foreignKey({ + columns: [table.orgId], + foreignColumns: [organizations.id], + name: "api_keys_org_id_fkey" + }).onDelete("cascade"), + unique("api_keys_hashed_key_key").on(table.hashedKey), +]); -export const invoiceItems = pgTable( - "invoice_items", - { - id: text().primaryKey().notNull(), - customerId: text("customer_id").notNull(), - createdAt: numeric("created_at"), - 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(), - addedToStripe: boolean("added_to_stripe"), - }, - (table) => [ - foreignKey({ - columns: [table.customerPriceId], - foreignColumns: [customerPrices.id], - name: "invoice_items_customer_price_id_fkey", - }).onDelete("cascade"), - ], -); +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 organizations = pgTable( - "organizations", - { - id: text().primaryKey().notNull(), - slug: text().notNull(), - defaultCurrency: text("default_currency"), - stripeConnected: boolean("stripe_connected"), - stripeConfig: jsonb("stripe_config"), - testPkey: text("test_pkey"), - livePkey: text("live_pkey"), - svixConfig: jsonb("svix_config"), - createdAt: numeric("created_at"), - config: jsonb().default({}), - }, - (table) => [ - unique("organizations_test_pkey_key").on(table.testPkey), - unique("organizations_live_pkey_key").on(table.livePkey), - ], -); +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 rewards = pgTable( - "rewards", - { - internalId: text("internal_id").primaryKey().notNull(), - promoCodes: jsonb("promo_codes").array(), - env: text(), - name: text(), - orgId: text("org_id"), - createdAt: numeric("created_at"), - id: text(), - discountConfig: jsonb("discount_config"), - freeProductId: text("free_product_id"), - type: text(), - }, - (table) => [ - foreignKey({ - columns: [table.orgId], - foreignColumns: [organizations.id], - name: "coupons_org_id_fkey", - }).onDelete("cascade"), - ], -); -export const customers = pgTable( - "customers", - { - name: text(), - orgId: text("org_id").notNull(), - createdAt: numeric("created_at").notNull(), - internalId: text("internal_id").primaryKey().notNull(), - id: text(), - env: text(), - processor: jsonb(), - email: text(), - fingerprint: text(), - metadata: jsonb().default({}), - }, - (table) => [ - index("idx_customers_composite").using( - "btree", - table.orgId.asc().nullsLast().op("text_ops"), - table.env.asc().nullsLast().op("text_ops"), - table.id.asc().nullsLast().op("text_ops"), - ), - index("idx_customers_org_id_env_created_at").using( - "btree", - table.orgId.asc().nullsLast().op("text_ops"), - table.env.asc().nullsLast().op("text_ops"), - table.createdAt.desc().nullsFirst().op("text_ops"), - ), - unique("cus_id_constraint").on(table.orgId, table.id, table.env), - ], -); +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.internalId], + 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 apiKeys = pgTable( - "api_keys", - { - id: text().primaryKey().notNull(), - createdAt: numeric("created_at").notNull(), - name: text(), - prefix: text(), - orgId: text("org_id"), - userId: text("user_id"), - env: text(), - meta: jsonb(), - hashedKey: text("hashed_key"), - }, - (table) => [ - index("idx_hashed_key").using( - "hash", - table.hashedKey.asc().nullsLast().op("text_ops"), - ), - foreignKey({ - columns: [table.orgId], - foreignColumns: [organizations.id], - name: "api_keys_org_id_fkey", - }).onDelete("cascade"), - unique("api_keys_hashed_key_key").on(table.hashedKey), - ], -); +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.internalId], + 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 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), - 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("cascade"), - foreignKey({ - columns: [table.internalProductId], - foreignColumns: [products.internalId], - name: "customer_products_internal_product_id_fkey", - }), - ], -); +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 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 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 features = pgTable( - "features", - { - internalId: text("internal_id").primaryKey().notNull(), - orgId: text("org_id"), - id: text().notNull(), - name: text(), - type: text(), - createdAt: numeric("created_at"), - config: jsonb(), - env: text().default("live"), - display: jsonb(), - }, - (table) => [ - index("idx_features_composite").using( - "btree", - table.orgId.asc().nullsLast().op("text_ops"), - table.env.asc().nullsLast().op("text_ops"), - ), - foreignKey({ - columns: [table.orgId], - foreignColumns: [organizations.id], - name: "features_org_id_fkey", - }).onDelete("cascade"), - unique("feature_id_constraint").on(table.orgId, table.id, table.env), - ], -); +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 features = pgTable("features", { + internalId: text("internal_id").primaryKey().notNull(), + orgId: text("org_id"), + id: text().notNull(), + name: text(), + type: text(), + createdAt: numeric("created_at").default('sql`(date_part('epoch'::text, now()) * (1000)::double precision)`'), + config: jsonb(), + env: text().default('live'), + display: jsonb(), +}, (table) => [ + foreignKey({ + columns: [table.orgId], + foreignColumns: [organizations.id], + name: "features_org_id_fkey" + }).onDelete("cascade"), + unique("feature_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(), + id: text().primaryKey().notNull(), + createdAt: numeric("created_at").notNull(), + expiresAt: numeric("expires_at"), + data: jsonb(), }); -export const entities = pgTable( - "entities", - { - internalId: text("internal_id").primaryKey().notNull(), - orgId: text("org_id"), - createdAt: numeric("created_at").notNull(), - internalCustomerId: text("internal_customer_id"), - internalFeatureId: text("internal_feature_id"), - featureId: text("feature_id"), - env: text(), - id: text(), - name: text(), - deleted: boolean(), - }, - (table) => [ - foreignKey({ - columns: [table.internalCustomerId], - foreignColumns: [customers.internalId], - name: "entities_internal_customer_id_fkey", - }).onDelete("cascade"), - foreignKey({ - columns: [table.internalFeatureId], - foreignColumns: [features.internalId], - 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.internalCustomerId, - table.env, - table.id, - ), - ], -); +export const organizations = pgTable("organizations", { + id: text().primaryKey().notNull(), + slug: text().notNull(), + defaultCurrency: text("default_currency"), + stripeConnected: boolean("stripe_connected"), + stripeConfig: jsonb("stripe_config"), + testPkey: text("test_pkey"), + livePkey: text("live_pkey"), + svixConfig: jsonb("svix_config"), + createdAt: numeric("created_at"), + config: jsonb().default({}), +}, (table) => [ + unique("organizations_test_pkey_key").on(table.testPkey), + unique("organizations_live_pkey_key").on(table.livePkey), +]); -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.internalId], - 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 prices = pgTable("prices", { + createdAt: numeric("created_at").notNull(), + config: jsonb(), + orgId: text("org_id"), + internalProductId: text("internal_product_id"), + id: text().primaryKey().notNull(), + name: text(), + billingType: text("billing_type"), + isCustom: boolean("is_custom").default(false), + entitlementId: text("entitlement_id"), +}, (table) => [ + foreignKey({ + columns: [table.entitlementId], + foreignColumns: [entitlements.id], + name: "prices_entitlement_id_fkey" + }), + foreignKey({ + columns: [table.internalProductId], + foreignColumns: [products.internalId], + name: "prices_internal_product_id_fkey" + }).onUpdate("cascade").onDelete("cascade"), + unique("prices_id_key").on(table.id), +]); -export const prices = pgTable( - "prices", - { - createdAt: numeric("created_at").notNull(), - config: jsonb(), - orgId: text("org_id"), - internalProductId: text("internal_product_id"), - id: text().primaryKey().notNull(), - name: text(), - billingType: text("billing_type"), - isCustom: boolean("is_custom").default(false), - entitlementId: text("entitlement_id"), - }, - (table) => [ - foreignKey({ - columns: [table.entitlementId], - foreignColumns: [entitlements.id], - name: "prices_entitlement_id_fkey", - }).onDelete("cascade"), - foreignKey({ - columns: [table.internalProductId], - foreignColumns: [products.internalId], - name: "prices_internal_product_id_fkey", - }) - .onUpdate("cascade") - .onDelete("cascade"), - unique("prices_id_key").on(table.id), - ], -); +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"), + unique("unique_product").on(table.orgId, table.env, table.id, table.version), +]); -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 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 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 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 freeTrials = pgTable( - "free_trials", - { - id: text().primaryKey().notNull(), - createdAt: numeric("created_at").notNull(), - internalProductId: text("internal_product_id"), - duration: text().default("day"), - length: numeric(), - 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 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 subscriptions = pgTable( - "subscriptions", - { - id: text().primaryKey().notNull(), - stripeId: text("stripe_id"), - stripeScheduleId: text("stripe_schedule_id"), - createdAt: numeric("created_at"), - usageFeatures: text("usage_features").array(), - metadata: jsonb().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"), - ], -); - -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"), - message: text(), - code: text(), - data: jsonb(), - }, - (table) => [ - foreignKey({ - columns: [table.internalCustomerId], - foreignColumns: [customers.internalId], - name: "migration_errors_internal_customer_Id_fkey", - }).onDelete("cascade"), - foreignKey({ - columns: [table.migrationJobId], - foreignColumns: [migrationJobs.id], - name: "migration_errors_migration_job_id_fkey", - }).onDelete("cascade"), - primaryKey({ - columns: [table.internalCustomerId, table.migrationJobId], - name: "migration_errors_pkey", - }), - ], -); +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"}), +]); diff --git a/server/src/external/stripe/stripeWebhooks.ts b/server/src/external/stripe/stripeWebhooks.ts index d37bdeda2..216bf9955 100644 --- a/server/src/external/stripe/stripeWebhooks.ts +++ b/server/src/external/stripe/stripeWebhooks.ts @@ -86,6 +86,7 @@ stripeWebhookRouter.post( case "customer.subscription.updated": const subscription = event.data.object; await handleSubscriptionUpdated({ + db, sb: request.sb, org, subscription, @@ -98,6 +99,7 @@ stripeWebhookRouter.post( case "customer.subscription.deleted": const deletedSubscription = event.data.object; await handleSubscriptionDeleted({ + db, sb: request.sb, subscription: deletedSubscription, org, diff --git a/server/src/external/stripe/webhookHandlers/handleSubDeleted.ts b/server/src/external/stripe/webhookHandlers/handleSubDeleted.ts index 2946cf687..e1203bc41 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubDeleted.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubDeleted.ts @@ -24,8 +24,10 @@ import { subIsPrematurelyCanceled } from "../stripeSubUtils.js"; import { getBillingType } from "@/internal/products/prices/priceUtils.js"; import { billForRemainingUsages } from "@/internal/customers/change-product/billRemainingUsages.js"; import { addProductsUpdatedWebhookTask } from "@/external/svix/handleProductsUpdatedWebhook.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; const handleCusProductDeleted = async ({ + db, cusProduct, subscription, logger, @@ -34,6 +36,7 @@ const handleCusProductDeleted = async ({ sb, prematurelyCanceled, }: { + db: DrizzleCli; cusProduct: FullCusProduct; subscription: Stripe.Subscription; logger: any; @@ -170,8 +173,8 @@ const handleCusProductDeleted = async ({ }); await activateDefaultProduct({ + db, productGroup: cusProduct.product.group, - orgId: org.id, customer: cusProduct.customer, org, sb, @@ -191,12 +194,14 @@ const handleCusProductDeleted = async ({ }; export const handleSubscriptionDeleted = async ({ + db, sb, subscription, org, env, logger, }: { + db: DrizzleCli; sb: SupabaseClient; subscription: Stripe.Subscription; org: Organization; @@ -236,6 +241,7 @@ export const handleSubscriptionDeleted = async ({ for (const cusProduct of activeCusProducts) { batchUpdate.push( handleCusProductDeleted({ + db, cusProduct, subscription, logger, diff --git a/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts b/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts index 77ec18949..5ea0e6ac7 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts @@ -25,8 +25,10 @@ import { import RecaseError from "@/utils/errorUtils.js"; import { SubService } from "@/internal/subscriptions/SubService.js"; import { addProductsUpdatedWebhookTask } from "@/external/svix/handleProductsUpdatedWebhook.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; export const handleSubscriptionUpdated = async ({ + db, sb, org, subscription, @@ -34,6 +36,7 @@ export const handleSubscriptionUpdated = async ({ env, logger, }: { + db: DrizzleCli; sb: any; org: Organization; env: AppEnv; @@ -71,7 +74,7 @@ export const handleSubscriptionUpdated = async ({ if (cusProducts.length === 0) { console.log( - `subscription.updated: no customer products found with stripe sub id: ${subscription.id}` + `subscription.updated: no customer products found with stripe sub id: ${subscription.id}`, ); return; } @@ -86,7 +89,7 @@ export const handleSubscriptionUpdated = async ({ if (!lockAcquired) { attempts++; console.log( - `sub.updated: failed to acquire lock for ${subscription.id}, attempt ${attempts}` + `sub.updated: failed to acquire lock for ${subscription.id}, attempt ${attempts}`, ); if (attempts < 3) { await new Promise((resolve) => setTimeout(resolve, 1000)); @@ -128,7 +131,7 @@ export const handleSubscriptionUpdated = async ({ ids: updatedCusProducts.map((cp) => cp.id), status: updatedCusProducts[0].status, canceled_at: updatedCusProducts[0].canceled_at, - } + }, ); } @@ -142,8 +145,8 @@ export const handleSubscriptionUpdated = async ({ // CANCELED CASE if (isCanceled && updatedCusProducts.length > 0 && !isAutumnDowngrade) { - let allDefaultProducts = await ProductService.getFullDefaultProducts({ - sb, + let allDefaultProducts = await ProductService.listDefault({ + db, orgId: org.id, env, }); @@ -159,8 +162,8 @@ export const handleSubscriptionUpdated = async ({ // Default products to activate... let defaultProducts = allDefaultProducts.filter((p) => updatedCusProducts.some( - (cp: FullCusProduct) => cp.product.group == p.group - ) + (cp: FullCusProduct) => cp.product.group == p.group, + ), ); if (defaultProducts.length > 0) { @@ -168,14 +171,14 @@ export const handleSubscriptionUpdated = async ({ `subscription.updated: canceled -> attempting to schedule default products: ${defaultProducts .map((p) => p.name) .join(", ")}, period end: ${formatUnixToDateTime( - fullSub.current_period_end * 1000 - )}` + fullSub.current_period_end * 1000, + )}`, ); } for (let product of defaultProducts) { let alreadyScheduled = cusProducts.some( - (cp: FullCusProduct) => cp.product.group == product.group + (cp: FullCusProduct) => cp.product.group == product.group, ); if (alreadyScheduled) { @@ -203,10 +206,10 @@ export const handleSubscriptionUpdated = async ({ try { let product = cusProd.product; let prices = cusProd.customer_prices.map( - (cp: FullCustomerPrice) => cp.price + (cp: FullCustomerPrice) => cp.price, ); let entitlements = cusProd.customer_entitlements.map( - (ce: FullCustomerEntitlement) => ce.entitlement + (ce: FullCustomerEntitlement) => ce.entitlement, ); await addProductsUpdatedWebhookTask({ internalCustomerId: cusProd.internal_customer_id, @@ -275,10 +278,10 @@ export const handleSubscriptionUpdated = async ({ for (let cusProd of updatedCusProducts) { let product = cusProd.product; let prices = cusProd.customer_prices.map( - (cp: FullCustomerPrice) => cp.price + (cp: FullCustomerPrice) => cp.price, ); let entitlements = cusProd.customer_entitlements.map( - (ce: FullCustomerEntitlement) => ce.entitlement + (ce: FullCustomerEntitlement) => ce.entitlement, ); await addProductsUpdatedWebhookTask({ internalCustomerId: cusProd.internal_customer_id, @@ -305,7 +308,7 @@ export const handleSubscriptionUpdated = async ({ } catch (error) { logger.warn( `Failed to update sub from stripe. Stripe sub ID: ${subscription.id}, org: ${org.slug}, env: ${env}`, - error + error, ); } @@ -327,7 +330,7 @@ export const handleSubscriptionUpdated = async ({ subscriptionId: subscription.id, stripeSubId: subscription.id, error: error.message, - } + }, ); } } diff --git a/server/src/internal/api/components/componentRouter.ts b/server/src/internal/api/components/componentRouter.ts index 04b907268..f0633be08 100644 --- a/server/src/internal/api/components/componentRouter.ts +++ b/server/src/internal/api/components/componentRouter.ts @@ -18,13 +18,13 @@ componentRouter.get("/pricing_table", async (req: any, res) => res, action: "get pricing table", handler: async () => { - const { sb, orgId, env, logtail: logger } = req; + const { sb, orgId, env, db, logtail: logger } = req; let customerId = req.query.customer_id; const [org, features, products, customer] = await Promise.all([ OrgService.getFromReq(req), FeatureService.getFromReq(req), - ProductService.getFullProducts({ sb, orgId, env }), + ProductService.listFull({ db, orgId, env }), (async () => { if (!customerId) { return null; diff --git a/server/src/internal/api/customers/cusUtils.ts b/server/src/internal/api/customers/cusUtils.ts index e79c2ee3d..ddf909f9b 100644 --- a/server/src/internal/api/customers/cusUtils.ts +++ b/server/src/internal/api/customers/cusUtils.ts @@ -28,6 +28,7 @@ import { sortCusEntsForDeduction } from "@/internal/customers/entitlements/cusEn import RecaseError from "@/utils/errorUtils.js"; import { StatusCodes } from "http-status-codes"; import { notNullish, nullish } from "@/utils/genUtils.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; export const updateCustomerDetails = async ({ sb, @@ -85,53 +86,13 @@ export const getCusByIdOrInternalId = async ({ return customer; }; -export const attachDefaultProducts = async ({ - sb, - orgId, - env, - customer, - nextResetAt, - org, -}: { - sb: SupabaseClient; - orgId: string; - env: AppEnv; - customer: Customer; - org: Organization; - nextResetAt?: number; -}) => { - const defaultProds = await ProductService.getFullDefaultProducts({ - sb, - orgId, - env, - }); - - for (const product of defaultProds) { - await createFullCusProduct({ - sb, - attachParams: { - org, - customer: customer, - product, - prices: product.prices, - entitlements: product.entitlements, - freeTrial: null, // TODO: Free trial not supported on default product yet - optionsList: [], - entities: [], - features: [], - }, - nextResetAt, - }); - } -}; - const CusProductResultSchema = CusProductSchema.extend({ customer: CustomerSchema, product: ProductSchema, }); export const flipProductResults = ( - cusProducts: z.infer[] + cusProducts: z.infer[], ) => { const customers = []; @@ -169,7 +130,7 @@ export const getCusInvoices = async ({ invoice: i, withItems, features, - }) + }), ); return processedInvoices; @@ -239,7 +200,7 @@ export const getCusEntsInFeatures = async ({ if (internalFeatureIds) { cusEnts = cusEnts.filter((cusEnt) => - internalFeatureIds.includes(cusEnt.internal_feature_id) + internalFeatureIds.includes(cusEnt.internal_feature_id), ); } @@ -248,7 +209,7 @@ export const getCusEntsInFeatures = async ({ cusEnts = cusEnts.filter( (cusEnt) => nullish(cusEnt.customer_product.internal_entity_id) || - cusEnt.customer_product.internal_entity_id === entity.internal_id + cusEnt.customer_product.internal_entity_id === entity.internal_id, // || cusEnt.entities ); } diff --git a/server/src/internal/api/customers/handlers/handleCreateCustomer.ts b/server/src/internal/api/customers/handlers/handleCreateCustomer.ts index fd6584985..16cfbbc3e 100644 --- a/server/src/internal/api/customers/handlers/handleCreateCustomer.ts +++ b/server/src/internal/api/customers/handlers/handleCreateCustomer.ts @@ -64,7 +64,6 @@ export const initStripeCusAndProducts = async ({ batchInit.push( initProductInStripe({ db, - sb, org, env, logger, @@ -101,13 +100,11 @@ export const createNewCustomer = async ({ logger.info(`Org ID: ${org.id}`); logger.info(`Customer data: ${JSON.stringify(customer)}`); - const [defaultProds] = await Promise.all([ - ProductService.getFullDefaultProducts({ - sb, - orgId: org.id, - env, - }), - ]); + const defaultProds = await ProductService.listDefault({ + db, + orgId: org.id, + env, + }); const nonFreeProds = defaultProds.filter((p) => !isFreeProduct(p.prices)); const freeProds = defaultProds.filter((p) => isFreeProduct(p.prices)); diff --git a/server/src/internal/api/customers/handlers/handleCusProductExpired.ts b/server/src/internal/api/customers/handlers/handleCusProductExpired.ts index 75a06cd61..a95ec03fa 100644 --- a/server/src/internal/api/customers/handlers/handleCusProductExpired.ts +++ b/server/src/internal/api/customers/handlers/handleCusProductExpired.ts @@ -1,3 +1,4 @@ +import { DrizzleCli } from "@/db/initDrizzle.js"; import { createStripeCli } from "@/external/stripe/utils.js"; import { cancelFutureProductSchedule } from "@/internal/customers/change-product/scheduleUtils.js"; import { CusService } from "@/internal/customers/CusService.js"; @@ -65,6 +66,7 @@ export const removeScheduledProduct = async ({ }; export const expireCusProduct = async ({ + db, sb, cusProduct, // cus product to expire cusProducts, // other cus products @@ -74,6 +76,7 @@ export const expireCusProduct = async ({ customer, expireImmediately = true, }: { + db: DrizzleCli; sb: SupabaseClient; cusProduct: FullCusProduct; cusProducts: FullCusProduct[]; @@ -87,11 +90,11 @@ export const expireCusProduct = async ({ logger.info( `🔔 Expiring cutomer product (${ expireImmediately ? "immediately" : "end of cycle" - })` + })`, ); logger.info(`Customer: ${customer.id} (${env}), Org: ${org.id}`); logger.info( - `Product: ${cusProduct.product.name}, Status: ${cusProduct.status}` + `Product: ${cusProduct.product.name}, Status: ${cusProduct.status}`, ); // If current product is scheduled @@ -178,6 +181,7 @@ export const expireCusProduct = async ({ if (!cancelled) { await expireAndActivate({ + db, sb, env, cusProduct, @@ -190,6 +194,8 @@ export const expireCusProduct = async ({ export const handleCusProductExpired = async (req: any, res: any) => { try { + const { db, sb, logtail: logger } = req; + const org = await OrgService.getFromReq(req); const customerProductId = req.params.customer_product_id; @@ -212,7 +218,8 @@ export const handleCusProductExpired = async (req: any, res: any) => { }); await expireCusProduct({ - sb: req.sb, + db, + sb, cusProduct, cusProducts, org, diff --git a/server/src/internal/api/customers/handlers/handleUpdateBalances.ts b/server/src/internal/api/customers/handlers/handleUpdateBalances.ts index df4f56de8..9d55e48ea 100644 --- a/server/src/internal/api/customers/handlers/handleUpdateBalances.ts +++ b/server/src/internal/api/customers/handlers/handleUpdateBalances.ts @@ -3,7 +3,7 @@ import { handleRequestError } from "@/utils/errorUtils.js"; import { CusService } from "@/internal/customers/CusService.js"; import { FeatureService } from "@/internal/features/FeatureService.js"; import RecaseError from "@/utils/errorUtils.js"; -import { CusProductStatus, ErrCode } from "@autumn/shared"; +import { ErrCode } from "@autumn/shared"; import { StatusCodes } from "http-status-codes"; import { getCusEntsInFeatures } from "../cusUtils.js"; import { Decimal } from "decimal.js"; @@ -13,8 +13,6 @@ import { } from "@/trigger/updateBalanceTask.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; -import { initGroupBalancesFromUpdateBalances } from "@/internal/customers/entitlements/groupByUtils.js"; - import { getCusEntBalance, getUnlimitedAndUsageAllowed, @@ -51,7 +49,7 @@ export const handleUpdateBalances = async (req: any, res: any) => { try { const logger = req.logtail; const cusId = req.params.customer_id; - const { sb, env } = req; + const { sb, env, db } = req; const { balances } = req.body; if (!Array.isArray(balances)) { @@ -65,7 +63,7 @@ export const handleUpdateBalances = async (req: any, res: any) => { const { customer, features, org } = await getCusFeaturesAndOrg(req, cusId); const featuresToUpdate = features.filter((f: any) => - balances.map((b: any) => b.feature_id).includes(f.id) + balances.map((b: any) => b.feature_id).includes(f.id), ); if (featuresToUpdate.length === 0) { @@ -85,22 +83,15 @@ export const handleUpdateBalances = async (req: any, res: any) => { logger: req.logtail, }); - // // Initialize balances - // await initGroupBalancesFromUpdateBalances({ - // sb: req.sb, - // cusEnts, - // features: featuresToUpdate, - // updates: balances, - // }); - logger.info("--------------------------------"); logger.info( - `REQUEST: UPDATE BALANCES FOR CUSTOMER ${customer.id}, ORG: ${req.minOrg.slug}` + `REQUEST: UPDATE BALANCES FOR CUSTOMER ${customer.id}, ORG: ${req.minOrg.slug}`, ); logger.info( `Features to update: ${balances.map( - (b: any) => `${b.feature_id} - ${b.unlimited ? "unlimited" : b.balance}` - )}` + (b: any) => + `${b.feature_id} - ${b.unlimited ? "unlimited" : b.balance}`, + )}`, ); // Get deductions for each feature @@ -201,17 +192,18 @@ export const handleUpdateBalances = async (req: any, res: any) => { ? cusEnts.find( (cusEnt) => cusEnt.internal_feature_id === feature!.internal_id! && - cusEnt.entitlement.interval === interval + cusEnt.entitlement.interval === interval, ) : cusEnts.find( - (cusEnt) => cusEnt.internal_feature_id === feature!.internal_id! + (cusEnt) => + cusEnt.internal_feature_id === feature!.internal_id!, ); if (!cusEnt) { logger.warn( `No active cus ent to set unlimited balance for feature: ${ feature!.id - }` + }`, ); return; } @@ -239,7 +231,8 @@ export const handleUpdateBalances = async (req: any, res: any) => { toDeduct = await deductAllowanceFromCusEnt({ toDeduct, deductParams: { - sb: req.sb, + db, + sb, feature: featureDeduction.feature!, env: req.env, org, @@ -262,6 +255,7 @@ export const handleUpdateBalances = async (req: any, res: any) => { toDeduct, cusEnts, deductParams: { + db, sb, feature: featureDeduction.feature!, env, diff --git a/server/src/internal/api/customers/handlers/handleUpdateEntitlement.ts b/server/src/internal/api/customers/handlers/handleUpdateEntitlement.ts index 6364181de..d12234406 100644 --- a/server/src/internal/api/customers/handlers/handleUpdateEntitlement.ts +++ b/server/src/internal/api/customers/handlers/handleUpdateEntitlement.ts @@ -16,21 +16,22 @@ import { } from "@/internal/customers/entitlements/cusEntUtils.js"; import { performDeductionOnCusEnt } from "@/trigger/updateBalanceTask.js"; import { ExtendedRequest } from "@/utils/models/Request.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; const getCusOrgAndCusPrice = async ({ + db, req, sb, cusEnt, - orgId, }: { + db: DrizzleCli; req: ExtendedRequest; sb: SupabaseClient; cusEnt: FullCustomerEntitlement; - orgId: string; }) => { const [cusPrice, customer, org] = await Promise.all([ CusPriceService.getRelatedToCusEnt({ - sb: sb, + db, cusEnt, }), CusService.getByInternalId({ @@ -45,6 +46,7 @@ const getCusOrgAndCusPrice = async ({ export const handleUpdateEntitlement = async (req: any, res: any) => { try { + const { db, sb } = req; const { customer_entitlement_id } = req.params; const { balance, next_reset_at, entity_id } = req.body; @@ -125,10 +127,10 @@ export const handleUpdateEntitlement = async (req: any, res: any) => { }); const { cusPrice, customer, org } = await getCusOrgAndCusPrice({ + db, req, - sb: req.sb, + sb, cusEnt, - orgId: req.orgId, }); if (!cusPrice) { @@ -137,6 +139,7 @@ export const handleUpdateEntitlement = async (req: any, res: any) => { } await adjustAllowance({ + db, sb: req.sb, env: req.env, org: org, diff --git a/server/src/internal/api/customers/products/attachRouter.ts b/server/src/internal/api/customers/products/attachRouter.ts index 5600f37c0..309331e17 100644 --- a/server/src/internal/api/customers/products/attachRouter.ts +++ b/server/src/internal/api/customers/products/attachRouter.ts @@ -186,7 +186,7 @@ export const checkStripeConnections = async ({ for (const product of products) { batchProductUpdates.push( checkStripeProductExists({ - sb: req.sb, + db: req.db, org, env, product, @@ -436,7 +436,6 @@ attachRouter.post("/attach", async (req: any, res) => { res, attachParams, curCusProduct, - isCustom, }); return; } diff --git a/server/src/internal/api/customers/products/expireRouter.ts b/server/src/internal/api/customers/products/expireRouter.ts index 21b8834ff..b10d0a7a8 100644 --- a/server/src/internal/api/customers/products/expireRouter.ts +++ b/server/src/internal/api/customers/products/expireRouter.ts @@ -14,7 +14,7 @@ expireRouter.post("", async (req, res) => res, action: "expire", handler: async (req, res) => { - let { sb, orgId, env, logtail: logger } = req; + let { db, sb, orgId, env, logtail: logger } = req; let { customer_id, product_id, entity_id, cancel_immediately } = req.body; let expireImmediately = cancel_immediately || false; @@ -44,7 +44,7 @@ expireRouter.post("", async (req, res) => let cusProductsToExpire = cusProducts.filter( (cusProduct: FullCusProduct) => cusProduct.product.id == product_id && - (entity_id ? cusProduct.entity_id == entity_id : true) + (entity_id ? cusProduct.entity_id == entity_id : true), ); if (cusProductsToExpire.length == 0) { @@ -56,6 +56,7 @@ expireRouter.post("", async (req, res) => for (const cusProduct of cusProductsToExpire) { await expireCusProduct({ + db, sb, cusProduct, cusProducts, @@ -73,7 +74,7 @@ expireRouter.post("", async (req, res) => product_id: product_id, }); }, - }) + }), ); export default expireRouter; diff --git a/server/src/internal/api/entities/handleCreateEntity.ts b/server/src/internal/api/entities/handleCreateEntity.ts index 97dd2a261..b4c09cc8d 100644 --- a/server/src/internal/api/entities/handleCreateEntity.ts +++ b/server/src/internal/api/entities/handleCreateEntity.ts @@ -23,6 +23,8 @@ import { import { getEntityResponse } from "./getEntityUtils.js"; import { StatusCodes } from "http-status-codes"; import { orgToVersion } from "@/utils/versionUtils.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { routeHandler } from "@/utils/routerUtils.js"; interface CreateEntityData { id: string; @@ -154,7 +156,7 @@ export const logEntityToAction = ({ entityToAction[id].replace.internal_id })` : "" - }` + }`, ); } }; @@ -227,8 +229,8 @@ export const validateAndGetInputEntities = async ({ logger.info("Existing entities:"); logger.info( existingEntities.map( - (e: any) => `${e.id} - ${e.name}, deleted: ${e.deleted}` - ) + (e: any) => `${e.id} - ${e.name}, deleted: ${e.deleted}`, + ), ); let noIdEntities = existingEntities.filter((e: any) => !e.id); @@ -266,6 +268,7 @@ export const validateAndGetInputEntities = async ({ }; export const createEntities = async ({ + db, sb, env, org, @@ -277,6 +280,7 @@ export const createEntities = async ({ apiVersion, fromAutoCreate = false, }: { + db: DrizzleCli; sb: any; org: Organization; features: Feature[]; @@ -324,12 +328,12 @@ export const createEntities = async ({ let product = cusProduct.product; let mainCusEnt = cusEnts.find( - (e: any) => e.entitlement.feature.id === feature_id + (e: any) => e.entitlement.feature.id === feature_id, ); // Get linked features let linkedCusEnts = cusEnts.filter( - (e: any) => e.entitlement.entity_feature_id === feature.id + (e: any) => e.entitlement.entity_feature_id === feature.id, ); if (linkedCusEnts.length > 0 && inputEntities.some((e: any) => !e.id)) { @@ -342,18 +346,18 @@ export const createEntities = async ({ // 1. Pay for new seats let replacedCount = Object.keys(entityToAction).filter( - (id) => entityToAction[id].action === "replace" + (id) => entityToAction[id].action === "replace", ).length; let newCount = Object.keys(entityToAction).filter( - (id) => entityToAction[id].action === "create" + (id) => entityToAction[id].action === "create", ).length; if (mainCusEnt) { if (fromAutoCreate) { let cusPrice = getRelatedCusPrice( mainCusEnt, - cusProduct.customer_prices || [] + cusProduct.customer_prices || [], ); if (cusPrice) { @@ -371,6 +375,7 @@ export const createEntities = async ({ mainCusEnt.balance - (newCount + replacedCount) + (unused || 0); await adjustAllowance({ + db, sb, env, org, @@ -480,49 +485,51 @@ export const createEntities = async ({ return entities; }; -export const handlePostEntityRequest = async (req: any, res: any) => { - try { - // Create entity! - const { sb, pg, env, logtail: logger } = req; +export const handlePostEntityRequest = async (req: any, res: any) => + routeHandler({ + req, + res, + action: "create entity", + handler: async (req: any, res: any) => { + const { sb, env, db, logtail: logger } = req; - const [org, features] = await Promise.all([ - OrgService.getFromReq(req), - FeatureService.getFromReq(req), - ]); + const [org, features] = await Promise.all([ + OrgService.getFromReq(req), + FeatureService.getFromReq(req), + ]); - let apiVersion = orgToVersion({ - org, - reqApiVersion: req.apiVersion, - }); - - const entities = await createEntities({ - sb, - org, - features, - logger, - env, - customerId: req.params.customer_id, - createEntityData: req.body, - withAutumnId: req.query.with_autumn_id === "true", - apiVersion, - }); - - logger.info(` Created / replaced entities!`); - - if (apiVersion < APIVersion.v1_2) { - res.status(200).json({ - success: true, + let apiVersion = orgToVersion({ + org, + reqApiVersion: req.apiVersion, }); - return; - } - if (Array.isArray(req.body)) { - res.status(200).json({ - list: entities, + + const entities = await createEntities({ + db, + sb, + org, + features, + logger, + env, + customerId: req.params.customer_id, + createEntityData: req.body, + withAutumnId: req.query.with_autumn_id === "true", + apiVersion, }); - } else { - res.status(200).json(entities[0]); - } - } catch (error) { - handleRequestError({ error, req, res, action: "create entity" }); - } -}; + + logger.info(` Created / replaced entities!`); + + if (apiVersion < APIVersion.v1_2) { + res.status(200).json({ + success: true, + }); + return; + } + if (Array.isArray(req.body)) { + res.status(200).json({ + list: entities, + }); + } else { + res.status(200).json(entities[0]); + } + }, + }); diff --git a/server/src/internal/api/entities/handleDeleteEntity.ts b/server/src/internal/api/entities/handleDeleteEntity.ts index 4b2c4a4e7..b04d85556 100644 --- a/server/src/internal/api/entities/handleDeleteEntity.ts +++ b/server/src/internal/api/entities/handleDeleteEntity.ts @@ -10,19 +10,17 @@ import { getCusEntMasterBalance, getRelatedCusPrice, } from "@/internal/customers/entitlements/cusEntUtils.js"; +import { createStripeCli } from "@/external/stripe/utils.js"; import { fullCusProductToCusEnts } from "@/internal/customers/products/cusProductUtils.js"; import { removeEntityFromCusEnt } from "./entityUtils.js"; import { CustomerEntitlementService } from "@/internal/customers/entitlements/CusEntitlementService.js"; - -import { createStripeCli } from "@/external/stripe/utils.js"; - import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js"; import { cancelCurSubs } from "@/internal/customers/change-product/handleDowngrade/cancelCurSubs.js"; import { removeScheduledProduct } from "../customers/handlers/handleCusProductExpired.js"; export const handleDeleteEntity = async (req: any, res: any) => { try { - const { orgId, env, logtail: logger, sb } = req; + const { orgId, env, db, logtail: logger, sb } = req; const { customer_id, entity_id } = req.params; await handleCustomerRaceCondition({ @@ -96,7 +94,7 @@ export const handleDeleteEntity = async (req: any, res: any) => { let cusEnt = cusEnts.find( (e: any) => - e.entitlement.feature.internal_id === entity.internal_feature_id + e.entitlement.feature.internal_id === entity.internal_feature_id, ); if (!cusEnt) { @@ -105,7 +103,7 @@ export const handleDeleteEntity = async (req: any, res: any) => { let relatedCusPrice = getRelatedCusPrice( cusEnt, - cusProduct.customer_prices + cusProduct.customer_prices, ); if (relatedCusPrice) { @@ -120,6 +118,7 @@ export const handleDeleteEntity = async (req: any, res: any) => { let newBalance = cusEnt.balance + 1 + (unused || 0); await adjustAllowance({ + db, sb, env, org, @@ -144,7 +143,7 @@ export const handleDeleteEntity = async (req: any, res: any) => { for (const cusEnt of cusEnts) { let relatedCusPrice = getRelatedCusPrice( cusEnt, - cusProducts.flatMap((p: any) => p.customer_prices) + cusProducts.flatMap((p: any) => p.customer_prices), ); await removeEntityFromCusEnt({ sb, @@ -194,7 +193,7 @@ export const handleDeleteEntity = async (req: any, res: any) => { // Perform deduction on cus ent let updateCusEnt = cusEnts.find( - (e: any) => e.entitlement.feature.id === entity.feature_id + (e: any) => e.entitlement.feature.id === entity.feature_id, ); if (updateCusEnt) { await CustomerEntitlementService.incrementBalance({ diff --git a/server/src/internal/api/entitled/checkUtils.ts b/server/src/internal/api/entitled/checkUtils.ts index b53ffc2b3..9c1fac8cc 100644 --- a/server/src/internal/api/entitled/checkUtils.ts +++ b/server/src/internal/api/entitled/checkUtils.ts @@ -5,10 +5,9 @@ import { import { orgToVersion } from "@/utils/versionUtils.js"; import { APIVersion, - CusEntWithEntitlement, Feature, FullCusProduct, - FullProduct, + FullCustomerEntitlement, Organization, ProductItem, SuccessCode, @@ -16,8 +15,10 @@ import { } from "@autumn/shared"; import { getCheckPreview } from "./getCheckPreview.js"; import { SupabaseClient } from "@supabase/supabase-js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; export const getBooleanEntitledResult = async ({ + db, sb, customer_id, cusEnts, @@ -29,9 +30,10 @@ export const getBooleanEntitledResult = async ({ cusProducts, allFeatures, }: { + db: DrizzleCli; sb: SupabaseClient; customer_id: string; - cusEnts: CusEntWithEntitlement[]; + cusEnts: FullCustomerEntitlement[]; org: Organization; res: any; feature: Feature; @@ -52,6 +54,7 @@ export const getBooleanEntitledResult = async ({ allowed, preview: withPreview ? await getCheckPreview({ + db, sb, allowed, balance: undefined, diff --git a/server/src/internal/api/entitled/entitledRouter.ts b/server/src/internal/api/entitled/entitledRouter.ts index c4f8f219d..e99d33a00 100644 --- a/server/src/internal/api/entitled/entitledRouter.ts +++ b/server/src/internal/api/entitled/entitledRouter.ts @@ -296,7 +296,7 @@ entitledRouter.post("", async (req: any, res: any) => { entity_id, } = req.body; - const { logtail: logger } = req; + const { logtail: logger, sb, db } = req; if (!customer_id) { throw new RecaseError({ @@ -348,8 +348,6 @@ entitledRouter.post("", async (req: any, res: any) => { quantity = floatQuantity; } - const { sb } = req; - const { cusEnts, feature, creditSystems, org, cusProducts, allFeatures } = await getCusEntsAndFeatures({ sb, @@ -367,6 +365,7 @@ entitledRouter.post("", async (req: any, res: any) => { // 2. If boolean, return true if (feature.type === FeatureType.Boolean) { return await getBooleanEntitledResult({ + db, customer_id, res, cusEnts, @@ -451,6 +450,7 @@ entitledRouter.post("", async (req: any, res: any) => { try { preview = await getCheckPreview({ + db, allowed, balance: balanceObj?.balance, feature: featureToUse!, diff --git a/server/src/internal/api/entitled/getCheckPreview.ts b/server/src/internal/api/entitled/getCheckPreview.ts index 977055855..07b71de5c 100644 --- a/server/src/internal/api/entitled/getCheckPreview.ts +++ b/server/src/internal/api/entitled/getCheckPreview.ts @@ -1,3 +1,4 @@ +import { DrizzleCli } from "@/db/initDrizzle.js"; import { fullCusProductToProduct } from "@/internal/customers/products/cusProductUtils.js"; import { ProductService } from "@/internal/products/ProductService.js"; import { @@ -17,6 +18,7 @@ import { import { SupabaseClient } from "@supabase/supabase-js"; export const getCheckPreview = async ({ + db, sb, allowed, balance, @@ -25,6 +27,7 @@ export const getCheckPreview = async ({ raw = false, allFeatures, }: { + db: DrizzleCli; sb: SupabaseClient; allowed: boolean; balance?: number; @@ -49,7 +52,7 @@ export const getCheckPreview = async ({ cusOwnedProducts.length > 0 ? cusOwnedProducts[0] : null; let products: FullProduct[] = await ProductService.getByFeature({ - sb, + db, internalFeatureId: feature.internal_id!, }); diff --git a/server/src/internal/api/entitled/handlers/handleProductCheck.ts b/server/src/internal/api/entitled/handlers/handleProductCheck.ts index 25305f765..3e503fe0d 100644 --- a/server/src/internal/api/entitled/handlers/handleProductCheck.ts +++ b/server/src/internal/api/entitled/handlers/handleProductCheck.ts @@ -44,11 +44,11 @@ export const handleProductCheck = async ({ entityId: entity_id, entityData: entity_data, }), - ProductService.getFullProduct({ - sb, + ProductService.getFull({ + db, orgId, env, - productId: product_id, + idOrInternalId: product_id, }), ]); diff --git a/server/src/internal/api/features/handlers/handleUpdateFeature.ts b/server/src/internal/api/features/handlers/handleUpdateFeature.ts index 2c6fe6a61..ee2157b01 100644 --- a/server/src/internal/api/features/handlers/handleUpdateFeature.ts +++ b/server/src/internal/api/features/handlers/handleUpdateFeature.ts @@ -292,7 +292,7 @@ export const handleUpdateFeature = async (req: any, res: any) => if (isChangingType || isChangingId || isChangingUsageType) { let { entitlements, prices, creditSystems, linkedEntitlements } = await getObjectsUsingFeature({ - sb: req.sb, + db, orgId: req.orgId, env: req.env, allFeatures: features, diff --git a/server/src/internal/api/migrations/migrationRouter.ts b/server/src/internal/api/migrations/migrationRouter.ts index 1d5fdc05b..f8b184a63 100644 --- a/server/src/internal/api/migrations/migrationRouter.ts +++ b/server/src/internal/api/migrations/migrationRouter.ts @@ -18,24 +18,24 @@ migrationRouter.post("", async (req: any, res: any) => { res, action: "migrate", handler: async (req: any, res: any) => { - const { orgId, env, sb } = req; + const { orgId, env, sb, db } = req; const { from_product_id, from_version, to_product_id, to_version } = req.body; - let fromProduct = await ProductService.getFullProduct({ - sb, + let fromProduct = await ProductService.getFull({ + db, env, orgId, - productId: from_product_id, + idOrInternalId: from_product_id, version: from_version, }); - let toProduct = await ProductService.getFullProduct({ - sb, + let toProduct = await ProductService.getFull({ + db, env, orgId, - productId: to_product_id, + idOrInternalId: to_product_id, version: to_version, }); diff --git a/server/src/internal/api/products/handleDeleteProduct.ts b/server/src/internal/api/products/handleDeleteProduct.ts index ad987789d..616021e27 100644 --- a/server/src/internal/api/products/handleDeleteProduct.ts +++ b/server/src/internal/api/products/handleDeleteProduct.ts @@ -11,18 +11,12 @@ export const handleDeleteProduct = (req: any, res: any) => res, action: "delete product", handler: async () => { + const { db, orgId, env, sb } = req; const { productId } = req.params; - const sb = req.sb; - const orgId = req.orgId; - const env = req.env; - console.log("Org ID", orgId); - console.log("Product ID", productId); - console.log("Env", env); - - const product = await ProductService.getProductStrict({ - sb, - productId, + const product = await ProductService.get({ + db, + id: productId, orgId, env, }); @@ -50,7 +44,7 @@ export const handleDeleteProduct = (req: any, res: any) => // 2. Delete prices, entitlements, and product await ProductService.deleteByInternalId({ - sb, + db, internalId: product.internal_id, orgId, env, diff --git a/server/src/internal/api/products/handleGetProduct.ts b/server/src/internal/api/products/handleGetProduct.ts index 1714f6777..2bc03f90d 100644 --- a/server/src/internal/api/products/handleGetProduct.ts +++ b/server/src/internal/api/products/handleGetProduct.ts @@ -1,10 +1,12 @@ import { FeatureService } from "@/internal/features/FeatureService.js"; import { ProductService } from "@/internal/products/ProductService.js"; import { + getProductResponse, mapToProductItems, mapToProductV2, } from "@/internal/products/productV2Utils.js"; import RecaseError from "@/utils/errorUtils.js"; +import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js"; import { routeHandler } from "@/utils/routerUtils.js"; import { ErrCode, ProductResponseSchema } from "@autumn/shared"; import { ProductItemResponseSchema } from "@autumn/shared"; @@ -14,14 +16,12 @@ export const handleGetProduct = async (req: any, res: any) => routeHandler({ req, res, - action: "get /products/:productId", - handler: async () => { + action: "get product", + handler: async (req: ExtendedRequest, res: ExtendedResponse) => { const { productId } = req.params; - let { schemaVersion } = req.query; + let { schemaVersion } = req.query as { schemaVersion: string }; - const sb = req.sb; - const orgId = req.orgId; - const env = req.env; + const { db, orgId, env } = req; if (!productId) { throw new RecaseError({ @@ -31,11 +31,11 @@ export const handleGetProduct = async (req: any, res: any) => } let [product, features] = await Promise.all([ - ProductService.getFullProduct({ - sb, + ProductService.getFull({ + db, orgId, env, - productId, + idOrInternalId: productId, }), FeatureService.getFromReq(req), ]); @@ -48,26 +48,16 @@ export const handleGetProduct = async (req: any, res: any) => }); } - schemaVersion = schemaVersion ? parseInt(schemaVersion) : 2; + let schemaVersionInt = schemaVersion ? parseInt(schemaVersion) : 2; - if (schemaVersion == 1) { + if (schemaVersionInt == 1) { res.status(200).json(product); } else { res.status(200).json( - ProductResponseSchema.parse({ - ...product, - name: product.name || null, - group: product.group || null, - // autumn_id: product.internal_id, - items: mapToProductItems({ - prices: product.prices, - entitlements: product.entitlements, - features: features, - }).map((item) => { - // console.log(item); - return ProductItemResponseSchema.parse(item); - }), - }) + getProductResponse({ + product, + features, + }), ); } }, diff --git a/server/src/internal/api/products/handleUpdateProduct.ts b/server/src/internal/api/products/handleUpdateProduct.ts index cb26bfc2f..e86c768aa 100644 --- a/server/src/internal/api/products/handleUpdateProduct.ts +++ b/server/src/internal/api/products/handleUpdateProduct.ts @@ -21,20 +21,21 @@ import { routeHandler } from "@/utils/routerUtils.js"; import { handleNewProductItems } from "@/internal/products/product-items/productItemInitUtils.js"; import { RewardProgramService } from "@/internal/rewards/RewardProgramService.js"; import { mapToProductItems } from "@/internal/products/productV2Utils.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; export const handleUpdateProductDetails = async ({ + db, newProduct, curProduct, org, sb, - cusProductExists, rewardPrograms, }: { + db: DrizzleCli; curProduct: Product; newProduct: UpdateProduct; org: Organization; sb: SupabaseClient; - cusProductExists: boolean; rewardPrograms: RewardProgram[]; }) => { // 1. Check if they're same @@ -96,8 +97,8 @@ export const handleUpdateProductDetails = async ({ console.log(`Updating product ${curProduct.id} (org: ${org.slug})`); // 2. Update product - await ProductService.update({ - sb, + await ProductService.updateByInternalId({ + db, internalId: curProduct.internal_id, update: { id: newProduct.id, @@ -114,79 +115,6 @@ export const handleUpdateProductDetails = async ({ curProduct.is_default = newProduct.is_default || curProduct.is_default; }; -export const handleUpdateProduct = async (req: any, res: any) => { - const { productId } = req.params; - const sb = req.sb; - const orgId = req.orgId; - const env = req.env; - - const { prices, entitlements, free_trial } = req.body; - - try { - const [features, org, fullProduct] = await Promise.all([ - FeatureService.getFromReq(req), - OrgService.getFromReq(req), - ProductService.getFullProduct({ - sb, - productId, - orgId, - env, - }), - ]); - - if (!fullProduct) { - throw new RecaseError({ - message: "Product not found", - code: ErrCode.ProductNotFound, - statusCode: 404, - }); - } - - const cusProductsCurVersion = - await CusProductService.getByInternalProductId( - sb, - fullProduct.internal_id, - ); - - let cusProductExists = cusProductsCurVersion.length > 0; - - await handleUpdateProductDetails({ - sb, - curProduct: fullProduct, - newProduct: UpdateProductSchema.parse(req.body), - org, - cusProductExists, - rewardPrograms: [], - }); - - let productHasChanged = productsAreDifferent({ - product1: fullProduct, - product2: { - ...fullProduct, - prices: notNullish(prices) ? prices : fullProduct.prices, - entitlements: notNullish(entitlements) - ? entitlements - : fullProduct.entitlements, - free_trial: - free_trial !== undefined ? free_trial : fullProduct.free_trial, - }, - }); - - if (cusProductExists && productHasChanged) { - // would've versioned product - return; - } - - // Else update free trial, entitlements, prices - - res.status(200).json({ message: "Product updated" }); - return; - } catch (error) { - handleRequestError({ req, error, res, action: "Update product" }); - } -}; - -// Update product v2 export const handleUpdateProductV2 = async (req: any, res: any) => routeHandler({ req, @@ -202,9 +130,9 @@ export const handleUpdateProductV2 = async (req: any, res: any) => sb, orgId, }), - ProductService.getFullProduct({ - sb, - productId, + ProductService.getFull({ + db, + idOrInternalId: productId, orgId, env, }), @@ -235,11 +163,11 @@ export const handleUpdateProductV2 = async (req: any, res: any) => let cusProductExists = cusProductsCurVersion.length > 0; await handleUpdateProductDetails({ + db, sb, curProduct: fullProduct, newProduct: UpdateProductSchema.parse(req.body), org, - cusProductExists, rewardPrograms, }); diff --git a/server/src/internal/api/products/handleVersionProduct.ts b/server/src/internal/api/products/handleVersionProduct.ts index cda3f9d7d..d1eca8885 100644 --- a/server/src/internal/api/products/handleVersionProduct.ts +++ b/server/src/internal/api/products/handleVersionProduct.ts @@ -64,7 +64,7 @@ export const handleVersionProductV2 = async ({ env, }); - await ProductService.create({ sb, product: newProduct }); + await ProductService.insert({ db, product: newProduct }); await handleNewProductItems({ db, diff --git a/server/src/internal/api/products/handlers/handleCopyProduct.ts b/server/src/internal/api/products/handlers/handleCopyProduct.ts index e2e90d627..97b7bd458 100644 --- a/server/src/internal/api/products/handlers/handleCopyProduct.ts +++ b/server/src/internal/api/products/handlers/handleCopyProduct.ts @@ -36,9 +36,9 @@ export const handleCopyProduct = async (req: any, res: any) => } // 1. Check if product exists in live already... - const toProduct = await ProductService.getProductStrict({ - sb, - productId: toId, + const toProduct = await ProductService.get({ + db, + id: toId, orgId, env: toEnv, }); @@ -53,9 +53,9 @@ export const handleCopyProduct = async (req: any, res: any) => // 1. Get sandbox product const [fromFullProduct, fromFeatures, toFeatures] = await Promise.all([ - ProductService.getFullProduct({ - sb, - productId: fromProductId, + ProductService.getFull({ + db, + idOrInternalId: fromProductId, orgId, env: fromEnv, }), diff --git a/server/src/internal/api/products/handlers/handleCreateProduct.ts b/server/src/internal/api/products/handlers/handleCreateProduct.ts index bed81d221..53dab6e4f 100644 --- a/server/src/internal/api/products/handlers/handleCreateProduct.ts +++ b/server/src/internal/api/products/handlers/handleCreateProduct.ts @@ -27,7 +27,7 @@ import { ExtendedRequest } from "@/utils/models/Request.js"; const validateCreateProduct = async ({ req }: { req: ExtendedRequest }) => { let { free_trial, items } = req.body; - let { orgId, env, sb } = req; + let { orgId, env, db } = req; let productData = CreateProductSchema.parse(req.body); @@ -39,9 +39,9 @@ const validateCreateProduct = async ({ req }: { req: ExtendedRequest }) => { const [features, existingProduct] = await Promise.all([ FeatureService.getFromReq(req), - ProductService.getProductStrict({ - sb, - productId: productData.id, + ProductService.get({ + db, + id: productData.id, orgId, env, }), @@ -108,7 +108,7 @@ export const handleCreateProduct = async (req: Request, res: any) => env, }); - let product = await ProductService.create({ sb, product: newProduct }); + let product = await ProductService.insert({ db, product: newProduct }); if (notNullish(items)) { await handleNewProductItems({ diff --git a/server/src/internal/api/products/handlers/handleListProducts.ts b/server/src/internal/api/products/handlers/handleListProducts.ts index c60040286..37e0f0760 100644 --- a/server/src/internal/api/products/handlers/handleListProducts.ts +++ b/server/src/internal/api/products/handlers/handleListProducts.ts @@ -11,16 +11,19 @@ export const handleListProducts = async (req: any, res: any) => res, action: "List products", handler: async (req: ExtendedRequest, res: ExtendedResponse) => { - const [org, features, products] = await Promise.all([ - OrgService.getFromReq(req), + const { db, orgId, env } = req; + + const [features, products] = await Promise.all([ FeatureService.getFromReq(req), - ProductService.getFullProducts({ - sb: req.sb, - orgId: req.orgId, - env: req.env, + ProductService.listFull({ + db, + orgId, + env, }), ]); + console.log("Products", products); + let prods = products.map((p) => getProductResponse({ product: p, features }), ); diff --git a/server/src/internal/api/products/productRouter.ts b/server/src/internal/api/products/productRouter.ts index 6cbecbd80..5acdd1509 100644 --- a/server/src/internal/api/products/productRouter.ts +++ b/server/src/internal/api/products/productRouter.ts @@ -18,15 +18,11 @@ import { handleGetProduct } from "./handleGetProduct.js"; import { handleCopyProduct } from "./handlers/handleCopyProduct.js"; import { handleCreateProduct } from "./handlers/handleCreateProduct.js"; +import { handleListProducts } from "./handlers/handleListProducts.js"; export const productApiRouter = Router(); -productApiRouter.get("", async (req: any, res) => { - try { - } catch (error) { - handleRequestError({ req, error, res, action: "Get products" }); - } -}); +productApiRouter.get("", handleListProducts); productApiRouter.post("", handleCreateProduct); @@ -43,8 +39,8 @@ productApiRouter.post("/all/init_stripe", async (req: any, res) => { const { sb, orgId, env, logtail: logger, db } = req; const [fullProducts, org] = await Promise.all([ - ProductService.getFullProducts({ - sb, + ProductService.listFull({ + db, orgId, env, }), @@ -62,7 +58,7 @@ productApiRouter.post("/all/init_stripe", async (req: any, res) => { const batch = fullProducts.slice(i, i + productBatchSize); const batchPromises = batch.map((product) => checkStripeProductExists({ - sb, + db, org, env, product, diff --git a/server/src/internal/customers/change-product/handleChangeProduct.ts b/server/src/internal/customers/change-product/handleChangeProduct.ts index be9125d7b..211b61d10 100644 --- a/server/src/internal/customers/change-product/handleChangeProduct.ts +++ b/server/src/internal/customers/change-product/handleChangeProduct.ts @@ -15,18 +15,15 @@ export const handleChangeProduct = async ({ res, attachParams, curCusProduct, - isCustom, }: { req: any; res: any; attachParams: AttachParams; curCusProduct: FullCusProduct; - isCustom: boolean; }) => { // Get subscription const curProduct = curCusProduct.product; - const { org, customer, products, prices, entitlements, optionsList } = - attachParams; + const { org, customer, products } = attachParams; // Can only upgrade once for now if (products.length > 1) { @@ -54,11 +51,9 @@ export const handleChangeProduct = async ({ logger, }); - let product = products[0]; - - const curFullProduct = await ProductService.getFullProduct({ - sb: req.sb, - productId: curProduct.id, + const curFullProduct = await ProductService.getFull({ + db: req.db, + idOrInternalId: curProduct.id, orgId: org.id, env: customer.env, }); diff --git a/server/src/internal/customers/cusUtils/getOrCreateCustomer.ts b/server/src/internal/customers/cusUtils/getOrCreateCustomer.ts index 355021f9b..b0602e3bb 100644 --- a/server/src/internal/customers/cusUtils/getOrCreateCustomer.ts +++ b/server/src/internal/customers/cusUtils/getOrCreateCustomer.ts @@ -129,6 +129,7 @@ export const getOrCreateCustomer = async ({ logger.info(`Auto creating entity ${entityId} for customer ${customerId}`); let newEntities = await createEntities({ + db, sb, org, customerId, diff --git a/server/src/internal/customers/entitlements/cusEntUtils.ts b/server/src/internal/customers/entitlements/cusEntUtils.ts index b1f8fc5fa..3fb997e4c 100644 --- a/server/src/internal/customers/entitlements/cusEntUtils.ts +++ b/server/src/internal/customers/entitlements/cusEntUtils.ts @@ -4,8 +4,6 @@ import { AllowanceType, AppEnv, BillingType, - CusEntWithEntitlement, - CusProduct, CusProductStatus, Customer, EntInterval, @@ -418,7 +416,7 @@ export const getUnlimitedAndUsageAllowed = ({ cusEnts, internalFeatureId, }: { - cusEnts: FullCustomerEntitlement[] | CusEntWithEntitlement[]; + cusEnts: FullCustomerEntitlement[]; internalFeatureId: string; }) => { // Unlimited @@ -489,7 +487,7 @@ export const cusEntsContainFeature = ({ cusEnts, feature, }: { - cusEnts: FullCustomerEntitlement[] | CusEntWithEntitlement[]; + cusEnts: FullCustomerEntitlement[]; feature: Feature; }) => { return cusEnts.some( diff --git a/server/src/internal/customers/entitlements/groupByUtils.ts b/server/src/internal/customers/entitlements/groupByUtils.ts index 08de30bcb..0d672433a 100644 --- a/server/src/internal/customers/entitlements/groupByUtils.ts +++ b/server/src/internal/customers/entitlements/groupByUtils.ts @@ -1,346 +1,11 @@ -import { - CusEntWithEntitlement, - Feature, - FeatureType, - Event, - FullCustomerEntitlement, - CreditSchemaItem, -} from "@autumn/shared"; -import { CustomerEntitlementService } from "./CusEntitlementService.js"; -import { SupabaseClient } from "@supabase/supabase-js"; -import { - creditSystemContainsFeature, - getCreditSystemsFromFeature, -} from "@/internal/features/creditSystemUtils.js"; -import { - notNullish, - notNullOrUndefined, - nullOrUndefined, -} from "@/utils/genUtils.js"; - -export const getGroupbalanceFromParams = ({ - params, - feature, - cusEnt, -}: { - params: any; - feature: Feature; - cusEnt: CusEntWithEntitlement; -}) => { - // if (!feature.config?.group_by) { - // return { - // groupField: null, - // groupVal: null, - // balance: cusEnt.balance, - // adjustment: cusEnt.adjustment, - // }; - // } - // let groupField = feature.config?.group_by?.property; - // if (nullOrUndefined(params[groupField])) { - // return { - // groupField: null, - // groupVal: null, - // balance: cusEnt.balance, - // adjustment: cusEnt.adjustment, - // }; - // } - // let groupVal = params[groupField]; - // let balance = cusEnt.balances?.[groupVal]?.balance; - // let adjustment = cusEnt.balances?.[groupVal]?.adjustment; - // if (nullOrUndefined(balance)) { - // return { groupField, groupVal, balance: null, adjustment: null }; - // } - // return { groupField, groupVal, balance, adjustment }; -}; -// 1. Event contains group_by value -export const getGroupValFromProperties = ({ - properties, - feature, -}: { - properties: any; - feature: Feature; -}) => { - if (!feature.config?.group_by) { - return null; - } - - return properties[feature.config.group_by.property]; -}; - -export const getGroupBalanceFromProperties = ({ - properties, - feature, - features, - cusEnt, -}: { - properties: any; - feature?: Feature; - features?: Feature[]; - cusEnt: CusEntWithEntitlement; -}) => { - // if (features && !feature) { - // feature = features.find( - // (f) => f.internal_id == cusEnt.entitlement.internal_feature_id - // )!; - // } - // // TODO: Add support for credit systems? - // let groupVal = getGroupValFromProperties({ properties, feature: feature! }); - // if (nullOrUndefined(groupVal)) { - // return { - // groupVal: null, - // balance: cusEnt.balance, - // }; - // } - // let balance = cusEnt.balances?.[groupVal]?.balance; - // if (nullOrUndefined(balance)) { - // return { - // groupVal, - // balance: null, - // }; - // } - // return { - // groupVal, - // balance, - // }; -}; - -export const getGroupBalanceUpdate = ({ - // entityId, - cusEnt, - newEntities, -}: { - // entityId: any; - cusEnt: CusEntWithEntitlement; - newEntities: Record; -}) => { - // if (newEntities) { - // let adjustment = cusEnt.entities?.[entityId]?.adjustment || 0; - // return { - // entities: { - // ...cusEnt.entities, - // [entityId]: { - // balance: newBalance, - // adjustment, - // }, - // }, - // }; - // } - // return { - // balance: newBalance, - // }; -}; - -// INIT UTILS -const initCusEntGroupBalance = async ({ - sb, - cusEnt, - entityId, -}: { - sb: SupabaseClient; - cusEnt: CusEntWithEntitlement; - entityId: any; -}) => { - let entities = cusEnt.entities; - let shouldUpdate = false; - - if (!entities) { - entities = {}; - shouldUpdate = true; - } - - const entity = entities[entityId]; - - if (!entity) { - entities[entityId] = { - id: entityId, - balance: cusEnt.entitlement.allowance || 0, - adjustment: 0, - }; - - shouldUpdate = true; - } - - if (shouldUpdate) { - await CustomerEntitlementService.update({ - sb, - id: cusEnt.id, - updates: { entities }, - }); - - console.log( - ` - Initialized ${cusEnt.feature_id} balance for entity ${entityId}` - ); - return entities; - } - - return entities; -}; - -export const initGroupBalances = async ({ - sb, - feature, - cusEnts, - groupValue, -}: { - sb: SupabaseClient; - feature: Feature; - cusEnts: CusEntWithEntitlement[]; - groupValue: any; -}) => { - // let groupField = feature.config?.group_by?.property; - // if (!groupField) { - // return; - // } - // let batchInit = []; - // for (const cusEnt of cusEnts) { - // batchInit.push(initCusEntGroupBalance({ sb, cusEnt, groupValue })); - // } - // const results = await Promise.all(batchInit); - // for (let i = 0; i < results.length; i++) { - // cusEnts[i].entities = results[i]; - // } -}; - -export const initGroupBalancesForEvent = async ({ - sb, - features, - cusEnts, - properties, -}: { - sb: SupabaseClient; - features: Feature[]; - cusEnts: CusEntWithEntitlement[]; - properties: any; -}) => { - let meteredFeatures = features.filter((f) => f.type == FeatureType.Metered); - - for (const feature of meteredFeatures) { - const groupField = feature.config?.group_by?.property; - if (!groupField) { - continue; - } - - const groupValue = properties[groupField]; - if (nullOrUndefined(groupValue)) { - continue; - } - - let creditSystems = features.filter( - (f) => - f.type == FeatureType.CreditSystem && - creditSystemContainsFeature({ - creditSystem: f, - meteredFeatureId: feature.id, - }) - ); - - let affectedCusEnts = cusEnts.filter((cusEnt) => { - return [feature, ...creditSystems].some((f) => { - return cusEnt.entitlement.internal_feature_id == f.internal_id; - }); - }); - - if (affectedCusEnts.length > 0) { - await initGroupBalances({ - sb, - feature, - cusEnts: affectedCusEnts, - groupValue, - }); - } - } -}; - -export const initGroupBalancesFromGetCus = async ({ - sb, - cusEnts, - params, -}: { - sb: SupabaseClient; - cusEnts: FullCustomerEntitlement[]; - params: any; -}) => { - let features = cusEnts.map((cusEnt) => cusEnt.entitlement.feature); - - for (const query in params) { - let groupField = query; - let groupValue = params[query]; - - let feature = features.find( - (f) => f.config?.group_by?.property == groupField - ); - - if (!feature || nullOrUndefined(groupValue)) { - continue; - } - - let creditSystems = getCreditSystemsFromFeature({ - featureId: feature.id, - features, - }); - - let affectedCusEnts = cusEnts.filter((cusEnt) => { - return [feature, ...creditSystems].some((f) => { - return cusEnt.entitlement.internal_feature_id == f.internal_id; - }); - }); - - if (affectedCusEnts.length > 0) { - await initGroupBalances({ - sb, - feature, - cusEnts: affectedCusEnts, - groupValue, - }); - } - } -}; - -export const initGroupBalancesFromUpdateBalances = async ({ - sb, - cusEnts, - updates, - features, -}: { - sb: SupabaseClient; - updates: any; - cusEnts: CusEntWithEntitlement[]; - features: Feature[]; -}) => { - for (const update of updates) { - let featureId = update.feature_id; - let feature = features.find((f) => f.id == featureId); - if (!feature) { - continue; - } - - let groupField = feature.config?.group_by?.property; - let groupValue = update[groupField]; - - if (nullOrUndefined(groupValue)) { - continue; - } - - let affectedCusEnts = cusEnts.filter((cusEnt) => { - return cusEnt.entitlement.internal_feature_id == feature.internal_id; - }); - - if (affectedCusEnts.length > 0) { - await initGroupBalances({ - sb, - feature, - cusEnts: affectedCusEnts, - groupValue, - }); - } - } -}; +import { FullCustomerEntitlement } from "@autumn/shared"; +import { notNullish } from "@/utils/genUtils.js"; export const getResetBalancesUpdate = ({ cusEnt, allowance, }: { - cusEnt: CusEntWithEntitlement; + cusEnt: FullCustomerEntitlement; allowance?: number; }) => { let update = {}; diff --git a/server/src/internal/customers/internalCusRouter.ts b/server/src/internal/customers/internalCusRouter.ts index 0c979f933..616b54e9a 100644 --- a/server/src/internal/customers/internalCusRouter.ts +++ b/server/src/internal/customers/internalCusRouter.ts @@ -72,23 +72,19 @@ cusRouter.post("/search", async (req: any, res: any) => { }); cusRouter.get("/:customer_id/data", async (req: any, res: any) => { - const { sb, org, env } = req; - const { customer_id } = req.params; - const orgId = req.orgId; - try { - // Get customer invoices + const { db, sb, org, features, env } = req; + const { customer_id } = req.params; + const orgId = req.orgId; - const [org, features, coupons, products, customer] = await Promise.all([ - OrgService.getFromReq(req), - FeatureService.getFromReq(req), + const [coupons, products, customer] = await Promise.all([ RewardService.getAll({ sb, orgId: orgId, env, }), - ProductService.getFullProducts({ sb, orgId, env, returnAll: true }), + ProductService.listFull({ db, orgId, env, returnAll: true }), CusService.getByIdOrInternalId({ sb, orgId, @@ -294,12 +290,12 @@ cusRouter.get("/:customer_id/referrals", async (req: any, res: any) => { cusRouter.get( "/:customer_id/product/:product_id", async (req: any, res: any) => { - const { sb, org, env } = req; - const { customer_id, product_id } = req.params; - const { version, customer_product_id, entity_id } = req.query; - const orgId = req.orgId; - try { + const { sb, org, env, db } = req; + const { customer_id, product_id } = req.params; + const { version, customer_product_id, entity_id } = req.query; + const orgId = req.orgId; + const customer = await CusService.getWithProducts({ sb, orgId, @@ -372,11 +368,11 @@ cusRouter.get( isCustom: cusProduct.is_custom, }; } else { - product = await ProductService.getFullProduct({ - sb, + product = await ProductService.getFull({ + db, orgId, env, - productId: product_id, + idOrInternalId: product_id, version: version && Number.isInteger(parseInt(version)) ? parseInt(version) @@ -387,7 +383,7 @@ cusRouter.get( } let numVersions = await ProductService.getProductVersionCount({ - sb, + db, orgId, env, productId: product_id, diff --git a/server/src/internal/customers/previews/getUpgradePreview.ts b/server/src/internal/customers/previews/getUpgradePreview.ts index ceca7c673..e072c2cf6 100644 --- a/server/src/internal/customers/previews/getUpgradePreview.ts +++ b/server/src/internal/customers/previews/getUpgradePreview.ts @@ -134,7 +134,7 @@ const createStripeProductAndPrices = async ({ }) => { if (!product.processor?.id) { await checkStripeProductExists({ - sb, + db, org, env, product, diff --git a/server/src/internal/customers/prices/CusPriceService.ts b/server/src/internal/customers/prices/CusPriceService.ts index 320ae5303..a9748f019 100644 --- a/server/src/internal/customers/prices/CusPriceService.ts +++ b/server/src/internal/customers/prices/CusPriceService.ts @@ -1,72 +1,28 @@ -import RecaseError from "@/utils/errorUtils.js"; -import { - CustomerEntitlement, - ErrCode, - FullCustomerEntitlement, -} from "@autumn/shared"; -import { SupabaseClient } from "@supabase/supabase-js"; -import { StatusCodes } from "http-status-codes"; +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { FullCustomerEntitlement, FullCustomerPrice } from "@autumn/shared"; +import { customerPrices } from "@shared/models/cusProductModels/cusPriceModels/cusPriceTable.js"; + +import { eq } from "drizzle-orm"; export class CusPriceService { - static async getByCusProductId({ - sb, - customerProductId, - }: { - sb: SupabaseClient; - customerProductId: string; - }) { - const { data, error } = await sb - .from("customer_prices") - .select("*, price:prices(*)") - .eq("customer_product_id", customerProductId); - - if (error) { - throw new RecaseError({ - message: "Error getting customer prices", - code: ErrCode.GetCusPriceFailed, - statusCode: StatusCodes.INTERNAL_SERVER_ERROR, - data: error, - }); - } - - return data; - } - static async getRelatedToCusEnt({ - sb, + db, cusEnt, }: { - sb: SupabaseClient; + db: DrizzleCli; cusEnt: FullCustomerEntitlement; }) { - const { data, error } = await sb - .from("customer_prices") - .select("*, price:prices!inner(*)") - .eq("customer_product_id", cusEnt.customer_product_id) - .eq("price.entitlement_id", cusEnt.entitlement.id); - // .eq( - // "price.config->>internal_feature_id", - // cusEnt.entitlement.internal_feature_id - // ); + const customerPricesData = await db.query.customerPrices.findMany({ + where: eq(customerPrices.customer_product_id, cusEnt.customer_product_id), + with: { + price: true, + }, + }); - if (error) { - throw new RecaseError({ - message: "Error getting customer prices", - code: ErrCode.GetCusPriceFailed, - statusCode: StatusCodes.INTERNAL_SERVER_ERROR, - data: error, - }); - } + const matchingCustomerPrice = customerPricesData.find( + (cp) => cp.price?.entitlement_id === cusEnt.entitlement.id, + ) as FullCustomerPrice | undefined; - if (data.length === 0) { - return null; - throw new RecaseError({ - message: `No customer price found for usage based entitlement ${cusEnt.entitlement.internal_feature_id}`, - code: ErrCode.CusPriceNotFound, - statusCode: StatusCodes.NOT_FOUND, - }); - } - - return data[0]; + return matchingCustomerPrice || null; } } diff --git a/server/src/internal/customers/products/attachUtils.ts b/server/src/internal/customers/products/attachUtils.ts index 423ede7d9..dcbeab99d 100644 --- a/server/src/internal/customers/products/attachUtils.ts +++ b/server/src/internal/customers/products/attachUtils.ts @@ -39,14 +39,14 @@ import { Decimal } from "decimal.js"; import { DrizzleCli } from "@/db/initDrizzle.js"; const getProducts = async ({ - sb, + db, productId, productIds, orgId, env, version, }: { - sb: SupabaseClient; + db: DrizzleCli; productId?: string; productIds?: string[]; orgId: string; @@ -62,9 +62,9 @@ const getProducts = async ({ } if (productId) { - const product = await ProductService.getFullProduct({ - sb, - productId, + const product = await ProductService.getFull({ + db, + idOrInternalId: productId, orgId, env, version, @@ -92,8 +92,8 @@ const getProducts = async ({ }); } - const products = await ProductService.getFullProducts({ - sb, + const products = await ProductService.listFull({ + db, orgId, env, inIds: productIds, @@ -200,7 +200,14 @@ const getCustomerAndProducts = async ({ entityId, entityData, }), - getProducts({ sb, productId, productIds, orgId: org.id, env, version }), + getProducts({ + db, + productId, + productIds, + orgId: org.id, + env, + version, + }), ]); let cusProducts = customer.customer_products; diff --git a/server/src/internal/customers/products/cusProductUtils.ts b/server/src/internal/customers/products/cusProductUtils.ts index ff5c459f9..189cf41d6 100644 --- a/server/src/internal/customers/products/cusProductUtils.ts +++ b/server/src/internal/customers/products/cusProductUtils.ts @@ -43,6 +43,7 @@ import { addProductsUpdatedWebhookTask, constructProductsUpdatedData, } from "@/external/svix/handleProductsUpdatedWebhook.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; export const isActiveStatus = (status: CusProductStatus) => { return ( @@ -173,16 +174,16 @@ export const cancelCusProductSubscriptions = async ({ }; export const activateDefaultProduct = async ({ + db, productGroup, - orgId, customer, org, sb, env, curCusProduct, }: { + db: DrizzleCli; productGroup: string; - orgId: string; customer: Customer; org: Organization; sb: SupabaseClient; @@ -190,8 +191,8 @@ export const activateDefaultProduct = async ({ curCusProduct?: FullCusProduct; }) => { // 1. Expire current product - const defaultProducts = await ProductService.getFullDefaultProducts({ - sb, + const defaultProducts = await ProductService.listDefault({ + db, orgId: org.id, env, }); @@ -231,11 +232,13 @@ export const activateDefaultProduct = async ({ }; export const expireAndActivate = async ({ + db, sb, env, cusProduct, org, }: { + db: DrizzleCli; sb: SupabaseClient; env: AppEnv; cusProduct: FullCusProduct; @@ -249,8 +252,8 @@ export const expireAndActivate = async ({ }); await activateDefaultProduct({ + db, productGroup: cusProduct.product.group, - orgId: org.id, customer: cusProduct.customer, org, sb, diff --git a/server/src/internal/features/featureUtils.ts b/server/src/internal/features/featureUtils.ts index 421b1b556..bd38650f1 100644 --- a/server/src/internal/features/featureUtils.ts +++ b/server/src/internal/features/featureUtils.ts @@ -97,20 +97,20 @@ export const validateCreditSystem = (config: CreditSystemConfig) => { }; export const getObjectsUsingFeature = async ({ - sb, + db, orgId, env, allFeatures, feature, }: { - sb: any; + db: DrizzleCli; orgId: string; env: any; allFeatures: Feature[]; feature: Feature; }) => { - let products = await ProductService.getFullProducts({ - sb, + let products = await ProductService.listFull({ + db, orgId, env, }); diff --git a/server/src/internal/metadata/metadataUtils.ts b/server/src/internal/metadata/metadataUtils.ts index 86ca8da70..92ed4d4df 100644 --- a/server/src/internal/metadata/metadataUtils.ts +++ b/server/src/internal/metadata/metadataUtils.ts @@ -1,16 +1,4 @@ -import { - AutumnMetadata, - EntitlementWithFeature, - Price, - PricesInput, - FullProduct, - Customer, - Organization, - AppEnv, - FeatureOptions, - CusProduct, - FullCusProduct, -} from "@autumn/shared"; +import { AutumnMetadata } from "@autumn/shared"; import { generateId } from "@/utils/genUtils.js"; import { addDays } from "date-fns"; @@ -49,7 +37,7 @@ export const createCheckoutMetadata = async ({ export const getMetadataFromCheckoutSession = async ( checkoutSession: Stripe.Checkout.Session, - sb: SupabaseClient + sb: SupabaseClient, ) => { const metadataId = checkoutSession.metadata?.autumn_metadata_id; diff --git a/server/src/internal/migrations/runMigrationTask.ts b/server/src/internal/migrations/runMigrationTask.ts index 527eddeac..a35a58bc0 100644 --- a/server/src/internal/migrations/runMigrationTask.ts +++ b/server/src/internal/migrations/runMigrationTask.ts @@ -32,15 +32,15 @@ export const runMigrationTask = async ({ // Get from and to products let [fromProduct, toProduct] = await Promise.all([ - ProductService.getFullProduct({ - sb, - internalId: migrationJob.from_internal_product_id, + ProductService.getFull({ + db, + idOrInternalId: migrationJob.from_internal_product_id, orgId, env, }), - ProductService.getFullProduct({ - sb, - internalId: migrationJob.to_internal_product_id, + ProductService.getFull({ + db, + idOrInternalId: migrationJob.to_internal_product_id, orgId, env, }), diff --git a/server/src/internal/orgs/onboarding/onboardingRouter.ts b/server/src/internal/orgs/onboarding/onboardingRouter.ts index 20e092ec3..b1515c3e7 100644 --- a/server/src/internal/orgs/onboarding/onboardingRouter.ts +++ b/server/src/internal/orgs/onboarding/onboardingRouter.ts @@ -45,8 +45,8 @@ onboardingRouter.post("", async (req: Request, res: any) => }); } - let curProducts = await ProductService.getFullProducts({ - sb, + let curProducts = await ProductService.listFull({ + db, orgId: org.id, env: AppEnv.Sandbox, }); @@ -88,7 +88,7 @@ onboardingRouter.post("", async (req: Request, res: any) => }), (async () => { for (const product of products) { - await ProductService.create({ sb, product }); + await ProductService.insert({ db, product }); } })(), ]); diff --git a/server/src/internal/products/ProductService.ts b/server/src/internal/products/ProductService.ts index 53e6e2e02..e24a37ebc 100644 --- a/server/src/internal/products/ProductService.ts +++ b/server/src/internal/products/ProductService.ts @@ -1,345 +1,253 @@ import RecaseError from "@/utils/errorUtils.js"; -import { AppEnv, ErrCode, FullProduct, Price, Product } from "@autumn/shared"; +import { + AppEnv, + entitlements, + ErrCode, + features, + FreeTrial, + freeTrials, + FullProduct, + prices, + Product, + products, +} from "@autumn/shared"; import { SupabaseClient } from "@supabase/supabase-js"; import { StatusCodes } from "http-status-codes"; import { getLatestProducts, sortProductsByPrice } from "./productUtils.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { and, desc, eq, exists, inArray, or, sql } from "drizzle-orm"; + +const parseFreeTrials = ({ + products, + product, +}: { + products?: FullProduct[]; + product?: FullProduct; +}) => { + if (products) { + for (const prod of products) { + prod.free_trial = + prod.free_trials && prod.free_trials.length > 0 + ? prod.free_trials[0] + : null; + } + } else if (product) { + product!.free_trial = + product!.free_trials && product!.free_trials.length > 0 + ? product!.free_trials[0] + : null; + } + return product; +}; export class ProductService { // GET - static async getById({ - sb, - productId, - orgId, - env, - version, - }: { - sb: SupabaseClient; - productId: string; - orgId: string; - env: AppEnv; - version?: number; - }) { - const query = sb - .from("products") - .select("*") - .eq("id", productId) - .eq("org_id", orgId) - .eq("env", env); - - if (version) { - query.eq("version", version); - } else { - query.order("version", { ascending: false }); - } - - const { data, error } = await query; - - if (error) { - throw error; - } - - if (data.length === 0) { - return null; - } - - return data[0]; - } - static async getByInternalId({ - sb, + db, internalId, - orgId, - env, }: { - sb: SupabaseClient; + db: DrizzleCli; internalId: string; - orgId: string; - env: AppEnv; }) { - const { data, error } = await sb - .from("products") - .select("*") - .eq("internal_id", internalId) - .eq("org_id", orgId) - .eq("env", env) - .single(); - - if (error) { - throw error; - } - return data; + return (await db.query.products.findFirst({ + where: eq(products.internal_id, internalId), + })) as Product; } - static async getFullDefaultProducts({ - sb, + static async listDefault({ + db, orgId, env, }: { - sb: SupabaseClient; + db: DrizzleCli; orgId: string; env: AppEnv; }) { - const { data, error } = await sb - .from("products") - .select( - "*, prices(*), entitlements(*, feature:features(*)), free_trial:free_trials(*)", - ) - .eq("org_id", orgId) - .eq("env", env) - .eq("is_default", true) - .eq("prices.is_custom", false) - .eq("entitlements.is_custom", false) - .eq("free_trial.is_custom", false); + let prods = (await db.query.products.findMany({ + where: and( + eq(products.org_id, orgId), + eq(products.env, env), + eq(products.is_default, true), + ), + with: { + entitlements: { + with: { + feature: true, + }, + where: eq(entitlements.is_custom, false), + }, + prices: { where: eq(prices.is_custom, false) }, + free_trials: { where: eq(freeTrials.is_custom, false) }, + }, + })) as FullProduct[]; - if (error) { - throw error; - } + parseFreeTrials({ products: prods }); - for (const product of data) { - product.free_trial = - product.free_trial.length > 0 ? product.free_trial[0] : null; - } + let latestProducts = getLatestProducts(prods); - // Get latest version of each product - let latestProducts = getLatestProducts(data); - - return latestProducts; + return latestProducts as FullProduct[]; } - static async create({ - sb, - product, - }: { - sb: SupabaseClient; - product: Product; - }) { - const { data, error } = await sb - .from("products") - .insert(product) - .select() - .single(); + static async insert({ db, product }: { db: DrizzleCli; product: Product }) { + let prod = await db.insert(products).values(product).returning(); - if (error) { + if (!prod || prod.length === 0) { throw new RecaseError({ message: "Failed to create product", code: ErrCode.InternalError, statusCode: StatusCodes.INTERNAL_SERVER_ERROR, - data: error, }); } - return data; + return prod[0] as Product; } - static async getProductStrict({ - sb, - productId, + static async get({ + db, + id, orgId, env, version, }: { - sb: SupabaseClient; - productId: string; + db: DrizzleCli; + id: string; orgId: string; env: AppEnv; version?: number; }) { - const query = sb - .from("products") - .select("*") - .eq("id", productId) - .eq("org_id", orgId) - .eq("env", env); + let data = await db.query.products.findMany({ + where: and( + eq(products.id, id), + eq(products.org_id, orgId), + eq(products.env, env), + version ? eq(products.version, version) : undefined, + ), + orderBy: [desc(products.version)], + }); - if (version) { - query.eq("version", version); - } else { - query.order("version", { ascending: false }); - } - - const { data, error } = await query; - - if (error) { - throw error; - } - - if (data.length === 0) { + if (!data || data.length === 0) { return null; - // throw new RecaseError({ - // message: `Product ${productId}${version ? ` (v${version})` : ""} not found`, - // code: ErrCode.ProductNotFound, - // statusCode: StatusCodes.NOT_FOUND, - // }); } return data[0]; } - static async getFullProducts({ - sb, + static async listFull({ + db, orgId, env, inIds, returnAll = false, }: { - sb: SupabaseClient; + db: DrizzleCli; orgId: string; env: AppEnv; inIds?: string[]; returnAll?: boolean; }) { - const query = sb - .from("products") - .select( - `*, - entitlements ( - *, - feature:features (*) - ), - prices(*), - free_trial:free_trials(*) - `, - ) - .eq("org_id", orgId) - .eq("env", env) - .eq("prices.is_custom", false) - .eq("entitlements.is_custom", false) - .eq("free_trial.is_custom", false) - .order("created_at", { ascending: false }) - .order("id"); + let data = (await db.query.products.findMany({ + where: and( + eq(products.org_id, orgId), + eq(products.env, env), + inIds ? inArray(products.id, inIds) : undefined, + ), + with: { + entitlements: { + with: { + feature: true, + }, + where: eq(entitlements.is_custom, false), + }, + prices: { where: eq(prices.is_custom, false) }, + free_trials: { where: eq(freeTrials.is_custom, false) }, + }, + orderBy: [desc(products.internal_id)], + })) as FullProduct[]; - if (inIds) { - query.in("id", inIds); - } - - const { data, error } = await query; - - if (error) { - throw error; - } - - for (const product of data) { - product.free_trial = - product.free_trial.length > 0 ? product.free_trial[0] : null; - } + parseFreeTrials({ products: data }); if (returnAll) { - return data as FullProduct[]; + return data; } - // Get latest of each version - const versionCounts = data.reduce((acc: any, product: any) => { - if (!acc[product.id]) { - acc[product.id] = 1; - } else { - acc[product.id]++; - } - return acc; - }, {}); - const latestProducts = data.reduce((acc: any, product: any) => { - if (!acc[product.id]) { - acc[product.id] = product; - } else if (product.version > acc[product.id].version) { - acc[product.id] = product; - } - return acc; - }, {}); + const latestProducts = getLatestProducts(data); - return Object.values(latestProducts) as FullProduct[]; + return latestProducts as FullProduct[]; } - static async getFullProduct({ - sb, - productId, - internalId, + static async getFull({ + db, + idOrInternalId, orgId, env, version, }: { - sb: SupabaseClient; - productId?: string; - internalId?: string; + db: DrizzleCli; + idOrInternalId: string; orgId: string; env: AppEnv; version?: number; }) { - const query = sb.from("products").select( - ` *, - free_trial:free_trials(*), - entitlements ( - *, - feature:features (id, name, type) + let data = (await db.query.products.findFirst({ + where: and( + or( + eq(products.id, idOrInternalId), + eq(products.internal_id, idOrInternalId), ), - prices (*) - `, - ); + eq(products.org_id, orgId), + eq(products.env, env), + version ? eq(products.version, version) : undefined, + ), + orderBy: [desc(products.version)], + with: { + entitlements: { + with: { + feature: true, + }, + where: eq(entitlements.is_custom, false), + }, + prices: { where: eq(prices.is_custom, false) }, + free_trials: { where: eq(freeTrials.is_custom, false) }, + }, + })) as FullProduct; - if (productId) { - query.eq("id", productId); - } else if (internalId) { - query.eq("internal_id", internalId); - } + parseFreeTrials({ product: data }); - query - .eq("org_id", orgId) - .eq("env", env) - .eq("prices.is_custom", false) - .eq("entitlements.is_custom", false) - .eq("free_trial.is_custom", false); - - if (version && productId) { - query.eq("version", version); - } else { - query.order("version", { ascending: false }).limit(1); - } - - const { data, error } = await query; - - if (error) { - throw error; - } - - if (data.length === 0) { - // Throw error? + if (!data) { + // return null; throw new RecaseError({ - message: `Product ${productId}${ - version ? ` (v${version})` : "" - } not found`, + message: `Product ${idOrInternalId} not found`, code: ErrCode.ProductNotFound, statusCode: StatusCodes.NOT_FOUND, }); } - let product = data[0]; - - product.free_trial = - product.free_trial.length > 0 ? product.free_trial[0] : null; - return product; + return data as FullProduct; } static async getProductVersionCount({ - sb, + db, productId, orgId, env, }: { - sb: SupabaseClient; + db: DrizzleCli; productId: string; orgId: string; env: AppEnv; }) { - const { data, error } = await sb - .from("products") - .select("version") - .eq("id", productId) - .eq("org_id", orgId) - .eq("env", env) - .order("version", { ascending: false }) - .limit(1); - - if (error) { - throw error; - } + const data = await db.query.products.findMany({ + columns: { + version: true, + }, + limit: 1, + where: and( + eq(products.id, productId), + eq(products.org_id, orgId), + eq(products.env, env), + ), + orderBy: [desc(products.version)], + }); if (data.length === 0) { throw new RecaseError({ @@ -352,109 +260,81 @@ export class ProductService { return data[0].version; } - static async getEntitlementsByProductId({ - sb, - internalProductId, - orgId, - env, - }: { - sb: SupabaseClient; - internalProductId: string; - orgId: string; - env: AppEnv; - }) { - const { data, error } = await sb - .from("entitlements") - .select("*, feature:features(id, name, type)") - .eq("internal_product_id", internalProductId) - .eq("org_id", orgId) - .eq("env", env); - - if (error) { - throw error; - } - - return data; - } - // UPDATES - static async update({ - sb, + static async updateByInternalId({ + db, internalId, update, }: { - sb: SupabaseClient; + db: DrizzleCli; internalId: string; update: any; }) { - const { data, error } = await sb - .from("products") - .update(update) - .eq("internal_id", internalId); - - if (error) { - throw new RecaseError({ - message: `Error updating product...please try again later.`, - code: ErrCode.InternalError, - statusCode: StatusCodes.INTERNAL_SERVER_ERROR, - data: error, - }); - } + const data = await db + .update(products) + .set(update) + .where(eq(products.internal_id, internalId)); } // DELETES static async deleteByInternalId({ - sb, + db, internalId, orgId, env, }: { - sb: SupabaseClient; + db: DrizzleCli; internalId: string; orgId: string; env: AppEnv; }) { - const { data, error } = await sb - .from("products") - .delete() - .eq("internal_id", internalId) - .eq("org_id", orgId) - .eq("env", env) - .single(); - - if (error) { - throw error; - } - - return data; + await db + .delete(products) + .where( + and( + eq(products.internal_id, internalId), + eq(products.org_id, orgId), + eq(products.env, env), + ), + ); } static async getByFeature({ - sb, + db, internalFeatureId, }: { - sb: SupabaseClient; + db: DrizzleCli; internalFeatureId: string; }) { - const { data, error } = await sb.rpc("get_products_by_feature", { - p_internal_feature_id: internalFeatureId, - }); + let fullProducts = (await db.query.products.findMany({ + where: exists( + db + .select() + .from(entitlements) + .where( + and( + eq(entitlements.internal_product_id, products.internal_id), + eq(entitlements.internal_feature_id, internalFeatureId), + ), + ), + ), + with: { + entitlements: { + with: { + feature: true, + }, + }, + prices: { where: eq(prices.is_custom, false) }, + free_trials: { where: eq(freeTrials.is_custom, false) }, + }, + orderBy: [desc(products.version)], + })) as FullProduct[]; - if (error) { - throw error; - } + parseFreeTrials({ products: fullProducts }); - if (!data) { - console.log( - "ProductService.getByFeature returning no data, error:", - error, - ); - return []; - } - // Sort products by pricing - sortProductsByPrice(data); + let latestProducts = getLatestProducts(fullProducts); - return data; + return latestProducts; } } diff --git a/server/src/internal/products/internalProductRouter.ts b/server/src/internal/products/internalProductRouter.ts index 124669eed..f97013cde 100644 --- a/server/src/internal/products/internalProductRouter.ts +++ b/server/src/internal/products/internalProductRouter.ts @@ -18,34 +18,14 @@ import RecaseError, { handleFrontendReqError } from "@/utils/errorUtils.js"; export const productRouter = Router({ mergeParams: true }); -productRouter.get("/", async (req: any, res) => { - try { - let sb = req.sb; - let products = await ProductService.getFullProducts({ - sb, - orgId: req.orgId, - env: req.env, - }); - - res.status(200).send(products); - } catch (error) { - handleFrontendReqError({ - error, - req, - res, - action: "Get products (internal)", - }); - } -}); - productRouter.get("/data", async (req: any, res) => { try { - let { sb } = req; + let { db, sb } = req; const [products, features, org, coupons, rewardPrograms] = await Promise.all([ - ProductService.getFullProducts({ - sb, + ProductService.listFull({ + db, orgId: req.orgId, env: req.env, returnAll: true, @@ -82,9 +62,9 @@ productRouter.get("/data", async (req: any, res) => { productRouter.get("/counts", async (req: any, res) => { try { - let { sb, orgId, env } = req; - let products = await ProductService.getFullProducts({ - sb, + let { db, sb } = req; + let products = await ProductService.listFull({ + db, orgId: req.orgId, env: req.env, returnAll: true, @@ -99,12 +79,6 @@ productRouter.get("/counts", async (req: any, res) => { }), ); - // let result: { [key: string]: any } = {}; - - // for (let i = 0; i < products.length; i++) { - // result[products[i].internal_id] = counts[i]; - // } - // Group by ID let result: { [key: string]: any } = {}; for (let i = 0; i < products.length; i++) { if (!result[products[i].id]) { @@ -130,15 +104,13 @@ productRouter.get("/:productId/data", async (req: any, res) => { try { const { productId } = req.params; const { version } = req.query; - const sb = req.sb; - const orgId = req.orgId; - const env = req.env; + const { sb, db, orgId, env } = req; const [product, features, org, numVersions, existingMigrations] = await Promise.all([ - ProductService.getFullProduct({ - sb, - productId, + ProductService.getFull({ + db, + idOrInternalId: productId, orgId, env, version: version ? parseInt(version) : undefined, @@ -146,7 +118,7 @@ productRouter.get("/:productId/data", async (req: any, res) => { FeatureService.getFromReq(req), OrgService.getFromReq(req), ProductService.getProductVersionCount({ - sb, + db, productId, orgId, env, @@ -207,14 +179,15 @@ productRouter.get("/:productId/data", async (req: any, res) => { productRouter.get("/:productId/count", async (req: any, res) => { try { + const { db, sb, orgId, env } = req; const { productId } = req.params; const { version } = req.query; - const product = await ProductService.getProductStrict({ - sb: req.sb, - productId, - orgId: req.orgId, - env: req.env, + const product = await ProductService.get({ + db, + id: productId, + orgId, + env, version: version ? parseInt(version) : undefined, }); diff --git a/server/src/internal/products/prices/priceUtils.ts b/server/src/internal/products/prices/priceUtils.ts index b31b875da..e892c03b1 100644 --- a/server/src/internal/products/prices/priceUtils.ts +++ b/server/src/internal/products/prices/priceUtils.ts @@ -42,13 +42,21 @@ export const constructPrice = ({ fixedConfig?: FixedPriceConfig; usageConfig?: UsagePriceConfig; }) => { + if (!usageConfig && !fixedConfig) { + throw new RecaseError({ + message: "Usage config or fixed config must be provided", + code: ErrCode.InvalidRequest, + statusCode: StatusCodes.BAD_REQUEST, + }); + } + let newPrice: Price = { id: generateId("pr"), org_id: orgId, internal_product_id: internalProductId, created_at: Date.now(), is_custom: isCustom, - config: usageConfig || fixedConfig, + config: (usageConfig || fixedConfig)!, entitlement_id: entitlementId, }; diff --git a/server/src/internal/products/productUtils.ts b/server/src/internal/products/productUtils.ts index d72561f10..8ef160ab3 100644 --- a/server/src/internal/products/productUtils.ts +++ b/server/src/internal/products/productUtils.ts @@ -176,15 +176,6 @@ export const isProductUpgrade = ({ } }; -export const isSameBillingInterval = ( - product1: FullProduct, - product2: FullProduct, -) => { - return ( - getBillingInterval(product1.prices) === getBillingInterval(product2.prices) - ); -}; - export const isFreeProduct = (prices: Price[]) => { if (prices.length === 0) { return true; @@ -239,13 +230,13 @@ export const getOptionsFromPrices = (prices: Price[], features: Feature[]) => { }; export const checkStripeProductExists = async ({ - sb, + db, org, env, product, logger, }: { - sb: SupabaseClient; + db: DrizzleCli; org: Organization; env: AppEnv; product: FullProduct; @@ -278,8 +269,8 @@ export const checkStripeProductExists = async ({ name: product.name, }); - await ProductService.update({ - sb, + await ProductService.updateByInternalId({ + db, internalId: product.internal_id, update: { processor: { id: stripeProduct.id, type: ProcessorType.Stripe }, @@ -431,8 +422,8 @@ export const copyProduct = async ({ ); } - await ProductService.create({ - sb, + await ProductService.insert({ + db, product: { ...ProductSchema.parse(newProduct), version: 1, @@ -479,14 +470,12 @@ export const isOneOff = (prices: Price[]) => { export const initProductInStripe = async ({ db, - sb, org, env, logger, product, }: { db: DrizzleCli; - sb: SupabaseClient; org: Organization; env: AppEnv; logger: any; @@ -494,7 +483,7 @@ export const initProductInStripe = async ({ }) => { // 1. await checkStripeProductExists({ - sb, + db, org, env, product, diff --git a/server/src/internal/rewards/referralUtils.ts b/server/src/internal/rewards/referralUtils.ts index 49899bc39..3798d9275 100644 --- a/server/src/internal/rewards/referralUtils.ts +++ b/server/src/internal/rewards/referralUtils.ts @@ -19,6 +19,7 @@ import { CusProductService } from "../customers/products/CusProductService.js"; import { ProductService } from "../products/ProductService.js"; import { createFullCusProduct } from "../customers/add-product/createFullCusProduct.js"; import { InsertCusProductParams } from "../customers/products/AttachParams.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; export const generateReferralCode = () => { const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; @@ -52,7 +53,7 @@ export const triggerRedemption = async ({ redemption: RewardRedemption; }) => { logger.info( - `Triggering redemption ${redemption.id} for referral code ${referralCode.code}` + `Triggering redemption ${redemption.id} for referral code ${referralCode.code}`, ); let applyToCustomer = await CusService.getByInternalId({ @@ -75,7 +76,7 @@ export const triggerRedemption = async ({ let stripeCusId = applyToCustomer.processor.id; let stripeCus = (await stripeCli.customers.retrieve( - stripeCusId + stripeCusId, )) as Stripe.Customer; let applied = false; @@ -104,6 +105,7 @@ export const triggerRedemption = async ({ export const triggerFreeProduct = async ({ sb, + db, referralCode, redeemer, redemption, @@ -113,6 +115,7 @@ export const triggerFreeProduct = async ({ logger, }: { sb: any; + db: DrizzleCli; referralCode: ReferralCode; redeemer: Customer; redemption: RewardRedemption; @@ -122,7 +125,7 @@ export const triggerFreeProduct = async ({ logger: any; }) => { logger.info(`Triggering free product reward`); - let { product_ids, received_by } = rewardProgram; + let { received_by } = rewardProgram; let addToRedeemer = received_by === RewardReceivedBy.All; let addToReferrer = @@ -130,9 +133,9 @@ export const triggerFreeProduct = async ({ received_by === RewardReceivedBy.All; let productId = rewardProgram.reward.free_product_id!; - let fullProduct = await ProductService.getFullProduct({ - sb, - productId, + let fullProduct = await ProductService.getFull({ + db, + idOrInternalId: productId, orgId: org.id, env, }); diff --git a/server/src/internal/rewards/triggerCheckoutReward.ts b/server/src/internal/rewards/triggerCheckoutReward.ts index d6b86d0e6..3478d559e 100644 --- a/server/src/internal/rewards/triggerCheckoutReward.ts +++ b/server/src/internal/rewards/triggerCheckoutReward.ts @@ -4,11 +4,14 @@ import { triggerFreeProduct, triggerRedemption } from "./referralUtils.js"; import { RewardProgramService } from "../rewards/RewardProgramService.js"; import { getRewardCat } from "./rewardUtils.js"; import { createStripeCli } from "@/external/stripe/utils.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; export const runTriggerCheckoutReward = async ({ + db, sb, payload, logger, }: { + db: DrizzleCli; sb: any; payload: any; logger: any; @@ -46,13 +49,13 @@ export const runTriggerCheckoutReward = async ({ logger.info(`--------------------------------`); logger.info(`CHECKING FOR CHECKOUT REWARD, ORG: ${org.slug}`); logger.info( - `Redeemed by: ${customer.name} (${customer.id}) for referral program: ${reward_program.id}` + `Redeemed by: ${customer.name} (${customer.id}) for referral program: ${reward_program.id}`, ); logger.info(`Referral code: ${referralCode.code} (${referralCode.id})`); if (!reward_program.product_ids.includes(product.id)) { logger.info( - `Product ${product.name} (${product.id}) not included in referral program, skipping` + `Product ${product.name} (${product.id}) not included in referral program, skipping`, ); return; } @@ -78,7 +81,7 @@ export const runTriggerCheckoutReward = async ({ if (redemptionCount >= reward_program.max_redemptions) { logger.info( - `Max redemptions reached, not triggering latest redemption` + `Max redemptions reached, not triggering latest redemption`, ); return; } @@ -86,6 +89,7 @@ export const runTriggerCheckoutReward = async ({ let rewardCat = getRewardCat(reward); if (rewardCat === RewardCategory.FreeProduct) { await triggerFreeProduct({ + db, sb, referralCode, redeemer: customer, diff --git a/server/src/middleware/analyticsMiddleware.ts b/server/src/middleware/analyticsMiddleware.ts index 6828f79b0..c02e9dd63 100644 --- a/server/src/middleware/analyticsMiddleware.ts +++ b/server/src/middleware/analyticsMiddleware.ts @@ -33,6 +33,7 @@ const handleResFinish = (req: any, res: any, logtailContext: any) => { distinctId: req.org?.id, event: `${req.method} ${req.originalUrl}`, properties: { + authType: req.auth, orgSlug: req.org?.slug, statusCode: res.statusCode, res: res.locals.responseBody, diff --git a/server/src/middleware/apiMiddleware.ts b/server/src/middleware/apiMiddleware.ts index 84feb2899..24766c889 100644 --- a/server/src/middleware/apiMiddleware.ts +++ b/server/src/middleware/apiMiddleware.ts @@ -1,7 +1,7 @@ import { withOrgAuth } from "./authMiddleware.js"; import { verifyKey } from "@/internal/dev/api-keys/apiKeyUtils.js"; import { verifyBearerPublishableKey } from "./publicAuthMiddleware.js"; -import { ErrCode } from "@autumn/shared"; +import { AuthType, ErrCode } from "@autumn/shared"; import { floatToVersion } from "@/utils/versionUtils.js"; export const verifySecretKey = async (req: any, res: any, next: any) => { @@ -72,6 +72,7 @@ export const verifySecretKey = async (req: any, res: any, next: any) => { }; req.org = org; req.features = features; + req.auth = AuthType.SecretKey; next(); diff --git a/server/src/middleware/authMiddleware.ts b/server/src/middleware/authMiddleware.ts index b5f9ba30b..f1d0aed3d 100644 --- a/server/src/middleware/authMiddleware.ts +++ b/server/src/middleware/authMiddleware.ts @@ -1,4 +1,5 @@ import { OrgService } from "@/internal/orgs/OrgService.js"; +import { AuthType } from "@autumn/shared"; import { verifyToken } from "@clerk/express"; import { NextFunction } from "express"; @@ -56,13 +57,14 @@ export const withOrgAuth = async (req: any, res: any, next: NextFunction) => { req.user = tokenData!.user; req.org = org; req.features = features; + req.auth = AuthType.Frontend; next(); } catch (error: any) { console.log( // `withOrgAuth error (${req.headers["authorization"]}):`, `(warning) clerk auth failed:`, - error?.message || error + error?.message || error, ); res.status(401).json({ message: "Unauthorized" }); return; diff --git a/server/src/middleware/publicAuthMiddleware.ts b/server/src/middleware/publicAuthMiddleware.ts index c4f63f8f6..9c9365e8b 100644 --- a/server/src/middleware/publicAuthMiddleware.ts +++ b/server/src/middleware/publicAuthMiddleware.ts @@ -1,5 +1,5 @@ import { verifyPublicKey } from "@/internal/dev/api-keys/publicKeyUtils.js"; -import { AppEnv, ErrCode } from "@autumn/shared"; +import { AppEnv, AuthType, ErrCode } from "@autumn/shared"; const allowedEndpoints = [ { @@ -55,7 +55,7 @@ export const verifyBearerPublishableKey = async ( pkey: string, req: any, res: any, - next: any + next: any, ) => { try { if ( @@ -109,6 +109,7 @@ export const verifyBearerPublishableKey = async ( req.isPublic = true; req.org = org; req.features = features; + req.auth = AuthType.SecretKey; console.log("Public request from:", org.slug); next(); diff --git a/server/src/queue/queue.ts b/server/src/queue/queue.ts index 33bd62c94..dc7e22720 100644 --- a/server/src/queue/queue.ts +++ b/server/src/queue/queue.ts @@ -147,6 +147,7 @@ const initWorker = ({ try { await runTriggerCheckoutReward({ + db, payload: job.data, sb, logger: logtail, @@ -181,9 +182,15 @@ const initWorker = ({ payload: job.data, logger: logtail, sb, + db, }); } else if (job.name === JobName.UpdateUsage) { - await runUpdateUsageTask({ payload: job.data, logger: logtail, sb }); + await runUpdateUsageTask({ + payload: job.data, + logger: logtail, + sb, + db, + }); } } catch (error) { console.error("Error processing job:", error); diff --git a/server/src/trigger/adjustAllowance.ts b/server/src/trigger/adjustAllowance.ts index 2bdb76add..71589d011 100644 --- a/server/src/trigger/adjustAllowance.ts +++ b/server/src/trigger/adjustAllowance.ts @@ -40,12 +40,14 @@ import { InvoiceService } from "@/internal/customers/invoices/InvoiceService.js" import RecaseError from "@/utils/errorUtils.js"; import { formatUnixToDateTime } from "@/utils/genUtils.js"; import { getInvoiceItems } from "@/internal/customers/invoices/invoiceUtils.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; type CusEntWithCusProduct = FullCustomerEntitlement & { customer_product: CusProduct; }; export const adjustAllowance = async ({ + db, sb, env, org, @@ -60,6 +62,7 @@ export const adjustAllowance = async ({ replacedCount, fromEntities = false, }: { + db: DrizzleCli; sb: SupabaseClient; env: AppEnv; affectedFeature: Feature; @@ -189,10 +192,8 @@ export const adjustAllowance = async ({ if (!product) { product = await ProductService.getByInternalId({ - sb, + db, internalId: cusProduct.internal_product_id, - orgId: org.id, - env, }); } diff --git a/server/src/trigger/updateBalanceTask.ts b/server/src/trigger/updateBalanceTask.ts index d0dfe95c3..7d23818ce 100644 --- a/server/src/trigger/updateBalanceTask.ts +++ b/server/src/trigger/updateBalanceTask.ts @@ -1,11 +1,9 @@ import { AllowanceType, AppEnv, - CusEntWithEntitlement, CusProductStatus, Event, Feature, - FullCustomer, FullCustomerEntitlement, FullCustomerPrice, Organization, @@ -33,10 +31,12 @@ import { } from "@/internal/customers/entitlements/cusEntUtils.js"; import { entityFeatureIdExists } from "@/internal/api/entities/entityUtils.js"; import { CusService } from "@/internal/customers/CusService.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; // Decimal.set({ precision: 12 }); // 12 DP precision type DeductParams = { + db: DrizzleCli; sb: SupabaseClient; env: AppEnv; org: Organization; @@ -57,7 +57,7 @@ const getFeatureDeductions = ({ features: Feature[]; }) => { const meteredFeatures = features.filter( - (feature) => feature.type === FeatureType.Metered + (feature) => feature.type === FeatureType.Metered, ); const featureDeductions = []; for (const feature of features) { @@ -76,7 +76,7 @@ const getFeatureDeductions = ({ let unlimitedExists = cusEnts.some( (cusEnt) => cusEnt.entitlement.allowance_type === AllowanceType.Unlimited && - cusEnt.entitlement.internal_feature_id == feature.internal_id + cusEnt.entitlement.internal_feature_id == feature.internal_id, ); if (unlimitedExists || !deduction) { @@ -132,7 +132,7 @@ export const logBalanceUpdate = ({ console.log( ` - Customer: ${customer.id} (${customer.env}) | Org: ${ org.slug - } | Features: ${features.map((f) => f.id).join(", ")}` + } | Features: ${features.map((f) => f.id).join(", ")}`, ); console.log(" - Properties:", properties); console.log( @@ -142,7 +142,7 @@ export const logBalanceUpdate = ({ if (notNullish(cusEnt.entitlement.entity_feature_id)) { console.log( - ` - Entity feature ID found for feature: ${cusEnt.feature_id}` + ` - Entity feature ID found for feature: ${cusEnt.feature_id}`, ); if (notNullish(entityId)) { @@ -169,7 +169,7 @@ export const logBalanceUpdate = ({ })`; }), "| Deductions:", - featureDeductions.map((f: any) => `${f.feature.id}: ${f.deduction}`) + featureDeductions.map((f: any) => `${f.feature.id}: ${f.deduction}`), ); }; @@ -308,7 +308,7 @@ export const deductAllowanceFromCusEnt = async ({ entityId?: string | null; setZeroAdjustment?: boolean; }) => { - const { sb, feature, env, org, cusPrices, customer } = deductParams; + const { db, sb, feature, env, org, cusPrices, customer } = deductParams; if (toDeduct == 0) { return 0; @@ -354,6 +354,7 @@ export const deductAllowanceFromCusEnt = async ({ }); await adjustAllowance({ + db, sb, env, org, @@ -411,42 +412,22 @@ export const deductFromUsageBasedCusEnt = async ({ entityId?: string | null; setZeroAdjustment?: boolean; }) => { - const { sb, feature, env, org, cusPrices, customer, properties } = - deductParams; + const { db, sb, feature, env, org, cusPrices, customer } = deductParams; // Deduct from usage-based price const usageBasedEnt = cusEnts.find( - (cusEnt: CusEntWithEntitlement) => + (cusEnt: FullCustomerEntitlement) => cusEnt.usage_allowed && - cusEnt.entitlement.internal_feature_id == feature.internal_id + cusEnt.entitlement.internal_feature_id == feature.internal_id, ); if (!usageBasedEnt) { console.log( - ` - Feature ${feature.id}, To deduct: ${toDeduct} -> no usage-based entitlement found` + ` - Feature ${feature.id}, To deduct: ${toDeduct} -> no usage-based entitlement found`, ); return; } - // // Group by value - // let { groupVal, balance } = getGroupBalanceFromProperties({ - // properties, - // feature, - // cusEnt: usageBasedEnt, - // }); - - // if (groupVal && nullOrUndefined(balance)) { - // console.log( - // ` - Feature ${feature.id}, To deduct: ${toDeduct} -> no group balance found` - // ); - // return; - // } - - // let usageBasedEntBalance = new Decimal(balance!); - // let newBalance = usageBasedEntBalance.minus(toDeduct).toNumber(); - // console.log("DEDUCTING USAGE-BASED ENTITLEMENT"); - // console.log("OLD BALANCE", usageBasedEnt.balance); - // console.log("TO DEDUCT", toDeduct); let { newBalance, newEntities, deducted } = performDeductionOnCusEnt({ cusEnt: usageBasedEnt, toDeduct, @@ -455,9 +436,6 @@ export const deductFromUsageBasedCusEnt = async ({ setZeroAdjustment, }); - // console.log("NEW BALANCE", newBalance); - // console.log("DEDUCTED", deducted); - let oldGrpBalance = getTotalNegativeBalance({ cusEnt: usageBasedEnt, balance: usageBasedEnt.balance!, @@ -491,6 +469,7 @@ export const deductFromUsageBasedCusEnt = async ({ // .toNumber(); await adjustAllowance({ + db, sb, env, affectedFeature: feature, @@ -506,6 +485,7 @@ export const deductFromUsageBasedCusEnt = async ({ // Main function to update customer balance export const updateCustomerBalance = async ({ + db, sb, customerId, entityId, @@ -515,6 +495,7 @@ export const updateCustomerBalance = async ({ env, logger, }: { + db: DrizzleCli; sb: SupabaseClient; customerId: string; entityId: string; @@ -583,6 +564,7 @@ export const updateCustomerBalance = async ({ cusEnt, features, deductParams: { + db, sb, feature, env, @@ -605,6 +587,7 @@ export const updateCustomerBalance = async ({ toDeduct, cusEnts, deductParams: { + db, sb, feature, env, @@ -624,10 +607,12 @@ export const updateCustomerBalance = async ({ export const runUpdateBalanceTask = async ({ payload, logger, + db, sb, }: { payload: any; logger: any; + db: DrizzleCli; sb: SupabaseClient; }) => { try { @@ -636,10 +621,11 @@ export const runUpdateBalanceTask = async ({ console.log("--------------------------------"); console.log( - `UPDATING BALANCE FOR CUSTOMER (${customerId}), ORG: ${org.slug}` + `UPDATING BALANCE FOR CUSTOMER (${customerId}), ORG: ${org.slug}`, ); const cusEnts: any = await updateCustomerBalance({ + db, sb, customerId, features, diff --git a/server/src/trigger/updateUsageTask.ts b/server/src/trigger/updateUsageTask.ts index 417cdfe86..5c0366689 100644 --- a/server/src/trigger/updateUsageTask.ts +++ b/server/src/trigger/updateUsageTask.ts @@ -1,8 +1,3 @@ -import { createSupabaseClient } from "@/external/supabaseUtils.js"; -import { - getBelowThresholdPrice, - handleBelowThresholdInvoicing, -} from "./invoiceThresholdUtils.js"; import { AllowanceType, AppEnv, @@ -10,7 +5,6 @@ import { Customer, Feature, FeatureType, - FullCustomer, FullCustomerEntitlement, Organization, } from "@autumn/shared"; @@ -20,16 +14,14 @@ import { getCusEntsInFeatures } from "@/internal/api/customers/cusUtils.js"; import { featureToCreditSystem } from "@/internal/features/creditSystemUtils.js"; import { getFeatureBalance } from "@/internal/customers/entitlements/cusEntUtils.js"; import { Decimal } from "decimal.js"; -import { - getGroupBalanceFromProperties, - initGroupBalancesForEvent, -} from "@/internal/customers/entitlements/groupByUtils.js"; + import { deductAllowanceFromCusEnt, deductFromUsageBasedCusEnt, } from "./updateBalanceTask.js"; import { JobName } from "@/queue/JobName.js"; import { CusService } from "@/internal/customers/CusService.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; // 2. Get deductions for each feature const getFeatureDeductions = ({ @@ -52,7 +44,7 @@ const getFeatureDeductions = ({ let unlimitedExists = cusEnts.some( (cusEnt) => cusEnt.entitlement.allowance_type === AllowanceType.Unlimited && - cusEnt.entitlement.internal_feature_id == feature.internal_id + cusEnt.entitlement.internal_feature_id == feature.internal_id, ); if (unlimitedExists) { @@ -139,7 +131,7 @@ const logUsageUpdate = ({ org.slug } | Features: ${features.map((f) => f.id).join(", ")} | Set Usage: ${ setUsage ? "true" : "false" - }` + }`, ); console.log( @@ -163,12 +155,13 @@ const logUsageUpdate = ({ })`; }), "| Deductions:", - featureDeductions.map((f: any) => `${f.feature.id}: ${f.deduction}`) + featureDeductions.map((f: any) => `${f.feature.id}: ${f.deduction}`), ); }; // Main function to update customer balance export const updateUsage = async ({ + db, sb, customerId, features, @@ -180,6 +173,7 @@ export const updateUsage = async ({ logger, entityId, }: { + db: DrizzleCli; sb: SupabaseClient; customerId: string; features: Feature[]; @@ -245,6 +239,7 @@ export const updateUsage = async ({ cusEnt, features, deductParams: { + db, sb, feature, env, @@ -268,6 +263,7 @@ export const updateUsage = async ({ toDeduct, cusEnts, deductParams: { + db, sb, feature, env, @@ -288,10 +284,12 @@ export const updateUsage = async ({ export const runUpdateUsageTask = async ({ payload, logger, + db, sb, }: { payload: any; logger: any; + db: DrizzleCli; sb: SupabaseClient; }) => { try { @@ -310,10 +308,11 @@ export const runUpdateUsageTask = async ({ console.log("--------------------------------"); console.log( - `HANDLING USAGE TASK FOR CUSTOMER (${customerId}), ORG: ${org.slug}` + `HANDLING USAGE TASK FOR CUSTOMER (${customerId}), ORG: ${org.slug}`, ); const cusEnts: any = await updateUsage({ + db, sb, customerId, features, diff --git a/server/tests/advanced/arrear_prorated/arrear_prorated2.ts b/server/tests/advanced/arrear_prorated/arrear_prorated2.ts index 1d44b5575..848f79847 100644 --- a/server/tests/advanced/arrear_prorated/arrear_prorated2.ts +++ b/server/tests/advanced/arrear_prorated/arrear_prorated2.ts @@ -172,7 +172,7 @@ const advanceAPThroughBalances = async ({ }; describe(`${chalk.yellowBright( - "arrear_prorated2: testing update in arrear prorated through /balances" + "arrear_prorated2: testing update in arrear prorated through /balances", )}`, () => { const customerId = "arrear-prorated-balances"; @@ -190,7 +190,7 @@ describe(`${chalk.yellowBright( org: this.org, env: this.env, sb: this.sb, - } + }, ); stripeCli = createStripeCli({ @@ -238,6 +238,7 @@ describe(`${chalk.yellowBright( stripeSub = await stripeCli.subscriptions.retrieve(subId); await checkSubscriptionContainsProducts({ + db: this.db, sb: this.sb, org: this.org, env: this.env, diff --git a/server/tests/advanced/arrear_prorated/arrear_prorated3.ts b/server/tests/advanced/arrear_prorated/arrear_prorated3.ts index 7548d9f6a..1560fd827 100644 --- a/server/tests/advanced/arrear_prorated/arrear_prorated3.ts +++ b/server/tests/advanced/arrear_prorated/arrear_prorated3.ts @@ -161,17 +161,17 @@ const advanceAPThroughBalances = async ({ let nextMonthUsagePrice = Math.max(-balance * pricePerSeat, 0); let expectedInvoiceTotal = Number( - (accruedPrice + basePrice + nextMonthUsagePrice).toFixed(2) + (accruedPrice + basePrice + nextMonthUsagePrice).toFixed(2), ); console.log( - `Invoice total = ${accruedPrice} (Accrued) + ${nextMonthUsagePrice} (Next month usage) + ${basePrice} (Base) = ${expectedInvoiceTotal}` + `Invoice total = ${accruedPrice} (Accrued) + ${nextMonthUsagePrice} (Next month usage) + ${basePrice} (Base) = ${expectedInvoiceTotal}`, ); expect(expectedInvoiceTotal).to.lte( - new Decimal(invoice.total).plus(0.01).toNumber() + new Decimal(invoice.total).plus(0.01).toNumber(), ); expect(expectedInvoiceTotal).to.gte( - new Decimal(invoice.total).minus(0.01).toNumber() + new Decimal(invoice.total).minus(0.01).toNumber(), ); return { @@ -181,7 +181,7 @@ const advanceAPThroughBalances = async ({ }; describe(`${chalk.yellowBright( - "arrear_prorated3: Testing through /usage" + "arrear_prorated3: Testing through /usage", )}`, () => { const customerId = "arrear_prorated3"; @@ -199,7 +199,7 @@ describe(`${chalk.yellowBright( org: this.org, env: this.env, sb: this.sb, - } + }, ); stripeCli = createStripeCli({ @@ -229,6 +229,7 @@ describe(`${chalk.yellowBright( stripeSub = await stripeCli.subscriptions.retrieve(subId); await checkSubscriptionContainsProducts({ + db: this.db, sb: this.sb, org: this.org, env: this.env, diff --git a/server/tests/advanced/usage/usage3.ts b/server/tests/advanced/usage/usage3.ts index ead4ff270..6b0e2a23b 100644 --- a/server/tests/advanced/usage/usage3.ts +++ b/server/tests/advanced/usage/usage3.ts @@ -102,6 +102,7 @@ describe(`${chalk.yellowBright( let subscriptionId = res.products[0].subscription_ids![0]!; checkSubscriptionContainsProducts({ + db: this.db, sb: this.sb, org: this.org, env: this.env, diff --git a/server/tests/attach/01_multi_product3.ts b/server/tests/attach/01_multi_product3.ts index 343ae60c2..05dec2738 100644 --- a/server/tests/attach/01_multi_product3.ts +++ b/server/tests/attach/01_multi_product3.ts @@ -105,11 +105,12 @@ describe("Multi Product 3: premium1->starter1, premium2->starter2, then premium2 expect(starter1?.scheduled_ids![0]).to.equal(starter2?.scheduled_ids![0]); const stripeSchedule = await stripeCli.subscriptionSchedules.retrieve( - starter1?.scheduled_ids![0]! + starter1?.scheduled_ids![0]!, ); // console.log(stripeSchedule); checkScheduleContainsProducts({ + db: this.db, schedule: stripeSchedule, productIds: [ attachProducts.starterGroup1.id, @@ -154,6 +155,7 @@ describe("Multi Product 3: premium1->starter1, premium2->starter2, then premium2 }); checkScheduleContainsProducts({ + db: this.db, scheduleId: starterGroup2?.scheduled_ids![0], productIds: [attachProducts.starterGroup2.id], sb: this.sb, diff --git a/server/tests/utils/scheduleCheckUtils.ts b/server/tests/utils/scheduleCheckUtils.ts index 9b1ef5294..ad26d5b3e 100644 --- a/server/tests/utils/scheduleCheckUtils.ts +++ b/server/tests/utils/scheduleCheckUtils.ts @@ -4,7 +4,9 @@ import { SupabaseClient } from "@supabase/supabase-js"; import { expect } from "chai"; import Stripe from "stripe"; import { createStripeCli } from "@/external/stripe/utils.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; export const checkScheduleContainsProducts = async ({ + db, sb, org, env, @@ -12,6 +14,7 @@ export const checkScheduleContainsProducts = async ({ schedule, productIds, }: { + db: DrizzleCli; sb: SupabaseClient; org: Organization; env: AppEnv; @@ -30,9 +33,9 @@ export const checkScheduleContainsProducts = async ({ let priceCount = 0; for (const productId of productIds) { - let product = await ProductService.getFullProduct({ - productId: productId, - sb: sb, + let product = await ProductService.getFull({ + db, + idOrInternalId: productId, orgId: org.id, env: env, }); @@ -40,8 +43,8 @@ export const checkScheduleContainsProducts = async ({ for (const price of product.prices) { expect( schedule!.phases[0].items.some( - (item) => item.price === price.config.stripe_price_id - ) + (item) => item.price === price.config!.stripe_price_id, + ), ).to.be.true; priceCount++; } @@ -51,12 +54,14 @@ export const checkScheduleContainsProducts = async ({ }; export const checkSubscriptionContainsProducts = async ({ + db, sb, org, env, subscriptionId, productIds, }: { + db: DrizzleCli; sb: SupabaseClient; org: Organization; env: AppEnv; @@ -68,9 +73,9 @@ export const checkSubscriptionContainsProducts = async ({ let totalPriceCount = 0; for (const productId of productIds) { - let product = await ProductService.getFullProduct({ - productId: productId, - sb: sb, + let product = await ProductService.getFull({ + db, + idOrInternalId: productId, orgId: org.id, env: env, }); @@ -80,18 +85,18 @@ export const checkSubscriptionContainsProducts = async ({ try { expect( sub.items.data.some( - (item) => item.price.id === price.config.stripe_price_id - ) + (item) => item.price.id === price.config.stripe_price_id, + ), ).to.be.true; } catch (error) { console.log("Stripe sub prices not matching product prices"); console.log( "Prices:", - product.prices.map((p: any) => p.config.stripe_price_id) + product.prices.map((p: any) => p.config.stripe_price_id), ); console.log( "Sub items:", - sub.items.data.map((i: any) => i.price.id) + sub.items.data.map((i: any) => i.price.id), ); throw error; } diff --git a/shared/db/cusTable.ts b/shared/db/cusTable.ts deleted file mode 100644 index beed0877a..000000000 --- a/shared/db/cusTable.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { pgTable, text, numeric, jsonb, unique } from "drizzle-orm/pg-core"; -import { collatePgColumn } from "./utils.js"; - -export const customers = pgTable( - "customers", - { - internal_id: text("internal_id").primaryKey().notNull(), - name: text().default(""), - org_id: text("org_id").notNull(), - created_at: numeric("created_at").notNull(), - id: text(), - env: text(), - processor: jsonb(), - email: text().default(""), - fingerprint: text(), - metadata: jsonb().default({}), - }, - (table) => [ - unique("cus_id_constraint").on(table.org_id, table.id, table.env), - ], -).enableRLS(); - -collatePgColumn(customers.internal_id, "C"); diff --git a/shared/db/schema.ts b/shared/db/schema.ts index 7265be926..35b6d578f 100644 --- a/shared/db/schema.ts +++ b/shared/db/schema.ts @@ -3,26 +3,43 @@ import { organizations } from "../models/orgModels/orgTable.js"; import { chatResults } from "../models/chatResultModels/chatResultTable.js"; import { features } from "../models/featureModels/featureTable.js"; +// Customer Tables +import { customers } from "../models/cusModels/cusTable.js"; +import { entities } from "../models/cusModels/entityModels/entityTable.js"; + // Product Tables import { products } from "../models/productModels/productTable.js"; import { prices } from "../models/productModels/priceModels/priceTable.js"; import { entitlements } from "../models/productModels/entModels/entTable.js"; import { freeTrials } from "../models/productModels/freeTrialModels/freeTrialTable.js"; +// CusProduct Tables +import { customerProducts } from "../models/cusProductModels/cusProductTable.js"; +import { customerPrices } from "../models/cusProductModels/cusPriceModels/cusPriceTable.js"; +import { customerEntitlements } from "../models/cusProductModels/cusEntModels/cusEntTable.js"; + // Others import { apiKeys } from "./apiKeysTable.js"; -import { customers } from "./cusTable.js"; /* RELATIONS */ import { organizationsRelations } from "../models/orgModels/orgRelations.js"; import { apiKeysRelations } from "./relations.js"; import { featureRelations } from "../models/featureModels/featureRelations.js"; -import { freeTrialRelations } from "../models/productModels/freeTrialModels/freeTrialRelations.js"; + +// Customer Relations +import { customersRelations } from "../models/cusModels/cusRelations.js"; +import { entitiesRelations } from "../models/cusModels/entityModels/entityRelations.js"; // Product Relations import { entitlementsRelations } from "../models/productModels/entModels/entRelations.js"; import { priceRelations } from "../models/productModels/priceModels/priceRelations.js"; import { productRelations } from "../models/productModels/productRelations.js"; +import { freeTrialRelations } from "../models/productModels/freeTrialModels/freeTrialRelations.js"; + +// CusProduct Relations +import { customerProductsRelations } from "../models/cusProductModels/cusProductRelations.js"; +import { customerPricesRelations } from "../models/cusProductModels/cusPriceModels/cusPriceRelations.js"; +import { customerEntitlementsRelations } from "../models/cusProductModels/cusEntModels/cusEntRelations.js"; const relations = { organizationsRelations, @@ -32,6 +49,13 @@ const relations = { priceRelations, productRelations, freeTrialRelations, + customerProductsRelations, + customerPricesRelations, + customerEntitlementsRelations, + + // Customer Relations + customersRelations, + entitiesRelations, }; export const schemas = { @@ -43,7 +67,14 @@ export const schemas = { prices, features, products, + customerProducts, + customerPrices, + customerEntitlements, + + // Customer Relations customers, + entities, + ...relations, }; diff --git a/shared/index.ts b/shared/index.ts index 60476234d..d35cf5693 100644 --- a/shared/index.ts +++ b/shared/index.ts @@ -45,6 +45,10 @@ export * from "./models/productV2Models/productResponseModels.js"; export * from "./models/productV2Models/productItemModels/productItemModels.js"; export * from "./models/productV2Models/productItemModels/prodItemResponseModels.js"; +// CUS PRODUCT MODELS +export * from "./models/cusProductModels/cusProductModels.js"; +export * from "./models/cusProductModels/cusEntModels/cusEntModels.js"; + // 4. Chat Result Models export * from "./models/chatResultModels/chatResultTable.js"; export * from "./models/chatResultModels/chatResultFeature.js"; @@ -62,10 +66,8 @@ export * from "./models/metadataModels.js"; // Customer Models export * from "./models/cusModels/cusModels.js"; -export * from "./models/cusModels/cusEntModels/cusEntitlementModels.js"; -export * from "./models/cusModels/cusProductModels.js"; -export * from "./models/cusModels/cusPriceModels/pricesInputModel.js"; -export * from "./models/cusModels/cusPriceModels/cusPriceModels.js"; + +export * from "./models/cusProductModels/cusPriceModels/cusPriceModels.js"; export * from "./models/cusModels/invoiceModels/invoiceModels.js"; export * from "./models/cusModels/cusResponseModels.js"; diff --git a/shared/models/cusModels/cusEntModels/customEntModels.ts b/shared/models/cusModels/cusEntModels/customEntModels.ts deleted file mode 100644 index 793b6e1f9..000000000 --- a/shared/models/cusModels/cusEntModels/customEntModels.ts +++ /dev/null @@ -1,18 +0,0 @@ -// import { z } from "zod"; -// import { -// AllowanceType, -// EntInterval, -// } from "../../productModels/entitlementModels.js"; - -// export const CustomEntitlementSchema = z.object({ -// id: z.string(), -// org_id: z.string(), -// created_at: z.number(), -// internal_feature_id: z.string(), - -// allowance_type: z.nativeEnum(AllowanceType), -// allowance: z.number(), -// interval: z.nativeEnum(EntInterval), -// }); - -// export type CustomEntitlement = z.infer; diff --git a/shared/models/cusModels/cusPriceModels/pricesInputModel.ts b/shared/models/cusModels/cusPriceModels/pricesInputModel.ts deleted file mode 100644 index 7e4c889f1..000000000 --- a/shared/models/cusModels/cusPriceModels/pricesInputModel.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { z } from "zod"; -import { FixedPriceConfigSchema } from "../../productModels/priceModels/priceConfig/fixedPriceConfig.js"; -import { UsagePriceConfigSchema } from "../../productModels/priceModels/priceConfig/usagePriceConfig.js"; - -export const PriceOptionsSchema = z.object({ - quantity: z.number().optional(), // for usage in-advance - threshold: z.number().optional(), // for usage below threshold -}); - -export const PricesInputSchema = z.array( - z.object({ - id: z.string(), - options: PriceOptionsSchema, - config: FixedPriceConfigSchema.or(UsagePriceConfigSchema), - }), -); - -export type PricesInput = z.infer; -export type PriceOptions = z.infer; diff --git a/shared/models/cusModels/cusRelations.ts b/shared/models/cusModels/cusRelations.ts new file mode 100644 index 000000000..f5a412b46 --- /dev/null +++ b/shared/models/cusModels/cusRelations.ts @@ -0,0 +1,14 @@ +import { relations } from "drizzle-orm"; +import { customers } from "./cusTable.js"; +import { customerPrices } from "../cusProductModels/cusPriceModels/cusPriceTable.js"; +import { customerProducts } from "../cusProductModels/cusProductTable.js"; +import { customerEntitlements } from "../cusProductModels/cusEntModels/cusEntTable.js"; +import { entities } from "./entityModels/entityTable.js"; + +export const customersRelations = relations(customers, ({ one, many }) => ({ + customerProducts: many(customerProducts), + customerPrices: many(customerPrices), + customerEntitlements: many(customerEntitlements), + + entities: many(entities), +})); diff --git a/shared/models/cusModels/cusResponseModels.ts b/shared/models/cusModels/cusResponseModels.ts index b6eb3ce0f..96738014d 100644 --- a/shared/models/cusModels/cusResponseModels.ts +++ b/shared/models/cusModels/cusResponseModels.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { CusProductStatus } from "./cusProductModels.js"; +import { CusProductStatus } from "../cusProductModels/cusProductModels.js"; import { AppEnv } from "../genModels/genEnums.js"; import { EntInterval } from "../productModels/entModels/entEnums.js"; import { InvoiceResponseSchema } from "./invoiceModels/invoiceResponseModels.js"; diff --git a/shared/models/cusModels/cusTable.ts b/shared/models/cusModels/cusTable.ts new file mode 100644 index 000000000..b9b3fa26b --- /dev/null +++ b/shared/models/cusModels/cusTable.ts @@ -0,0 +1,43 @@ +import { + pgTable, + text, + numeric, + jsonb, + unique, + foreignKey, +} from "drizzle-orm/pg-core"; +import { collatePgColumn } from "../../db/utils.js"; +import { organizations } from "../orgModels/orgTable.js"; +import { sql } from "drizzle-orm"; + +export type CustomerProcessor = { + type: "stripe"; + id: string; +}; + +export const customers = pgTable( + "customers", + { + internal_id: text("internal_id").primaryKey().notNull(), + org_id: text("org_id").notNull(), + created_at: numeric({ mode: "number" }).notNull(), + name: text(), + id: text(), + email: text(), + fingerprint: text().default(sql`null`), + metadata: jsonb().$type>(), + + env: text().notNull(), + processor: jsonb().$type(), + }, + (table) => [ + unique("cus_id_constraint").on(table.org_id, table.id, table.env), + foreignKey({ + columns: [table.org_id], + foreignColumns: [organizations.id], + name: "customers_org_id_fkey", + }).onDelete("cascade"), + ], +).enableRLS(); + +collatePgColumn(customers.internal_id, "C"); diff --git a/shared/models/cusModels/entityModels/entityRelations.ts b/shared/models/cusModels/entityModels/entityRelations.ts new file mode 100644 index 000000000..8734ccb30 --- /dev/null +++ b/shared/models/cusModels/entityModels/entityRelations.ts @@ -0,0 +1,20 @@ +import { relations } from "drizzle-orm"; +import { entities } from "./entityTable.js"; +import { customers } from "../cusTable.js"; +import { features } from "../../featureModels/featureTable.js"; +import { organizations } from "../../orgModels/orgTable.js"; + +export const entitiesRelations = relations(entities, ({ one }) => ({ + customer: one(customers, { + fields: [entities.internal_customer_id], + references: [customers.internal_id], + }), + feature: one(features, { + fields: [entities.internal_feature_id], + references: [features.internal_id], + }), + organization: one(organizations, { + fields: [entities.org_id], + references: [organizations.id], + }), +})); diff --git a/shared/models/cusModels/entityModels/entityTable.ts b/shared/models/cusModels/entityModels/entityTable.ts new file mode 100644 index 000000000..1b52c5297 --- /dev/null +++ b/shared/models/cusModels/entityModels/entityTable.ts @@ -0,0 +1,53 @@ +import { + boolean, + foreignKey, + numeric, + pgTable, + unique, +} from "drizzle-orm/pg-core"; +import { text } from "drizzle-orm/pg-core"; +import { customers } from "../cusTable.js"; +import { features } from "../../featureModels/featureTable.js"; +import { organizations } from "../../orgModels/orgTable.js"; + +export const entities = pgTable( + "entities", + { + internal_id: text("internal_id").primaryKey().notNull(), + org_id: text("org_id"), + env: text(), + internal_customer_id: text("internal_customer_id"), + created_at: numeric("created_at").notNull(), + id: text(), + name: text(), + deleted: boolean().default(false).notNull(), + internal_feature_id: text("internal_feature_id"), + + // Optional... + feature_id: text("feature_id"), + }, + (table) => [ + foreignKey({ + columns: [table.internal_customer_id], + foreignColumns: [customers.internal_id], + name: "entities_internal_customer_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.internal_feature_id], + foreignColumns: [features.internal_id], + name: "entities_internal_feature_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.org_id], + foreignColumns: [organizations.id], + name: "entities_org_id_fkey", + }).onDelete("cascade"), + + unique("entity_id_constraint").on( + table.org_id, + table.env, + table.internal_customer_id, + table.id, + ), + ], +); diff --git a/shared/models/cusModels/fullCusModel.ts b/shared/models/cusModels/fullCusModel.ts index 1d6b72e2f..a96affa75 100644 --- a/shared/models/cusModels/fullCusModel.ts +++ b/shared/models/cusModels/fullCusModel.ts @@ -1,5 +1,5 @@ +import { FullCusProduct } from "../cusProductModels/cusProductModels.js"; import { Customer } from "./cusModels.js"; -import { FullCusProduct } from "./cusProductModels.js"; import { Entity } from "./entityModels/entityModels.js"; import { Invoice } from "./invoiceModels/invoiceModels.js"; diff --git a/shared/models/cusModels/cusEntModels/cusEntitlementModels.ts b/shared/models/cusProductModels/cusEntModels/cusEntModels.ts similarity index 60% rename from shared/models/cusModels/cusEntModels/cusEntitlementModels.ts rename to shared/models/cusProductModels/cusEntModels/cusEntModels.ts index 76b5a7b52..155b9ff00 100644 --- a/shared/models/cusModels/cusEntModels/cusEntitlementModels.ts +++ b/shared/models/cusProductModels/cusEntModels/cusEntModels.ts @@ -1,16 +1,14 @@ import { z } from "zod"; -import { - EntitlementSchema, - EntitlementWithFeatureSchema, -} from "../../productModels/entModels/entModels.js"; -import { FeatureSchema } from "../../featureModels/featureModels.js"; +import { EntitlementWithFeatureSchema } from "../../productModels/entModels/entModels.js"; export const EntityBalanceSchema = z.object({ id: z.string(), balance: z.number(), adjustment: z.number(), }); + export type EntityBalance = z.infer; + export const CustomerEntitlementSchema = z.object({ // Foreign keys id: z.string(), @@ -39,25 +37,7 @@ export const FullCustomerEntitlementSchema = CustomerEntitlementSchema.extend({ entitlement: EntitlementWithFeatureSchema, }); -export const CusEntWithEntitlementSchema = CustomerEntitlementSchema.extend({ - entitlement: EntitlementSchema, -}); - -export const CusEntWithFeatureSchema = CustomerEntitlementSchema.extend({ - feature: FeatureSchema, -}); - -export const CusEntWithFeatureAndEntitlementSchema = - CusEntWithFeatureSchema.extend({ - entitlement: EntitlementSchema, - }); - -export type CusEntWithFeatureAndEntitlement = z.infer< - typeof CusEntWithFeatureAndEntitlementSchema ->; export type CustomerEntitlement = z.infer; -export type CusEntWithEntitlement = z.infer; -export type CusEntWithFeature = z.infer; export type FullCustomerEntitlement = z.infer< typeof FullCustomerEntitlementSchema diff --git a/shared/models/cusProductModels/cusEntModels/cusEntRelations.ts b/shared/models/cusProductModels/cusEntModels/cusEntRelations.ts new file mode 100644 index 000000000..80fc4b27a --- /dev/null +++ b/shared/models/cusProductModels/cusEntModels/cusEntRelations.ts @@ -0,0 +1,29 @@ +import { relations } from "drizzle-orm"; + +import { entitlements } from "../../productModels/entModels/entTable.js"; +import { customers } from "../../cusModels/cusTable.js"; +import { features } from "../../featureModels/featureTable.js"; +import { customerEntitlements } from "./cusEntTable.js"; +import { customerProducts } from "../cusProductTable.js"; + +export const customerEntitlementsRelations = relations( + customerEntitlements, + ({ one, many }) => ({ + customerProduct: one(customerProducts, { + fields: [customerEntitlements.customer_product_id], + references: [customerProducts.id], + }), + customer: one(customers, { + fields: [customerEntitlements.internal_customer_id], + references: [customers.internal_id], + }), + entitlement: one(entitlements, { + fields: [customerEntitlements.entitlement_id], + references: [entitlements.id], + }), + feature: one(features, { + fields: [customerEntitlements.internal_feature_id], + references: [features.internal_id], + }), + }), +); diff --git a/shared/models/cusProductModels/cusEntModels/cusEntTable.ts b/shared/models/cusProductModels/cusEntModels/cusEntTable.ts new file mode 100644 index 000000000..2353249ef --- /dev/null +++ b/shared/models/cusProductModels/cusEntModels/cusEntTable.ts @@ -0,0 +1,55 @@ +import { + pgTable, + numeric, + boolean, + foreignKey, + unique, + text, + jsonb, +} from "drizzle-orm/pg-core"; + +import { features } from "../../featureModels/featureTable.js"; + +import { collatePgColumn } from "../../../db/utils.js"; +import { EntityBalance } from "./cusEntModels.js"; +import { customerProducts } from "../cusProductTable.js"; + +export const customerEntitlements = pgTable( + "customer_entitlements", + { + id: text().primaryKey().notNull(), + customer_product_id: text().notNull(), + entitlement_id: text().notNull(), + internal_customer_id: text().notNull(), + internal_feature_id: text().notNull(), + + unlimited: boolean("unlimited").default(false), + balance: numeric({ mode: "number" }), + created_at: numeric({ mode: "number" }).notNull(), + next_reset_at: numeric({ mode: "number" }), + usage_allowed: boolean("usage_allowed").default(false), + adjustment: numeric({ mode: "number" }), + entities: jsonb("entities").$type>(), + + // Optional... + customer_id: text("customer_id"), + feature_id: text("feature_id"), + }, + (table) => [ + foreignKey({ + columns: [table.internal_feature_id], + foreignColumns: [features.internal_id], + name: "entitlements_internal_feature_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.customer_product_id], + foreignColumns: [customerProducts.id], + name: "customer_entitlements_customer_product_id_fkey", + }) + .onUpdate("cascade") + .onDelete("cascade"), + unique("entitlements_id_key").on(table.id), + ], +); + +collatePgColumn(customerEntitlements.id, "C"); diff --git a/shared/models/cusModels/cusPriceModels/cusPriceModels.ts b/shared/models/cusProductModels/cusPriceModels/cusPriceModels.ts similarity index 100% rename from shared/models/cusModels/cusPriceModels/cusPriceModels.ts rename to shared/models/cusProductModels/cusPriceModels/cusPriceModels.ts diff --git a/shared/models/cusProductModels/cusPriceModels/cusPriceRelations.ts b/shared/models/cusProductModels/cusPriceModels/cusPriceRelations.ts new file mode 100644 index 000000000..b77463a6b --- /dev/null +++ b/shared/models/cusProductModels/cusPriceModels/cusPriceRelations.ts @@ -0,0 +1,24 @@ +import { relations } from "drizzle-orm"; + +import { customers } from "../../cusModels/cusTable.js"; +import { prices } from "../../productModels/priceModels/priceTable.js"; +import { customerPrices } from "./cusPriceTable.js"; +import { customerProducts } from "../cusProductTable.js"; + +export const customerPricesRelations = relations( + customerPrices, + ({ one, many }) => ({ + customerProduct: one(customerProducts, { + fields: [customerPrices.customer_product_id], + references: [customerProducts.id], + }), + customer: one(customers, { + fields: [customerPrices.internal_customer_id], + references: [customers.internal_id], + }), + price: one(prices, { + fields: [customerPrices.price_id], + references: [prices.id], + }), + }), +); diff --git a/shared/models/cusProductModels/cusPriceModels/cusPriceTable.ts b/shared/models/cusProductModels/cusPriceModels/cusPriceTable.ts new file mode 100644 index 000000000..d3db38d7c --- /dev/null +++ b/shared/models/cusProductModels/cusPriceModels/cusPriceTable.ts @@ -0,0 +1,35 @@ +import { foreignKey, jsonb, text } from "drizzle-orm/pg-core"; +import { numeric, pgTable } from "drizzle-orm/pg-core"; + +import { customers } from "../../cusModels/cusTable.js"; +import { prices } from "../../productModels/priceModels/priceTable.js"; +import { customerProducts } from "../cusProductTable.js"; + +export const customerPrices = pgTable( + "customer_prices", + { + id: text().primaryKey().notNull(), + created_at: numeric({ mode: "number" }).notNull(), + price_id: text("price_id"), + options: jsonb(), + internal_customer_id: text("internal_customer_id"), + customer_product_id: text("customer_product_id"), + }, + (table) => [ + foreignKey({ + columns: [table.customer_product_id], + foreignColumns: [customerProducts.id], + name: "customer_prices_customer_product_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.internal_customer_id], + foreignColumns: [customers.internal_id], + name: "customer_prices_internal_customer_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.price_id], + foreignColumns: [prices.id], + name: "customer_prices_price_id_fkey", + }), + ], +); diff --git a/shared/models/cusModels/cusProductModels.ts b/shared/models/cusProductModels/cusProductModels.ts similarity index 93% rename from shared/models/cusModels/cusProductModels.ts rename to shared/models/cusProductModels/cusProductModels.ts index d21730184..300178866 100644 --- a/shared/models/cusModels/cusProductModels.ts +++ b/shared/models/cusProductModels/cusProductModels.ts @@ -1,14 +1,15 @@ import { z } from "zod"; import { ProcessorType } from "../genModels/genEnums.js"; import { ProductSchema } from "../productModels/productModels.js"; -import { CustomerPriceSchema } from "./cusPriceModels/cusPriceModels.js"; import { PriceSchema } from "../productModels/priceModels/priceModels.js"; -import { CustomerEntitlementSchema } from "./cusEntModels/cusEntitlementModels.js"; +import { CustomerEntitlementSchema } from "./cusEntModels/cusEntModels.js"; import { EntitlementSchema } from "../productModels/entModels/entModels.js"; import { FeatureSchema } from "../featureModels/featureModels.js"; -import { CustomerSchema } from "./cusModels.js"; +import { CustomerSchema } from "../cusModels/cusModels.js"; import { FreeTrialSchema } from "../productModels/freeTrialModels/freeTrialModels.js"; +import { CustomerPriceSchema } from "./cusPriceModels/cusPriceModels.js"; + export const FeatureOptionsSchema = z.object({ internal_feature_id: z.string().optional(), feature_id: z.string(), @@ -82,11 +83,6 @@ export const CusProductSchema = z.object({ export type CusProduct = z.infer; -export const CusProductWithProduct = CusProductSchema.extend({ - product: ProductSchema, -}); - -export type CusProductWithProduct = z.infer; export type FeatureOptions = z.infer; export const FullCusProductSchema = CusProductSchema.extend({ diff --git a/shared/models/cusProductModels/cusProductRelations.ts b/shared/models/cusProductModels/cusProductRelations.ts new file mode 100644 index 000000000..0013b9e09 --- /dev/null +++ b/shared/models/cusProductModels/cusProductRelations.ts @@ -0,0 +1,25 @@ +import { relations } from "drizzle-orm"; +import { customerProducts } from "./cusProductTable.js"; +import { customers } from "../cusModels/cusTable.js"; +import { products } from "../productModels/productTable.js"; +import { freeTrials } from "../productModels/freeTrialModels/freeTrialTable.js"; +import { customerEntitlements } from "./cusEntModels/cusEntTable.js"; + +export const customerProductsRelations = relations( + customerProducts, + ({ one, many }) => ({ + customer: one(customers, { + fields: [customerProducts.internal_customer_id], + references: [customers.internal_id], + }), + product: one(products, { + fields: [customerProducts.internal_product_id], + references: [products.internal_id], + }), + free_trial: one(freeTrials, { + fields: [customerProducts.free_trial_id], + references: [freeTrials.id], + }), + customer_entitlements: many(customerEntitlements), + }), +); diff --git a/shared/models/cusProductModels/cusProductTable.ts b/shared/models/cusProductModels/cusProductTable.ts new file mode 100644 index 000000000..2bfb514e8 --- /dev/null +++ b/shared/models/cusProductModels/cusProductTable.ts @@ -0,0 +1,77 @@ +import { + pgTable, + text, + numeric, + jsonb, + boolean, + foreignKey, +} from "drizzle-orm/pg-core"; +import { freeTrials } from "../productModels/freeTrialModels/freeTrialTable.js"; +import { customers } from "../cusModels/cusTable.js"; +import { products } from "../productModels/productTable.js"; +import { collatePgColumn } from "../../db/utils.js"; +import { entities } from "../cusModels/entityModels/entityTable.js"; + +export type CustomerProductProcessor = { + type: "stripe"; + id: string; +}; + +export const customerProducts = pgTable( + "customer_products", + { + id: text().primaryKey().notNull(), + internal_customer_id: text("internal_customer_id").notNull(), + internal_product_id: text("internal_product_id").notNull(), + internal_entity_id: text("internal_entity_id"), + + created_at: numeric({ mode: "number" }), + status: text(), + processor: jsonb().$type(), + canceled_at: numeric({ mode: "number" }), + ended_at: numeric({ mode: "number" }), + starts_at: numeric({ mode: "number" }), + options: jsonb().array(), + product_id: text("product_id"), + free_trial_id: text("free_trial_id"), + trial_ends_at: numeric({ mode: "number" }), + collection_method: text("collection_method").default( + "charge_automatically", + ), + subscription_ids: text("subscription_ids").array(), + scheduled_ids: text("scheduled_ids").array(), + quantity: numeric({ mode: "number" }).default(1), + + is_custom: boolean("is_custom").default(false).notNull(), + + // Optional... + customer_id: text("customer_id"), + entity_id: text("entity_id"), + }, + (table) => [ + foreignKey({ + columns: [table.free_trial_id], + foreignColumns: [freeTrials.id], + name: "customer_products_free_trial_id_fkey", + }), + foreignKey({ + columns: [table.internal_customer_id], + foreignColumns: [customers.internal_id], + name: "customer_products_internal_customer_id_fkey", + }) + .onUpdate("cascade") + .onDelete("cascade"), + foreignKey({ + columns: [table.internal_product_id], + foreignColumns: [products.internal_id], + name: "customer_products_internal_product_id_fkey", + }), + foreignKey({ + columns: [table.internal_entity_id], + foreignColumns: [entities.internal_id], + name: "customer_products_internal_entity_id_fkey", + }).onDelete("set null"), + ], +); + +collatePgColumn(customerProducts.id, "C"); diff --git a/shared/models/genModels/genEnums.ts b/shared/models/genModels/genEnums.ts index 59d4bfe89..befdae6ee 100644 --- a/shared/models/genModels/genEnums.ts +++ b/shared/models/genModels/genEnums.ts @@ -22,3 +22,9 @@ export enum Duration { export enum ProcessorType { Stripe = "stripe", } + +export enum AuthType { + SecretKey = "secret_key", + PublicKey = "public_key", + Frontend = "frontend", +} diff --git a/shared/models/productModels/freeTrialModels/freeTrialTable.ts b/shared/models/productModels/freeTrialModels/freeTrialTable.ts index 251ab7ac9..aa7cca493 100644 --- a/shared/models/productModels/freeTrialModels/freeTrialTable.ts +++ b/shared/models/productModels/freeTrialModels/freeTrialTable.ts @@ -14,7 +14,7 @@ export const freeTrials = pgTable( created_at: numeric({ mode: "number" }).notNull(), internal_product_id: text("internal_product_id"), duration: text().default("day"), - length: numeric(), + length: numeric({ mode: "number" }), unique_fingerprint: boolean("unique_fingerprint"), is_custom: boolean("is_custom").default(false), }, diff --git a/shared/models/productModels/priceModels/priceModels.ts b/shared/models/productModels/priceModels/priceModels.ts index 8957dbb6b..b02ecbf5a 100644 --- a/shared/models/productModels/priceModels/priceModels.ts +++ b/shared/models/productModels/priceModels/priceModels.ts @@ -26,7 +26,7 @@ export const PriceSchema = z.object({ is_custom: z.boolean().optional(), // name: z.string().optional(), - config: FixedPriceConfigSchema.or(UsagePriceConfigSchema).optional(), + config: FixedPriceConfigSchema.or(UsagePriceConfigSchema), entitlement_id: z.string().nullish(), }); diff --git a/shared/models/productModels/productModels.ts b/shared/models/productModels/productModels.ts index 8f4e924b1..d5582efb6 100644 --- a/shared/models/productModels/productModels.ts +++ b/shared/models/productModels/productModels.ts @@ -66,7 +66,8 @@ export const FullProductSchema = ProductSchema.extend({ feature: FeatureSchema, }), ), - free_trial: FreeTrialSchema.optional(), + free_trial: FreeTrialSchema.nullish(), + free_trials: z.array(FreeTrialSchema).nullish(), }); export type Product = z.infer; diff --git a/shared/models/productModels/productRelations.ts b/shared/models/productModels/productRelations.ts index 092fe174b..97e10309f 100644 --- a/shared/models/productModels/productRelations.ts +++ b/shared/models/productModels/productRelations.ts @@ -8,8 +8,5 @@ export const productRelations = relations(products, ({ many, one }) => ({ entitlements: many(entitlements), prices: many(prices), - freeTrial: one(freeTrials, { - fields: [products.internal_id], - references: [freeTrials.internal_product_id], - }), + free_trials: many(freeTrials), }));