diff --git a/server/drizzle/schema.ts b/server/drizzle/schema.ts index 9b44bd92d..2875c7786 100644 --- a/server/drizzle/schema.ts +++ b/server/drizzle/schema.ts @@ -4,64 +4,8 @@ import { unique, text, numeric, - jsonb, boolean, - primaryKey, } from "drizzle-orm/pg-core"; -import { sql } from "drizzle-orm"; - -export const rewards = pgTable( - "rewards", - { - internalId: text("internal_id").primaryKey().notNull(), - env: text(), - name: text(), - orgId: text("org_id"), - createdAt: numeric("created_at"), - discountConfig: jsonb("discount_config"), - freeProductId: text("free_product_id"), - id: text(), - promoCodes: jsonb("promo_codes").array(), - type: text(), - }, - (table) => [ - foreignKey({ - columns: [table.orgId], - foreignColumns: [organizations.id], - name: "coupons_org_id_fkey", - }).onDelete("cascade"), - ], -); - -export const rewardPrograms = pgTable( - "reward_programs", - { - internalId: text("internal_id").primaryKey().notNull(), - id: text(), - createdAt: numeric("created_at").notNull(), - internalRewardId: text("internal_reward_id"), - maxRedemptions: numeric("max_redemptions"), - unlimitedRedemptions: boolean("unlimited_redemptions").default(false), - orgId: text("org_id"), - env: text(), - when: text().default("immediately"), - productIds: text("product_ids").array().default([""]), - excludeTrial: boolean("exclude_trial").default(false), - receivedBy: text("received_by"), - }, - (table) => [ - foreignKey({ - columns: [table.internalRewardId], - foreignColumns: [rewards.internalId], - name: "reward_triggers_internal_reward_id_fkey", - }).onDelete("cascade"), - foreignKey({ - columns: [table.orgId], - foreignColumns: [organizations.id], - name: "reward_triggers_org_id_fkey", - }).onDelete("cascade"), - ], -); export const invoiceItems = pgTable( "invoice_items", @@ -89,130 +33,3 @@ export const invoiceItems = pgTable( unique("invoice_items_id_key").on(table.id), ], ); - -export const rewardRedemptions = pgTable( - "reward_redemptions", - { - id: text().primaryKey().notNull(), - createdAt: numeric("created_at").notNull(), - updatedAt: numeric("updated_at"), - internalCustomerId: text("internal_customer_id"), - triggered: boolean(), - internalRewardProgramId: text("internal_reward_program_id"), - applied: boolean().default(false), - referralCodeId: text("referral_code_id"), - }, - (table) => [ - foreignKey({ - columns: [table.internalCustomerId], - foreignColumns: [customers.internalId], - name: "reward_redemptions_internal_customer_id_fkey", - }).onDelete("cascade"), - foreignKey({ - columns: [table.internalRewardProgramId], - foreignColumns: [rewardPrograms.internalId], - name: "reward_redemptions_internal_reward_program_id_fkey", - }).onDelete("cascade"), - foreignKey({ - columns: [table.referralCodeId], - foreignColumns: [referralCodes.id], - name: "reward_redemptions_referral_code_id_fkey", - }).onDelete("cascade"), - ], -); - -export const migrationJobs = pgTable( - "migration_jobs", - { - id: text().primaryKey().notNull(), - createdAt: numeric("created_at").notNull(), - updatedAt: numeric("updated_at"), - currentStep: text("current_step"), - fromInternalProductId: text("from_internal_product_id"), - toInternalProductId: text("to_internal_product_id"), - stepDetails: jsonb("step_details"), - orgId: text("org_id"), - env: text(), - }, - (table) => [ - foreignKey({ - columns: [table.fromInternalProductId], - foreignColumns: [products.internalId], - name: "migration_jobs_from_internal_product_id_fkey", - }).onDelete("cascade"), - foreignKey({ - columns: [table.orgId], - foreignColumns: [organizations.id], - name: "migration_jobs_org_id_fkey", - }).onDelete("cascade"), - foreignKey({ - columns: [table.toInternalProductId], - foreignColumns: [products.internalId], - name: "migration_jobs_to_internal_product_id_fkey", - }).onDelete("cascade"), - ], -); - -export const referralCodes = pgTable( - "referral_codes", - { - code: text().notNull(), - orgId: text("org_id").notNull(), - env: text().notNull(), - internalCustomerId: text("internal_customer_id"), - internalRewardProgramId: text("internal_reward_program_id"), - id: text().notNull(), - createdAt: numeric("created_at"), - }, - (table) => [ - foreignKey({ - columns: [table.internalCustomerId], - foreignColumns: [customers.internalId], - name: "referral_codes_internal_customer_id_fkey", - }).onDelete("cascade"), - foreignKey({ - columns: [table.internalRewardProgramId], - foreignColumns: [rewardPrograms.internalId], - name: "referral_codes_internal_reward_program_id_fkey", - }).onDelete("cascade"), - foreignKey({ - columns: [table.orgId], - foreignColumns: [organizations.id], - name: "referral_codes_org_id_fkey", - }).onDelete("cascade"), - primaryKey({ - columns: [table.code, table.orgId, table.env], - name: "referral_codes_pkey", - }), - unique("referral_codes_id_key").on(table.id), - ], -); - -export const migrationErrors = pgTable( - "migration_errors", - { - internalCustomerId: text("internal_customer_id").notNull(), - migrationJobId: text("migration_job_id").notNull(), - createdAt: numeric("created_at"), - updatedAt: numeric("updated_at"), - data: jsonb(), - message: text(), - code: text(), - }, - (table) => [ - foreignKey({ - columns: [table.internalCustomerId], - foreignColumns: [customers.internalId], - name: "migration_customers_internal_customer_id_fkey", - }).onDelete("cascade"), - foreignKey({ - columns: [table.migrationJobId], - foreignColumns: [migrationJobs.id], - name: "migration_customers_migration_job_id_fkey", - }).onDelete("cascade"), - primaryKey({ - columns: [table.internalCustomerId, table.migrationJobId], - name: "migration_errors_pkey", - }), - ], -); diff --git a/server/src/cron.ts b/server/src/cron.ts index 3a3750354..ead64efaf 100644 --- a/server/src/cron.ts +++ b/server/src/cron.ts @@ -26,6 +26,7 @@ import { UTCDate } from "@date-fns/utc"; import { DrizzleCli, initDrizzle } from "./db/initDrizzle.js"; import { isEqual } from "lodash-es"; +import { CusPriceService } from "./internal/customers/prices/CusPriceService.js"; dotenv.config(); @@ -92,11 +93,9 @@ const checkSubAnchor = async ({ }; const resetCustomerEntitlement = async ({ - sb, db, cusEnt, }: { - sb: SupabaseClient; db: DrizzleCli; cusEnt: FullCusEntWithProduct; }) => { @@ -106,15 +105,10 @@ const resetCustomerEntitlement = async ({ } // Fetch related price - const { data: cusPrices, error: cusPricesError } = await sb - .from("customer_prices") - .select("*, price:prices!inner(*)") - .eq("customer_product_id", cusEnt.customer_product_id); - - if (cusPricesError) { - console.log("Error fetching customer prices:", cusPricesError); - throw new Error("Error fetching customer prices"); - } + const cusPrices = await CusPriceService.getByCustomerProductId({ + db, + customerProductId: cusEnt.customer_product_id, + }); // 2. Quantity is from prices... const relatedCusPrice = getRelatedCusPrice(cusEnt, cusPrices); @@ -178,6 +172,7 @@ const resetCustomerEntitlement = async ({ let resetBalanceUpdate = getResetBalancesUpdate({ cusEnt, + allowance: resetBalance || undefined, }); try { @@ -224,8 +219,7 @@ export const cronTask = async () => { "\n----------------------------------\nRUNNING RESET CRON:", format(new UTCDate(), "yyyy-MM-dd HH:mm:ss"), ); - // 1. Query customer_entitlements for all customers with reset_interval < now - const sb = createSupabaseClient(); + const { db, client } = initDrizzle(); try { @@ -239,7 +233,6 @@ export const cronTask = async () => { for (const cusEnt of batch) { batchResets.push( resetCustomerEntitlement({ - sb, db, cusEnt: cusEnt as FullCusEntWithProduct, }), diff --git a/server/src/db/initDrizzle.ts b/server/src/db/initDrizzle.ts index 683876ce4..8a7ebf96b 100644 --- a/server/src/db/initDrizzle.ts +++ b/server/src/db/initDrizzle.ts @@ -10,6 +10,7 @@ export const initDrizzle = () => { const db = drizzle(client, { schema: schemas, + // logger: true, // Enable SQL logging for debugging }); return { db, client }; diff --git a/server/src/errors/errCodes.ts b/server/src/errors/errCodes.ts index be7064e10..548a2c21e 100644 --- a/server/src/errors/errCodes.ts +++ b/server/src/errors/errCodes.ts @@ -44,6 +44,7 @@ export const ErrCode = { AttachProductToCustomerFailed: "attach_product_to_customer_failed", MultipleProductsFound: "multiple_products_found", MultipleCustomersFound: "multiple_customers_found", + GetCusWithProductsFailed: "get_cus_with_products_failed", // Product InvalidProduct: "invalid_product", diff --git a/server/src/external/stripe/stripeCusUtils.ts b/server/src/external/stripe/stripeCusUtils.ts index 221dba9c1..d1b358c83 100644 --- a/server/src/external/stripe/stripeCusUtils.ts +++ b/server/src/external/stripe/stripeCusUtils.ts @@ -159,14 +159,14 @@ export const getCusPaymentMethod = async ({ // 2. Create a payment method and attach to customer export const attachPmToCus = async ({ - sb, + db, customer, org, env, willFail = false, testClockId, }: { - sb: SupabaseClient; + db: DrizzleCli; customer: Customer; org: Organization; env: AppEnv; @@ -184,15 +184,17 @@ export const attachPmToCus = async ({ testClockId, }); - await sb - .from("customers") - .update({ + await CusService.update({ + db, + internalCusId: customer.internal_id, + update: { processor: { id: stripeCustomer.id, - type: "stripe", + type: ProcessorType.Stripe, }, - }) - .eq("internal_id", customer.internal_id); + }, + }); + stripeCusId = stripeCustomer.id; customer.processor = { id: stripeCustomer.id, diff --git a/server/src/external/stripe/stripeInvoiceUtils.ts b/server/src/external/stripe/stripeInvoiceUtils.ts index 91394f307..1c71f3376 100644 --- a/server/src/external/stripe/stripeInvoiceUtils.ts +++ b/server/src/external/stripe/stripeInvoiceUtils.ts @@ -15,6 +15,7 @@ import RecaseError, { isPaymentDeclined } from "@/utils/errorUtils.js"; import { isStripeCardDeclined } from "./stripeCardUtils.js"; import { SupabaseClient } from "@supabase/supabase-js"; import { InvoiceService } from "@/internal/customers/invoices/InvoiceService.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; export const getStripeExpandedInvoice = async ({ stripeCli, @@ -72,7 +73,7 @@ export const payForInvoice = async ({ }; } catch (error: any) { logger.error( - ` ❌ Stripe error: Failed to pay invoice: ${error?.message || error}` + ` ❌ Stripe error: Failed to pay invoice: ${error?.message || error}`, ); if (isStripeCardDeclined(error)) { @@ -97,22 +98,22 @@ export const payForInvoice = async ({ }; export const updateInvoiceIfExists = async ({ - sb, + db, invoice, }: { - sb: SupabaseClient; + db: DrizzleCli; invoice: Stripe.Invoice; }) => { // TODO: Can optimize this function... - const existingInvoice = await InvoiceService.getInvoiceByStripeId({ - sb, - stripeInvoiceId: invoice.id, + const existingInvoice = await InvoiceService.getByStripeId({ + db, + stripeId: invoice.id, }); if (existingInvoice) { await InvoiceService.updateByStripeId({ - sb, - stripeInvoiceId: invoice.id, + db, + stripeId: invoice.id, updates: { status: invoice.status as InvoiceStatus, hosted_invoice_url: invoice.hosted_invoice_url, @@ -148,7 +149,7 @@ export const getInvoiceDiscounts = ({ let autumnDiscounts = expandedInvoice.discounts.map((discount: any) => { const amountOff = discount.coupon.amount_off; const amountUsed = totalDiscountAmounts?.find( - (item) => item.discount === discount.id + (item) => item.discount === discount.id, )?.amount; let autumnDiscount: InvoiceDiscount = { diff --git a/server/src/external/stripe/stripeSubUtils.ts b/server/src/external/stripe/stripeSubUtils.ts index 68fd5b913..649f90d06 100644 --- a/server/src/external/stripe/stripeSubUtils.ts +++ b/server/src/external/stripe/stripeSubUtils.ts @@ -10,8 +10,8 @@ import { import { differenceInSeconds } from "date-fns"; import { ProrationBehavior } from "@/internal/customers/change-product/handleUpgrade.js"; import { SubService } from "@/internal/subscriptions/SubService.js"; -import { SupabaseClient } from "@supabase/supabase-js"; import { stripeToAutumnInterval } from "./utils.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; export const getStripeSubs = async ({ stripeCli, @@ -34,7 +34,7 @@ export const getStripeSubs = async ({ } catch (error: any) { console.log( `(warning) getStripeSubs: Failed to get sub ${subId}`, - error.message + error.message, ); return null; } @@ -86,13 +86,13 @@ export const deleteScheduledIds = async ({ // Get in advance sub export const getUsageBasedSub = async ({ - sb, + db, stripeCli, subIds, feature, stripeSubs, }: { - sb: SupabaseClient; + db: DrizzleCli; stripeCli: Stripe; subIds: string[]; feature: Feature; @@ -111,7 +111,7 @@ export const getUsageBasedSub = async ({ let finalSubIds = subs.map((sub) => sub.id); let autumnSubs = await SubService.getInStripeIds({ - sb, + db, ids: finalSubIds, }); @@ -122,7 +122,7 @@ export const getUsageBasedSub = async ({ let autumnSub = autumnSubs?.find((sub) => sub.stripe_id == stripeSub.id); if (autumnSub) { let containsFeature = autumnSub.usage_features.includes( - feature.internal_id + feature.internal_id!, ); if (containsFeature) { return stripeSub; @@ -138,7 +138,7 @@ export const getUsageBasedSub = async ({ if ( !usageFeatures || usageFeatures.find( - (feat: any) => feat.internal_id == feature.internal_id + (feat: any) => feat.internal_id == feature.internal_id, ) === undefined ) { continue; @@ -168,14 +168,15 @@ export const getSubItemsForCusProduct = async ({ prices.some( (p) => p.config?.stripe_price_id == item.price.id || - (p.config as UsagePriceConfig).stripe_product_id == item.price.product + (p.config as UsagePriceConfig).stripe_product_id == + item.price.product, ) ) { subItems.push(item); } } let otherSubItems = stripeSub.items.data.filter( - (item) => !subItems.some((i) => i.id == item.id) + (item) => !subItems.some((i) => i.id == item.id), ); return { subItems, otherSubItems }; @@ -191,9 +192,8 @@ export const getStripeSchedules = async ({ const batchGet = []; const getStripeSchedule = async (scheduleId: string) => { try { - const schedule = await stripeCli.subscriptionSchedules.retrieve( - scheduleId - ); + const schedule = + await stripeCli.subscriptionSchedules.retrieve(scheduleId); const batchPricesGet = []; for (const item of schedule.phases[0].items) { diff --git a/server/src/external/stripe/stripeSubUtils/createStripeSub.ts b/server/src/external/stripe/stripeSubUtils/createStripeSub.ts index 9626cfe90..3b6f6ff81 100644 --- a/server/src/external/stripe/stripeSubUtils/createStripeSub.ts +++ b/server/src/external/stripe/stripeSubUtils/createStripeSub.ts @@ -13,11 +13,12 @@ import { SubService } from "@/internal/subscriptions/SubService.js"; import { generateId } from "@/utils/genUtils.js"; import { SupabaseClient } from "@supabase/supabase-js"; import { ItemSet } from "@/utils/models/ItemSet.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; // Get payment method export const createStripeSub = async ({ - sb, + db, stripeCli, customer, org, @@ -27,7 +28,7 @@ export const createStripeSub = async ({ itemSet, shouldPreview = false, }: { - sb: SupabaseClient; + db: DrizzleCli; stripeCli: Stripe; customer: Customer; freeTrial: FreeTrial | null; @@ -55,11 +56,11 @@ export const createStripeSub = async ({ const { items, prices, interval, subMeta, usageFeatures } = itemSet; let subItems = items.filter( (i: any, index: number) => - prices[index].config!.interval !== BillingInterval.OneOff + prices[index].config!.interval !== BillingInterval.OneOff, ); let invoiceItems = items.filter( (i: any, index: number) => - prices[index].config!.interval === BillingInterval.OneOff + prices[index].config!.interval === BillingInterval.OneOff, ); if (shouldPreview) { @@ -93,7 +94,7 @@ export const createStripeSub = async ({ // Store await SubService.createSub({ - sb, + db, sub: { id: generateId("sub"), stripe_id: subscription.id, diff --git a/server/src/external/stripe/stripeSubUtils/updateStripeSub.ts b/server/src/external/stripe/stripeSubUtils/updateStripeSub.ts index ceac5bb3c..c366b3cb0 100644 --- a/server/src/external/stripe/stripeSubUtils/updateStripeSub.ts +++ b/server/src/external/stripe/stripeSubUtils/updateStripeSub.ts @@ -14,9 +14,10 @@ import { getStripeProrationBehavior } from "../stripeSubUtils.js"; import { SupabaseClient } from "@supabase/supabase-js"; import { SubService } from "@/internal/subscriptions/SubService.js"; import { ItemSet } from "@/utils/models/ItemSet.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; export const updateStripeSubscription = async ({ - sb, + db, org, customer, stripeCli, @@ -28,7 +29,7 @@ export const updateStripeSubscription = async ({ itemSet, shouldPreview, }: { - sb: SupabaseClient; + db: DrizzleCli; org: Organization; customer: Customer; stripeCli: Stripe; @@ -57,7 +58,7 @@ export const updateStripeSubscription = async ({ let { items, prices, subMeta } = itemSet; let subItems = items.filter( (i: any, index: number) => - i.deleted || prices[index].config!.interval !== BillingInterval.OneOff + i.deleted || prices[index].config!.interval !== BillingInterval.OneOff, ); let subInvoiceItems = items.filter((i: any, index: number) => { @@ -105,7 +106,7 @@ export const updateStripeSubscription = async ({ // Upsert sub await SubService.addUsageFeatures({ - sb, + db, stripeId: subscriptionId, usageFeatures: itemSet.usageFeatures, orgId: org.id, diff --git a/server/src/external/stripe/stripeWebhooks.ts b/server/src/external/stripe/stripeWebhooks.ts index a847b782b..1d28cd9d2 100644 --- a/server/src/external/stripe/stripeWebhooks.ts +++ b/server/src/external/stripe/stripeWebhooks.ts @@ -27,10 +27,11 @@ stripeWebhookRouter.post( let event; const { orgId, env } = request.params; + const { db } = request; let org: Organization; try { - org = await OrgService.getFullOrg({ sb: request.sb, orgId }); + org = await OrgService.get({ db: request.db, orgId }); } catch (error) { console.log(`Org ${orgId} not found`); response.status(200).send(`Org ${orgId} not found`); @@ -69,14 +70,11 @@ stripeWebhookRouter.post( } | ID: ${event?.id}`, ); - const { db, pg, sb } = request; - try { switch (event.type) { case "customer.subscription.created": await handleSubCreated({ db, - sb: request.sb, org, subscription: event.data.object, env, @@ -88,7 +86,6 @@ stripeWebhookRouter.post( const subscription = event.data.object; await handleSubscriptionUpdated({ db, - sb: request.sb, org, subscription, previousAttributes: event.data.previous_attributes, @@ -101,7 +98,6 @@ stripeWebhookRouter.post( const deletedSubscription = event.data.object; await handleSubscriptionDeleted({ db, - sb: request.sb, subscription: deletedSubscription, org, env, @@ -113,7 +109,6 @@ stripeWebhookRouter.post( const checkoutSession = event.data.object; await handleCheckoutSessionCompleted({ db, - sb: request.sb, checkoutSession, org, env, @@ -126,7 +121,6 @@ stripeWebhookRouter.post( const invoice = event.data.object; await handleInvoicePaid({ db, - sb: request.sb, org, invoice, env, @@ -139,12 +133,10 @@ stripeWebhookRouter.post( const createdInvoice = event.data.object; await handleInvoiceCreated({ db, - sb, org, invoice: createdInvoice, env, event, - pg, }); break; @@ -152,7 +144,6 @@ stripeWebhookRouter.post( const finalizedInvoice = event.data.object; await handleInvoiceFinalized({ db, - sb: request.sb, org, invoice: finalizedInvoice, env, @@ -164,7 +155,6 @@ stripeWebhookRouter.post( const canceledSchedule = event.data.object; await handleSubscriptionScheduleCanceled({ db, - sb: request.sb, org, env, schedule: canceledSchedule, @@ -175,7 +165,6 @@ stripeWebhookRouter.post( case "customer.discount.deleted": await handleCusDiscountDeleted({ db, - sb: request.sb, org, discount: event.data.object, env, diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts index d727b753f..1d1d39bc5 100644 --- a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts +++ b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts @@ -104,20 +104,18 @@ export const itemMetasToOptions = async ({ export const handleCheckoutSessionCompleted = async ({ db, - sb, org, checkoutSession, env, logger, }: { db: DrizzleCli; - sb: SupabaseClient; org: Organization; checkoutSession: Stripe.Checkout.Session; env: AppEnv; logger: any; }) => { - const metadata = await getMetadataFromCheckoutSession(checkoutSession, sb); + const metadata = await getMetadataFromCheckoutSession(checkoutSession, db); if (!metadata) { console.log("checkout.completed: metadata not found, skipping"); return; @@ -176,7 +174,7 @@ export const handleCheckoutSessionCompleted = async ({ // 1. Insert sub into db await SubService.createSub({ - sb, + db, sub: { id: generateId("sub"), created_at: Date.now(), @@ -282,7 +280,7 @@ export const handleCheckoutSessionCompleted = async ({ ); const subscription = (await createStripeSub({ - sb, + db, stripeCli, customer: attachParams.customer, org, @@ -308,7 +306,6 @@ export const handleCheckoutSessionCompleted = async ({ let isOneOff = pricesOnlyOneOff(pricesForProduct); await createFullCusProduct({ db, - sb, attachParams: attachToInsertParams(attachParams, product), subscriptionId: !isOneOff ? (checkoutSession.subscription as string) @@ -339,7 +336,7 @@ export const handleCheckoutSessionCompleted = async ({ }); await InvoiceService.createInvoiceFromStripe({ - sb, + db, org, stripeInvoice: invoice, internalCustomerId: attachParams.customer.internal_id, diff --git a/server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts b/server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts index 32e96bfca..5b39aed8b 100644 --- a/server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts +++ b/server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts @@ -1,14 +1,12 @@ import { CusService } from "@/internal/customers/CusService.js"; import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; import { createStripeCli } from "../utils.js"; -import { SupabaseClient } from "@supabase/supabase-js"; import Stripe from "stripe"; import { notNullish, timeout } from "@/utils/genUtils.js"; import { DrizzleCli } from "@/db/initDrizzle.js"; export async function handleCusDiscountDeleted({ db, - sb, org, discount, env, @@ -16,7 +14,6 @@ export async function handleCusDiscountDeleted({ res, }: { db: DrizzleCli; - sb: any; org: any; discount: any; env: any; @@ -40,14 +37,11 @@ export async function handleCusDiscountDeleted({ // Check if any redemptions available, and apply to customer if so let redemptions = await RewardRedemptionService.getUnappliedRedemptions({ - sb, + db, internalCustomerId: customer.internal_id, }); if (redemptions.length == 0) { - // logger.info( - // `discount.deleted: no redemptions available for customer ${customer.id}` - // ); return; } @@ -84,7 +78,7 @@ export async function handleCusDiscountDeleted({ }); await RewardRedemptionService.update({ - sb, + db, id: redemption.id, updates: { applied: true, diff --git a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated.ts b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated.ts index c5867d109..0a1825884 100644 --- a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated.ts +++ b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated.ts @@ -37,7 +37,6 @@ import { DrizzleCli } from "@/db/initDrizzle.js"; const handleInArrearProrated = async ({ db, - sb, cusEnts, cusPrice, customer, @@ -45,11 +44,10 @@ const handleInArrearProrated = async ({ env, invoice, usageSub, - pg, logger, }: { db: DrizzleCli; - sb: SupabaseClient; + cusEnts: FullCustomerEntitlement[]; cusPrice: FullCustomerPrice; customer: Customer; @@ -57,7 +55,6 @@ const handleInArrearProrated = async ({ env: AppEnv; invoice: Stripe.Invoice; usageSub: Stripe.Subscription; - pg: any; logger: any; }) => { const cusEnt = getRelatedCusEnt({ @@ -163,15 +160,16 @@ const handleInArrearProrated = async ({ // Increase balance if (notNullish(cusEnt.balance)) { logger.info(`Incrementing balance for cus ent: ${cusEnt.id}`); - await pg.query( - `UPDATE customer_entitlements SET balance = balance + ${deletedEntities.length} WHERE id = '${cusEnt.id}'`, - ); + await CusEntService.increment({ + db, + id: cusEnt.id, + amount: deletedEntities.length, + }); } }; const handleUsageInArrear = async ({ db, - sb, invoice, customer, relatedCusEnt, @@ -182,7 +180,6 @@ const handleUsageInArrear = async ({ activeProduct, }: { db: DrizzleCli; - sb: SupabaseClient; invoice: Stripe.Invoice; customer: Customer; relatedCusEnt: FullCustomerEntitlement; @@ -307,24 +304,20 @@ const handleUsageInArrear = async ({ export const sendUsageAndReset = async ({ db, - sb, activeProduct, org, env, invoice, stripeSubs, logger, - pg, }: { db: DrizzleCli; - sb: SupabaseClient; activeProduct: FullCusProduct; org: Organization; env: AppEnv; invoice: Stripe.Invoice; stripeSubs: Stripe.Subscription[]; logger: any; - pg: Client; }) => { const fullCusProduct = await CusProductService.get({ db, @@ -367,7 +360,7 @@ export const sendUsageAndReset = async ({ } let usageBasedSub = await getUsageBasedSub({ - sb: sb, + db, stripeCli, subIds: activeProduct.subscription_ids || [], feature: relatedCusEnt.entitlement.feature, @@ -393,7 +386,6 @@ export const sendUsageAndReset = async ({ await handleUsageInArrear({ db, - sb, invoice, customer, relatedCusEnt, @@ -408,7 +400,6 @@ export const sendUsageAndReset = async ({ if (billingType == BillingType.InArrearProrated) { await handleInArrearProrated({ db, - sb, cusEnts, cusPrice, customer, @@ -417,7 +408,6 @@ export const sendUsageAndReset = async ({ invoice, usageSub: usageBasedSub, logger, - pg, }); } } @@ -444,16 +434,12 @@ const invoiceCusProductCreatedDifference = ({ export const handleInvoiceCreated = async ({ db, - pg, - sb, org, invoice, env, event, }: { db: DrizzleCli; - pg: Client; - sb: SupabaseClient; org: Organization; invoice: Stripe.Invoice; env: AppEnv; @@ -546,14 +532,12 @@ export const handleInvoiceCreated = async ({ for (const activeProduct of activeProducts) { await sendUsageAndReset({ db, - sb, activeProduct, org, env, stripeSubs, invoice, logger, - pg, }); } } diff --git a/server/src/external/stripe/webhookHandlers/handleInvoiceFinalized.ts b/server/src/external/stripe/webhookHandlers/handleInvoiceFinalized.ts index e675d35e9..5e74cae9d 100644 --- a/server/src/external/stripe/webhookHandlers/handleInvoiceFinalized.ts +++ b/server/src/external/stripe/webhookHandlers/handleInvoiceFinalized.ts @@ -21,7 +21,6 @@ import { DrizzleCli } from "@/db/initDrizzle.js"; export const handleInvoiceFinalized = async ({ db, - sb, org, invoice, env, @@ -29,7 +28,6 @@ export const handleInvoiceFinalized = async ({ logger, }: { db: DrizzleCli; - sb: SupabaseClient; org: Organization; invoice: Stripe.Invoice; env: AppEnv; @@ -57,7 +55,7 @@ export const handleInvoiceFinalized = async ({ } const updated = await updateInvoiceIfExists({ - sb, + db, invoice, }); @@ -76,7 +74,7 @@ export const handleInvoiceFinalized = async ({ }); await InvoiceService.createInvoiceFromStripe({ - sb, + db, stripeInvoice: expandedInvoice, internalCustomerId: activeProducts[0].internal_customer_id, productIds: activeProducts.map((p) => p.product.id), diff --git a/server/src/external/stripe/webhookHandlers/handleInvoicePaid.ts b/server/src/external/stripe/webhookHandlers/handleInvoicePaid.ts index 65cd8e6ed..2566e742f 100644 --- a/server/src/external/stripe/webhookHandlers/handleInvoicePaid.ts +++ b/server/src/external/stripe/webhookHandlers/handleInvoicePaid.ts @@ -28,19 +28,19 @@ import { getInvoiceItems } from "@/internal/customers/invoices/invoiceUtils.js"; import { DrizzleCli } from "@/db/initDrizzle.js"; const handleOneOffInvoicePaid = async ({ - sb, + db, stripeInvoice, logger, }: { - sb: SupabaseClient; + db: DrizzleCli; stripeInvoice: Stripe.Invoice; event: Stripe.Event; logger: any; }) => { // Search for invoice - const invoice = await InvoiceService.getInvoiceByStripeId({ - sb, - stripeInvoiceId: stripeInvoice.id, + const invoice = await InvoiceService.getByStripeId({ + db, + stripeId: stripeInvoice.id, }); if (!invoice) { @@ -50,8 +50,8 @@ const handleOneOffInvoicePaid = async ({ // Update invoice status await InvoiceService.updateByStripeId({ - sb, - stripeInvoiceId: stripeInvoice.id, + db, + stripeId: stripeInvoice.id, updates: { status: stripeInvoice.status as InvoiceStatus, hosted_invoice_url: stripeInvoice.hosted_invoice_url, @@ -66,14 +66,12 @@ const handleOneOffInvoicePaid = async ({ }; const convertToChargeAutomatically = async ({ - sb, org, env, invoice, activeCusProducts, logger, }: { - sb: SupabaseClient; org: Organization; env: AppEnv; invoice: Stripe.Invoice; @@ -141,7 +139,6 @@ const convertToChargeAutomatically = async ({ export const handleInvoicePaid = async ({ db, req, - sb, org, invoice, env, @@ -149,7 +146,6 @@ export const handleInvoicePaid = async ({ }: { db: DrizzleCli; req: any; - sb: SupabaseClient; org: Organization; invoice: Stripe.Invoice; env: AppEnv; @@ -163,7 +159,7 @@ export const handleInvoicePaid = async ({ }); await handleInvoicePaidDiscount({ - sb, + db, expandedInvoice, org, env, @@ -186,11 +182,11 @@ export const handleInvoicePaid = async ({ `invoice.paid: customer product not found for invoice ${invoice.id}`, ); } + return; } if (org.config.convert_to_charge_automatically) { await convertToChargeAutomatically({ - sb, org, env, invoice, @@ -200,7 +196,7 @@ export const handleInvoicePaid = async ({ } let updated = await updateInvoiceIfExists({ - sb, + db, invoice, }); @@ -212,8 +208,9 @@ export const handleInvoicePaid = async ({ ), logger, }); + await InvoiceService.createInvoiceFromStripe({ - sb, + db, stripeInvoice: expandedInvoice, internalCustomerId: activeCusProducts[0].internal_customer_id, internalEntityId: activeCusProducts[0].internal_entity_id, @@ -243,7 +240,7 @@ export const handleInvoicePaid = async ({ } } else { await handleOneOffInvoicePaid({ - sb, + db, stripeInvoice: expandedInvoice, event, logger, @@ -252,13 +249,13 @@ export const handleInvoicePaid = async ({ }; const handleInvoicePaidDiscount = async ({ - sb, + db, expandedInvoice, org, env, logger, }: { - sb: SupabaseClient; + db: DrizzleCli; expandedInvoice: Stripe.Invoice; org: Organization; env: AppEnv; @@ -293,9 +290,9 @@ const handleInvoicePaidDiscount = async ({ // 1. Fetch coupon from Autumn logger.info(`Fetching coupon from Autumn DB: ${couponId}`); - const autumnReward: Reward | null = await RewardService.getById({ - sb, - id: couponId, + const autumnReward: Reward | null = await RewardService.get({ + db, + idOrInternalId: couponId, orgId: org.id, env, }); diff --git a/server/src/external/stripe/webhookHandlers/handleSubCreated.ts b/server/src/external/stripe/webhookHandlers/handleSubCreated.ts index c5002d37c..bdcb133b2 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubCreated.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubCreated.ts @@ -20,14 +20,12 @@ import { DrizzleCli } from "@/db/initDrizzle.js"; export const handleSubCreated = async ({ db, - sb, subscription, org, env, logger, }: { db: DrizzleCli; - sb: SupabaseClient; subscription: Stripe.Subscription; org: Organization; env: AppEnv; @@ -48,13 +46,13 @@ export const handleSubCreated = async ({ // Update autumn sub let autumnSub = await SubService.getFromScheduleId({ - sb, + db, scheduleId: subscription.schedule as string, }); if (autumnSub) { await SubService.updateFromScheduleId({ - sb, + db, scheduleId: subscription.schedule as string, updates: { stripe_id: subscription.id, @@ -74,7 +72,7 @@ export const handleSubCreated = async ({ } await SubService.createSub({ - sb, + db, sub: { id: generateId("sub"), created_at: Date.now(), @@ -102,13 +100,13 @@ export const handleSubCreated = async ({ subIds.push(subscription.id); const updateCusProd = async () => { - await sb - .from("customer_products") - .update({ + await CusProductService.update({ + db, + cusProductId: cusProd.id, + updates: { subscription_ids: subIds, - // status: CusProductStatus.Active, - }) - .eq("id", cusProd.id); + }, + }); // Fetch latest invoice? const stripeCli = createStripeCli({ org, env }); @@ -126,7 +124,7 @@ export const handleSubCreated = async ({ }); await InvoiceService.createInvoiceFromStripe({ - sb, + db, stripeInvoice: invoice, internalCustomerId: cusProd.internal_customer_id, internalEntityId: cusProd.internal_entity_id, diff --git a/server/src/external/stripe/webhookHandlers/handleSubDeleted.ts b/server/src/external/stripe/webhookHandlers/handleSubDeleted.ts index b60b17d0c..0e8bfa9d9 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubDeleted.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubDeleted.ts @@ -34,7 +34,6 @@ const handleCusProductDeleted = async ({ logger, env, org, - sb, prematurelyCanceled, }: { db: DrizzleCli; @@ -43,7 +42,6 @@ const handleCusProductDeleted = async ({ logger: any; env: AppEnv; org: Organization; - sb: SupabaseClient; prematurelyCanceled: boolean; }) => { if ( @@ -74,7 +72,6 @@ const handleCusProductDeleted = async ({ ); await billForRemainingUsages({ db, - sb, curCusProduct: cusProduct, logger, attachParams: { @@ -155,7 +152,6 @@ const handleCusProductDeleted = async ({ const activatedFuture = await activateFutureProduct({ db, - sb, cusProduct, subscription, org, @@ -185,13 +181,11 @@ const handleCusProductDeleted = async ({ productGroup: cusProduct.product.group, customer: cusProduct.customer, org, - sb, env, curCusProduct: curMainProduct || undefined, }); await cancelCusProductSubscriptions({ - sb, cusProduct, org, env, @@ -201,14 +195,12 @@ const handleCusProductDeleted = async ({ export const handleSubscriptionDeleted = async ({ db, - sb, subscription, org, env, logger, }: { db: DrizzleCli; - sb: SupabaseClient; subscription: Stripe.Subscription; org: Organization; env: AppEnv; @@ -251,7 +243,6 @@ export const handleSubscriptionDeleted = async ({ logger, env, org, - sb, prematurelyCanceled, }), ); diff --git a/server/src/external/stripe/webhookHandlers/handleSubScheduleCanceled.ts b/server/src/external/stripe/webhookHandlers/handleSubScheduleCanceled.ts index e5cbae588..150676b57 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubScheduleCanceled.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubScheduleCanceled.ts @@ -1,5 +1,3 @@ -import { SupabaseClient } from "@supabase/supabase-js"; - import { AppEnv } from "@autumn/shared"; import Stripe from "stripe"; import { CusProductStatus, Organization } from "@autumn/shared"; @@ -10,14 +8,12 @@ import { DrizzleCli } from "@/db/initDrizzle.js"; export const handleSubscriptionScheduleCanceled = async ({ db, - sb, schedule, env, org, logger, }: { db: DrizzleCli; - sb: SupabaseClient; schedule: Stripe.SubscriptionSchedule; org: Organization; env: AppEnv; @@ -81,13 +77,13 @@ export const handleSubscriptionScheduleCanceled = async ({ // Delete from subscriptions try { let autumnSub = await SubService.getFromScheduleId({ - sb, + db, scheduleId: schedule.id, }); if (autumnSub && !autumnSub.stripe_id) { await SubService.deleteFromScheduleId({ - sb, + db, scheduleId: schedule.id, }); } diff --git a/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts b/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts index 186aac526..0ffb21953 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts @@ -16,7 +16,6 @@ import { formatUnixToDateTime, notNullish, nullish } from "@/utils/genUtils.js"; import { ProductService } from "@/internal/products/ProductService.js"; import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js"; import { cancelFutureProductSchedule } from "@/internal/customers/change-product/scheduleUtils.js"; -import { CusService } from "@/internal/customers/CusService.js"; import { getExistingCusProducts } from "@/internal/customers/add-product/handleExistingProduct.js"; import { getWebhookLock, @@ -29,7 +28,6 @@ import { DrizzleCli } from "@/db/initDrizzle.js"; export const handleSubscriptionUpdated = async ({ db, - sb, org, subscription, previousAttributes, @@ -37,7 +35,6 @@ export const handleSubscriptionUpdated = async ({ logger, }: { db: DrizzleCli; - sb: any; org: Organization; env: AppEnv; subscription: any; @@ -182,7 +179,6 @@ export const handleSubscriptionUpdated = async ({ } await createFullCusProduct({ db, - sb, attachParams: { customer: updatedCusProducts[0].customer, product, @@ -254,7 +250,6 @@ export const handleSubscriptionUpdated = async ({ }); await cancelFutureProductSchedule({ db, - sb, org, stripeCli, cusProducts: allCusProducts, @@ -299,7 +294,7 @@ export const handleSubscriptionUpdated = async ({ try { await SubService.updateFromStripe({ - sb, + db, stripeSub: fullSub, }); } catch (error) { diff --git a/server/src/external/svix/handleProductsUpdatedWebhook.ts b/server/src/external/svix/handleProductsUpdatedWebhook.ts index 301ffeaa8..999ae5a19 100644 --- a/server/src/external/svix/handleProductsUpdatedWebhook.ts +++ b/server/src/external/svix/handleProductsUpdatedWebhook.ts @@ -1,32 +1,26 @@ import { AppEnv, CusProductStatus, - CusResponseSchema, Entitlement, + ErrCode, FreeTrial, FullProduct, Organization, Price, Product, - ProductResponseSchema, } from "@autumn/shared"; import { sendSvixEvent } from "./svixUtils.js"; import { CusService } from "@/internal/customers/CusService.js"; -import { SupabaseClient } from "@supabase/supabase-js"; + import { getCustomerDetails } from "@/internal/api/customers/getCustomerDetails.js"; import { FeatureService } from "@/internal/features/FeatureService.js"; -import { z } from "zod"; + import { getProductResponse } from "@/internal/products/productV2Utils.js"; import { addTaskToQueue } from "@/queue/queueUtils.js"; import { JobName } from "@/queue/JobName.js"; import { DrizzleCli } from "@/db/initDrizzle.js"; - -const ProductsUpdatedWebhookSchema = z.object({ - scenario: z.string(), - product: ProductResponseSchema, - customer: CusResponseSchema, -}); +import RecaseError from "@/utils/errorUtils.js"; export const addProductsUpdatedWebhookTask = async ({ internalCustomerId, @@ -116,12 +110,10 @@ export const constructProductsUpdatedData = ({ export const sendProductsUpdatedWebhook = async ({ db, - sb, logger, data, }: { db: DrizzleCli; - sb: SupabaseClient; logger: any; data: { internalCustomerId: string; @@ -134,8 +126,8 @@ export const sendProductsUpdatedWebhook = async ({ }) => { const { org, env, product, scenario } = data; - let customer = await CusService.getWithProducts({ - sb, + let customer = await CusService.getFull({ + db, idOrInternalId: data.customerId || data.internalCustomerId, orgId: data.org.id, env: data.env, @@ -153,10 +145,10 @@ export const sendProductsUpdatedWebhook = async ({ }); const cusDetails = await getCustomerDetails({ + db, customer: customer, org, env, - sb, features, logger, cusProducts: customer.customer_products, diff --git a/server/src/internal/api/components/componentRouter.ts b/server/src/internal/api/components/componentRouter.ts index bdd978a59..4c74d1533 100644 --- a/server/src/internal/api/components/componentRouter.ts +++ b/server/src/internal/api/components/componentRouter.ts @@ -19,7 +19,7 @@ componentRouter.get("/pricing_table", async (req: any, res) => res, action: "get pricing table", handler: async () => { - const { sb, orgId, env, db, logtail: logger } = req; + const { orgId, env, db } = req; let customerId = req.query.customer_id; const [org, features, products, customer] = await Promise.all([ diff --git a/server/src/internal/api/customers/cusRouter.ts b/server/src/internal/api/customers/cusRouter.ts index 16fc8ddda..60ebc23e6 100644 --- a/server/src/internal/api/customers/cusRouter.ts +++ b/server/src/internal/api/customers/cusRouter.ts @@ -19,6 +19,8 @@ import { entityRouter } from "../entities/entityRouter.js"; import { handleUpdateCustomer } from "./handlers/handleUpdateCustomer.js"; import { handleCreateBillingPortal } from "./handlers/handleCreateBillingPortal.js"; import { handleGetCustomer } from "./handlers/handleGetCustomer.js"; +import { CusSearchService } from "@/internal/customers/CusSearchService.js"; +import assert from "assert"; export const cusRouter = Router(); @@ -26,19 +28,22 @@ cusRouter.post("/all/search", async (req: any, res: any) => { try { const { search, page_size = 50, page = 1, last_item, filters } = req.body; - const { data: customers, count } = await CusService.searchCustomers({ - sb: req.sb, + const searchStart1 = Date.now(); + const searchStart2 = Date.now(); + const { data: customers, count } = await CusSearchService.search({ + db: req.db, orgId: req.orgId, env: req.env, search, filters, lastItem: last_item, - pg: req.pg, pageNumber: page, pageSize: page_size, }); - res.status(200).json({ customers, totalCount: count }); + // let totalCount = Number(count) + page_size * (page - 1); + + res.status(200).json({ customers, totalCount: Number(count) }); } catch (error) { handleRequestError({ req, error, res, action: "search customers" }); } diff --git a/server/src/internal/api/customers/cusUtils.ts b/server/src/internal/api/customers/cusUtils.ts index 46575a57d..66f308458 100644 --- a/server/src/internal/api/customers/cusUtils.ts +++ b/server/src/internal/api/customers/cusUtils.ts @@ -9,6 +9,7 @@ import { ErrCode, Feature, FullCustomer, + Invoice, InvoiceResponse, Organization, ProductSchema, @@ -80,21 +81,20 @@ export const flipProductResults = ( }; export const getCusInvoices = async ({ - sb, + db, internalCustomerId, limit = 10, withItems = false, features, }: { - sb: SupabaseClient; + db: DrizzleCli; internalCustomerId: string; limit?: number; withItems?: boolean; features?: Feature[]; }): Promise => { - // Get customer invoices - const invoices = await InvoiceService.getByInternalCustomerId({ - sb, + const invoices = await InvoiceService.list({ + db, internalCustomerId, limit, }); @@ -148,13 +148,11 @@ export const processFullCusProducts = ({ // IMPORTANT FUNCTION export const getCusEntsInFeatures = async ({ - sb, customer, internalFeatureIds, logger, reverseOrder = false, }: { - sb: SupabaseClient; customer: FullCustomer; internalFeatureIds?: string[]; logger: any; diff --git a/server/src/internal/api/customers/getCustomerDetails.ts b/server/src/internal/api/customers/getCustomerDetails.ts index f640a7abc..d63319ad6 100644 --- a/server/src/internal/api/customers/getCustomerDetails.ts +++ b/server/src/internal/api/customers/getCustomerDetails.ts @@ -36,10 +36,11 @@ import { getCusInvoices, processFullCusProducts } from "./cusUtils.js"; import { invoicesToResponse } from "@/internal/customers/invoices/invoiceUtils.js"; import Stripe from "stripe"; import { orgToVersion } from "@/utils/versionUtils.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; export const sumValues = ( entList: CusEntResponse[], - key: keyof CusEntResponse + key: keyof CusEntResponse, ) => { return entList.reduce((acc, curr) => { if (curr[key]) { @@ -122,9 +123,9 @@ export const featuresToObject = ({ }; export const getCustomerDetails = async ({ + db, customer, features, - sb, org, env, params = {}, @@ -133,9 +134,9 @@ export const getCustomerDetails = async ({ expand, reqApiVersion, }: { + db: DrizzleCli; customer: FullCustomer; features: Feature[]; - sb: SupabaseClient; org: Organization; env: AppEnv; params?: any; @@ -166,7 +167,7 @@ export const getCustomerDetails = async ({ }); let subIds = cusProducts.flatMap( - (cp: FullCusProduct) => cp.subscription_ids || [] + (cp: FullCusProduct) => cp.subscription_ids || [], ); if (org.config.api_version >= BREAK_API_VERSION && org.stripe_connected) { @@ -225,7 +226,7 @@ export const getCustomerDetails = async ({ const [stripeCus, subsResult] = await Promise.all([ stripeCli.customers.retrieve( - customer.processor?.id! + customer.processor?.id!, ) as Promise, !subs ? getStripeSubs({ @@ -241,7 +242,7 @@ export const getCustomerDetails = async ({ } let stripeDiscounts: Stripe.Discount[] = subs?.flatMap( - (s) => s.discounts + (s) => s.discounts, ) as Stripe.Discount[]; if (stripeCus.discount) { @@ -313,7 +314,7 @@ export const getCustomerDetails = async ({ let withItems = org.config.api_version >= BREAK_API_VERSION; const processedInvoices = await getCusInvoices({ - sb, + db, internalCustomerId: customer.internal_id, limit: 20, withItems, diff --git a/server/src/internal/api/customers/handlers/cusDeleteHandlers.ts b/server/src/internal/api/customers/handlers/cusDeleteHandlers.ts index 1f9891ac1..bf2747c52 100644 --- a/server/src/internal/api/customers/handlers/cusDeleteHandlers.ts +++ b/server/src/internal/api/customers/handlers/cusDeleteHandlers.ts @@ -1,18 +1,16 @@ import { DrizzleCli } from "@/db/initDrizzle.js"; import { deleteStripeCustomer } from "@/external/stripe/stripeCusUtils.js"; import { CusService } from "@/internal/customers/CusService.js"; -import { OrgService } from "@/internal/orgs/OrgService.js"; + import RecaseError from "@/utils/errorUtils.js"; import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js"; import { routeHandler } from "@/utils/routerUtils.js"; -import { AppEnv, ErrCode, MinOrg, Organization } from "@autumn/shared"; -import { SupabaseClient } from "@supabase/supabase-js"; +import { AppEnv, ErrCode, Organization } from "@autumn/shared"; import chalk from "chalk"; import { StatusCodes } from "http-status-codes"; export const deleteCusById = async ({ db, - sb, org, customerId, env, @@ -20,7 +18,6 @@ export const deleteCusById = async ({ deleteInStripe = false, }: { db: DrizzleCli; - sb: SupabaseClient; org: Organization; customerId: string; env: AppEnv; @@ -86,11 +83,10 @@ export const handleDeleteCustomer = async (req: any, res: any) => res, action: "delete customer", handler: async (req: ExtendedRequest, res: ExtendedResponse) => { - const { env, logtail: logger, db, sb, org } = req; + const { env, logtail: logger, db, org } = req; const data = await deleteCusById({ db, - sb, org, customerId: req.params.customer_id, env, diff --git a/server/src/internal/api/customers/handlers/handleAddCouponToCus.ts b/server/src/internal/api/customers/handlers/handleAddCouponToCus.ts index 56c759dc7..19cc99235 100644 --- a/server/src/internal/api/customers/handlers/handleAddCouponToCus.ts +++ b/server/src/internal/api/customers/handlers/handleAddCouponToCus.ts @@ -10,7 +10,7 @@ import { StatusCodes } from "http-status-codes"; export const handleAddCouponToCus = async (req: any, res: any) => { try { const { customer_id, coupon_id } = req.params; - const { db, orgId, env, sb, logtail: logger } = req; + const { db, orgId, env, logtail: logger } = req; const [org, customer, coupon] = await Promise.all([ OrgService.getFromReq(req), @@ -20,9 +20,9 @@ export const handleAddCouponToCus = async (req: any, res: any) => { orgId, env, }), - RewardService.getByInternalId({ - sb: req.sb, - internalId: coupon_id, + RewardService.get({ + db, + idOrInternalId: coupon_id, orgId: req.orgId, env: req.env, }), @@ -36,6 +36,14 @@ export const handleAddCouponToCus = async (req: any, res: any) => { }); } + if (!coupon) { + throw new RecaseError({ + message: `Coupon ${coupon_id} not found`, + code: ErrCode.RewardNotFound, + statusCode: StatusCodes.NOT_FOUND, + }); + } + const stripeCli = createStripeCli({ org, env }); await createStripeCusIfNotExists({ diff --git a/server/src/internal/api/customers/handlers/handleCreateCustomer.ts b/server/src/internal/api/customers/handlers/handleCreateCustomer.ts index b573f4ac5..a7b96ed80 100644 --- a/server/src/internal/api/customers/handlers/handleCreateCustomer.ts +++ b/server/src/internal/api/customers/handlers/handleCreateCustomer.ts @@ -75,7 +75,6 @@ export const initStripeCusAndProducts = async ({ export const createNewCustomer = async ({ db, - sb, org, env, customer, @@ -85,7 +84,6 @@ export const createNewCustomer = async ({ createDefaultProducts = true, }: { db: DrizzleCli; - sb: SupabaseClient; org: Organization; env: AppEnv; customer: CreateCustomer; @@ -169,7 +167,6 @@ export const createNewCustomer = async ({ await handleAddProduct({ req: { db, - sb, logtail: logger, }, res: {}, @@ -193,7 +190,6 @@ export const createNewCustomer = async ({ for (const product of freeProds) { await createFullCusProduct({ db, - sb, attachParams: { org, customer: newCustomer, @@ -219,7 +215,6 @@ export const createNewCustomer = async ({ const handleIdIsNull = async ({ db, - sb, org, env, newCus, @@ -228,7 +223,6 @@ const handleIdIsNull = async ({ createDefaultProducts, }: { db: DrizzleCli; - sb: SupabaseClient; org: Organization; env: AppEnv; newCus: CreateCustomer; @@ -273,7 +267,6 @@ const handleIdIsNull = async ({ const createdCustomer = await createNewCustomer({ db, - sb, org, env, customer: newCus, @@ -288,7 +281,6 @@ const handleIdIsNull = async ({ // CAN ALSO USE DURING MIGRATION... export const handleCreateCustomerWithId = async ({ db, - sb, org, env, logger, @@ -297,7 +289,6 @@ export const handleCreateCustomerWithId = async ({ createDefaultProducts = true, }: { db: DrizzleCli; - sb: SupabaseClient; org: Organization; env: AppEnv; logger: any; @@ -351,7 +342,6 @@ export const handleCreateCustomerWithId = async ({ // 2. Handle email step... return await createNewCustomer({ db, - sb, org, env, customer: newCus, @@ -364,7 +354,6 @@ export const handleCreateCustomerWithId = async ({ export const handleCreateCustomer = async ({ db, cusData, - sb, org, env, logger, @@ -373,7 +362,6 @@ export const handleCreateCustomer = async ({ }: { db: DrizzleCli; cusData: CreateCustomer; - sb: SupabaseClient; org: Organization; env: AppEnv; logger: any; @@ -387,7 +375,6 @@ export const handleCreateCustomer = async ({ if (newCus.id === null) { createdCustomer = await handleIdIsNull({ db, - sb, org, env, newCus, @@ -398,7 +385,6 @@ export const handleCreateCustomer = async ({ } else { createdCustomer = await handleCreateCustomerWithId({ db, - sb, org, env, logger, @@ -414,7 +400,7 @@ export const handleCreateCustomer = async ({ export const handlePostCustomerRequest = async (req: any, res: any) => { const logger = req.logtail; try { - const { db, sb } = req; + const { db } = req; const data = req.body; const expand = parseCusExpand(req.query.expand); @@ -430,7 +416,6 @@ export const handlePostCustomerRequest = async (req: any, res: any) => { let features = await FeatureService.getFromReq(req); let customer = await getOrCreateCustomer({ db, - sb, org, env: req.env, customerId: data.id, @@ -449,8 +434,8 @@ export const handlePostCustomerRequest = async (req: any, res: any) => { }); let cusDetails = await getCustomerDetails({ + db, customer, - sb: req.sb, org, env: req.env, params: req.query, diff --git a/server/src/internal/api/customers/handlers/handleCusProductExpired.ts b/server/src/internal/api/customers/handlers/handleCusProductExpired.ts index bc7eb1e87..dc3a62ca0 100644 --- a/server/src/internal/api/customers/handlers/handleCusProductExpired.ts +++ b/server/src/internal/api/customers/handlers/handleCusProductExpired.ts @@ -18,15 +18,12 @@ import { FullCusProduct, Organization, AppEnv, - FullCustomer, Customer, } from "@autumn/shared"; -import { SupabaseClient } from "@supabase/supabase-js"; import { StatusCodes } from "http-status-codes"; export const removeScheduledProduct = async ({ db, - sb, cusProduct, cusProducts, org, @@ -35,7 +32,6 @@ export const removeScheduledProduct = async ({ renewCurProduct = true, }: { db: DrizzleCli; - sb: SupabaseClient; cusProduct: FullCusProduct; cusProducts: FullCusProduct[]; org: Organization; @@ -51,7 +47,6 @@ export const removeScheduledProduct = async ({ // 1. Cancel future product schedule await cancelFutureProductSchedule({ db, - sb, org, cusProducts, product: fullProduct, @@ -72,7 +67,6 @@ export const removeScheduledProduct = async ({ export const expireCusProduct = async ({ db, - sb, cusProduct, // cus product to expire cusProducts, // other cus products org, @@ -82,7 +76,6 @@ export const expireCusProduct = async ({ expireImmediately = true, }: { db: DrizzleCli; - sb: SupabaseClient; cusProduct: FullCusProduct; cusProducts: FullCusProduct[]; org: Organization; @@ -107,7 +100,6 @@ export const expireCusProduct = async ({ if (cusProduct.status == CusProductStatus.Scheduled) { await removeScheduledProduct({ db, - sb, cusProduct, cusProducts, org, @@ -142,7 +134,6 @@ export const expireCusProduct = async ({ // 2. If expire at cycle end, just cancel subscriptions if (!expireImmediately) { await cancelCusProductSubscriptions({ - sb, cusProduct, org, env, @@ -162,7 +153,6 @@ export const expireCusProduct = async ({ if (cusProduct.product.is_add_on) { await cancelCusProductSubscriptions({ - sb, cusProduct, org, env, @@ -183,7 +173,6 @@ export const expireCusProduct = async ({ // For regular products // 1. Cancel stripe subscriptions const cancelled = await cancelCusProductSubscriptions({ - sb, cusProduct, org, env, @@ -192,7 +181,6 @@ export const expireCusProduct = async ({ if (!cancelled) { await expireAndActivate({ db, - sb, env, cusProduct, org, @@ -204,7 +192,7 @@ export const expireCusProduct = async ({ export const handleCusProductExpired = async (req: any, res: any) => { try { - const { db, sb, logtail: logger } = req; + const { db, logtail: logger } = req; const org = await OrgService.getFromReq(req); const customerProductId = req.params.customer_product_id; @@ -237,7 +225,6 @@ export const handleCusProductExpired = async (req: any, res: any) => { await expireCusProduct({ db, - sb, cusProduct, cusProducts, org, diff --git a/server/src/internal/api/customers/handlers/handleGetCustomer.ts b/server/src/internal/api/customers/handlers/handleGetCustomer.ts index e59e07662..b3ddcd812 100644 --- a/server/src/internal/api/customers/handlers/handleGetCustomer.ts +++ b/server/src/internal/api/customers/handlers/handleGetCustomer.ts @@ -14,7 +14,7 @@ export const handleGetCustomer = async (req: any, res: any) => action: "get customer", handler: async () => { let customerId = req.params.customer_id; - let { orgId, env } = req; + let { orgId, env, db } = req; let { expand } = req.query; let expandArray = parseCusExpand(expand); @@ -22,8 +22,8 @@ export const handleGetCustomer = async (req: any, res: any) => const [features, org, customer] = await Promise.all([ FeatureService.getFromReq(req), OrgService.getFromReq(req), - CusService.getWithProducts({ - sb: req.sb, + CusService.getFull({ + db, idOrInternalId: customerId, orgId: orgId, env: env, @@ -34,6 +34,7 @@ export const handleGetCustomer = async (req: any, res: any) => ], withEntities: true, expand: expandArray, + allowNotFound: true, }), ]); @@ -49,8 +50,8 @@ export const handleGetCustomer = async (req: any, res: any) => } let cusData = await getCustomerDetails({ + db, customer, - sb: req.sb, org, env: req.env, logger: req.logtail, diff --git a/server/src/internal/api/customers/handlers/handleUpdateBalances.ts b/server/src/internal/api/customers/handlers/handleUpdateBalances.ts index cb42c9ac3..d7c6853bf 100644 --- a/server/src/internal/api/customers/handlers/handleUpdateBalances.ts +++ b/server/src/internal/api/customers/handlers/handleUpdateBalances.ts @@ -23,8 +23,8 @@ import { notNullish } from "@/utils/genUtils.js"; const getCusFeaturesAndOrg = async (req: any, customerId: string) => { // 1. Get customer const [customer, features, org] = await Promise.all([ - CusService.getWithProducts({ - sb: req.sb, + CusService.getFull({ + db: req.db, idOrInternalId: customerId, orgId: req.orgId, env: req.env, @@ -49,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, db } = req; + const { env, db } = req; const { balances } = req.body; if (!Array.isArray(balances)) { @@ -77,7 +77,6 @@ export const handleUpdateBalances = async (req: any, res: any) => { // Can't update feature -> credit system here... const { cusEnts, cusPrices } = await getCusEntsInFeatures({ - sb: req.sb, customer, internalFeatureIds: featuresToUpdate.map((f) => f.internal_id!), logger: req.logtail, @@ -232,7 +231,6 @@ export const handleUpdateBalances = async (req: any, res: any) => { toDeduct, deductParams: { db, - sb, feature: featureDeduction.feature!, env: req.env, org, @@ -256,7 +254,6 @@ export const handleUpdateBalances = async (req: any, res: any) => { cusEnts, deductParams: { db, - sb, feature: featureDeduction.feature!, env, org, diff --git a/server/src/internal/api/customers/handlers/handleUpdateCustomer.ts b/server/src/internal/api/customers/handlers/handleUpdateCustomer.ts index d561d7757..7955f2ca5 100644 --- a/server/src/internal/api/customers/handlers/handleUpdateCustomer.ts +++ b/server/src/internal/api/customers/handlers/handleUpdateCustomer.ts @@ -113,8 +113,8 @@ export const handleUpdateCustomer = async (req: any, res: any) => }, }); - let finalCustomer = await CusService.getWithProducts({ - sb: req.sb, + let finalCustomer = await CusService.getFull({ + db, idOrInternalId: originalCustomer.internal_id, orgId: req.orgId, env: req.env, @@ -123,7 +123,7 @@ export const handleUpdateCustomer = async (req: any, res: any) => // res.status(200).json({ customer: updatedCustomer }); let customerDetails = await getCustomerDetails({ - sb: req.sb, + db, customer: finalCustomer, org, env: req.env, diff --git a/server/src/internal/api/customers/handlers/handleUpdateEntitlement.ts b/server/src/internal/api/customers/handlers/handleUpdateEntitlement.ts index aa97ce0cc..6fa3bfc69 100644 --- a/server/src/internal/api/customers/handlers/handleUpdateEntitlement.ts +++ b/server/src/internal/api/customers/handlers/handleUpdateEntitlement.ts @@ -48,7 +48,7 @@ const getCusOrgAndCusPrice = async ({ export const handleUpdateEntitlement = async (req: any, res: any) => { try { - const { db, sb } = req; + const { db } = req; const { customer_entitlement_id } = req.params; const { balance, next_reset_at, entity_id } = req.body; @@ -134,8 +134,6 @@ export const handleUpdateEntitlement = async (req: any, res: any) => { cusEnt, }); - console.log("Customer", customer); - if (!cusPrice || !customer) { res.status(200).json({ success: true }); return; @@ -143,7 +141,7 @@ export const handleUpdateEntitlement = async (req: any, res: any) => { await adjustAllowance({ db, - sb: req.sb, + env: req.env, org: org, affectedFeature: cusEnt.entitlement.feature, diff --git a/server/src/internal/api/customers/products/attachRouter.ts b/server/src/internal/api/customers/products/attachRouter.ts index aa3bf7ea9..bd5b86dd0 100644 --- a/server/src/internal/api/customers/products/attachRouter.ts +++ b/server/src/internal/api/customers/products/attachRouter.ts @@ -273,11 +273,9 @@ attachRouter.post("/attach", async (req: any, res) => { checkout_session_params, } = req.body; - const { orgId, env } = req; + const { env } = req; const logger = req.logtail; - const sb = req.sb; - let itemsInput: ProductItem[] = items || []; const optionsListInput: FeatureOptions[] = options || []; @@ -307,7 +305,6 @@ attachRouter.post("/attach", async (req: any, res) => { // Get curCusProducts too... const attachParams: AttachParams = await getFullCusProductData({ - sb, db: req.db, customerId: customer_id, productId: product_id, @@ -418,7 +415,7 @@ attachRouter.post("/attach", async (req: any, res) => { if (useCheckout && !newProductsFree && !invoiceOnly) { logger.info("SCENARIO 2: USING CHECKOUT"); await handleCreateCheckout({ - sb, + db: req.db, req, res, attachParams, diff --git a/server/src/internal/api/customers/products/expireRouter.ts b/server/src/internal/api/customers/products/expireRouter.ts index b10d0a7a8..d28ec1091 100644 --- a/server/src/internal/api/customers/products/expireRouter.ts +++ b/server/src/internal/api/customers/products/expireRouter.ts @@ -14,20 +14,21 @@ expireRouter.post("", async (req, res) => res, action: "expire", handler: async (req, res) => { - let { db, sb, orgId, env, logtail: logger } = req; + let { db, orgId, env, logtail: logger } = req; let { customer_id, product_id, entity_id, cancel_immediately } = req.body; let expireImmediately = cancel_immediately || false; let [customer, org] = await Promise.all([ - CusService.getWithProducts({ - sb, + CusService.getFull({ + db, orgId, idOrInternalId: customer_id, env, withEntities: true, entityId: entity_id, inStatuses: [CusProductStatus.Active, CusProductStatus.PastDue], + allowNotFound: false, }), OrgService.getFromReq(req), ]); @@ -57,7 +58,6 @@ expireRouter.post("", async (req, res) => for (const cusProduct of cusProductsToExpire) { await expireCusProduct({ db, - sb, cusProduct, cusProducts, org, diff --git a/server/src/internal/api/entities/EntityService.ts b/server/src/internal/api/entities/EntityService.ts index 716b29864..ea4b2d79f 100644 --- a/server/src/internal/api/entities/EntityService.ts +++ b/server/src/internal/api/entities/EntityService.ts @@ -1,9 +1,7 @@ import { DrizzleCli } from "@/db/initDrizzle.js"; import RecaseError from "@/utils/errorUtils.js"; import { ErrCode } from "@autumn/shared"; -import { Entity } from "@shared/models/cusModels/entityModels/entityModels.js"; -import { entities } from "@shared/models/cusModels/entityModels/entityTable.js"; -import { SupabaseClient } from "@supabase/supabase-js"; +import { Entity, entities } from "@autumn/shared"; import { and, eq, inArray } from "drizzle-orm"; export class EntityService { diff --git a/server/src/internal/api/entities/entityRelations.ts b/server/src/internal/api/entities/entityRelations.ts index ccdc69f57..5ce221c86 100644 --- a/server/src/internal/api/entities/entityRelations.ts +++ b/server/src/internal/api/entities/entityRelations.ts @@ -1,7 +1,5 @@ -import { entities } from "@shared/models/cusModels/entityModels/entityTable.js"; -import { customers } from "@shared/models/cusModels/cusTable.js"; -import { features } from "@shared/models/featureModels/featureTable.js"; -import { organizations } from "@shared/models/orgModels/orgTable.js"; +import { entities, customers, features, organizations } from "@autumn/shared"; + import { relations } from "drizzle-orm"; export const entityRelations = relations(entities, ({ one }) => ({ diff --git a/server/src/internal/api/entities/entityRouter.ts b/server/src/internal/api/entities/entityRouter.ts index 6bf0579c3..560821b34 100644 --- a/server/src/internal/api/entities/entityRouter.ts +++ b/server/src/internal/api/entities/entityRouter.ts @@ -21,21 +21,14 @@ entityRouter.get("", (req, res) => const customerId = req.params.customer_id as string; let { orgId, env } = req; - let customer = await CusService.getWithProducts({ - sb: req.sb, + let customer = await CusService.getFull({ + db: req.db, idOrInternalId: customerId, orgId, env, withEntities: true, }); - if (!customer) { - throw new RecaseError({ - message: `Customer ${customerId} not found`, - code: ErrCode.CustomerNotFound, - }); - } - res.status(200).json({ data: customer.entities, }); diff --git a/server/src/internal/api/entities/getEntityUtils.ts b/server/src/internal/api/entities/getEntityUtils.ts index 7022caf33..0d7108942 100644 --- a/server/src/internal/api/entities/getEntityUtils.ts +++ b/server/src/internal/api/entities/getEntityUtils.ts @@ -1,3 +1,4 @@ +import { DrizzleCli } from "@/db/initDrizzle.js"; import { CusService } from "@/internal/customers/CusService.js"; import { getCusFeaturesResponse, @@ -20,7 +21,7 @@ import { import { SupabaseClient } from "@supabase/supabase-js"; export const getEntityResponse = async ({ - sb, + db, entityIds, org, env, @@ -30,7 +31,7 @@ export const getEntityResponse = async ({ withAutumnId = false, apiVersion, }: { - sb: SupabaseClient; + db: DrizzleCli; entityIds: string[]; org: Organization; env: AppEnv; @@ -40,11 +41,11 @@ export const getEntityResponse = async ({ withAutumnId?: boolean; apiVersion: number; }) => { - let customer = await CusService.getWithProducts({ + let customer = await CusService.getFull({ + db, idOrInternalId: customerId, orgId: org.id, env, - sb, inStatuses: [CusProductStatus.Active, CusProductStatus.PastDue], withEntities: true, withSubs: true, @@ -52,14 +53,6 @@ export const getEntityResponse = async ({ entityId, }); - if (!customer) { - throw new RecaseError({ - message: `Customer ${customerId} not found`, - code: ErrCode.CustomerNotFound, - statusCode: 400, - }); - } - let entities = customer.entities.filter((e: Entity) => entityIds.includes(e.id), ); diff --git a/server/src/internal/api/entities/handlers/handleCreateEntity.ts b/server/src/internal/api/entities/handlers/handleCreateEntity.ts index 3bbc1875f..21de30100 100644 --- a/server/src/internal/api/entities/handlers/handleCreateEntity.ts +++ b/server/src/internal/api/entities/handlers/handleCreateEntity.ts @@ -3,7 +3,7 @@ import { CusEntService } from "@/internal/customers/entitlements/CusEntitlementS import { FeatureService } from "@/internal/features/FeatureService.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; -import RecaseError, { handleRequestError } from "@/utils/errorUtils.js"; +import RecaseError from "@/utils/errorUtils.js"; import { EntityService } from "../EntityService.js"; import { APIVersion, @@ -162,7 +162,7 @@ export const logEntityToAction = ({ }; export const validateAndGetInputEntities = async ({ - sb, + db, orgId, features, customerId, @@ -170,7 +170,7 @@ export const validateAndGetInputEntities = async ({ env, logger, }: { - sb: any; + db: DrizzleCli; orgId: string; features: Feature[]; customerId: string; @@ -179,8 +179,8 @@ export const validateAndGetInputEntities = async ({ logger: any; }) => { // 1. Get customer, features and orgs - let customer = await CusService.getWithProducts({ - sb, + let customer = await CusService.getFull({ + db, idOrInternalId: customerId, orgId, env, @@ -269,7 +269,6 @@ export const validateAndGetInputEntities = async ({ export const createEntities = async ({ db, - sb, env, org, features, @@ -281,7 +280,6 @@ export const createEntities = async ({ fromAutoCreate = false, }: { db: DrizzleCli; - sb: any; org: Organization; features: Feature[]; env: AppEnv; @@ -300,7 +298,7 @@ export const createEntities = async ({ cusProducts, existingEntities, } = await validateAndGetInputEntities({ - sb, + db, customerId, orgId: org.id, env, @@ -370,13 +368,14 @@ export const createEntities = async ({ entities: existingEntities, }); - const originalBalance = mainCusEnt.balance + (unused || 0); + let mainCusEntBalance = mainCusEnt.balance || 0; + const originalBalance = mainCusEntBalance + (unused || 0); const newBalance = - mainCusEnt.balance - (newCount + replacedCount) + (unused || 0); + mainCusEntBalance - (newCount + replacedCount) + (unused || 0); await adjustAllowance({ db, - sb, + env, org, cusPrices: cusProducts.flatMap((p: any) => p.customer_prices), @@ -394,7 +393,7 @@ export const createEntities = async ({ await CusEntService.update({ db, id: mainCusEnt.id, - updates: { balance: mainCusEnt.balance - newCount }, + updates: { balance: mainCusEntBalance - newCount }, }); // await pg.query( @@ -413,15 +412,15 @@ export const createEntities = async ({ if (entityAction.action === "create") { newEntities[entity.id] = { id: entity.id, - balance: allowance, + balance: allowance!, adjustment: 0, }; } else if (entityAction.action === "replace") { let tmp = newEntities[entityAction.replace.id]; delete newEntities[entityAction.replace.id]; newEntities[entity.id] = { - id: entity.id, ...tmp, + id: entity.id, }; } } @@ -473,7 +472,7 @@ export const createEntities = async ({ } let { entities } = await getEntityResponse({ - sb, + db, entityIds: inputEntities.map((e: any) => e.id), org, env, @@ -491,7 +490,7 @@ export const handlePostEntityRequest = async (req: any, res: any) => res, action: "create entity", handler: async (req: any, res: any) => { - const { sb, env, db, logtail: logger } = req; + const { env, db, logtail: logger } = req; const [org, features] = await Promise.all([ OrgService.getFromReq(req), @@ -505,7 +504,6 @@ export const handlePostEntityRequest = async (req: any, res: any) => const entities = await createEntities({ db, - sb, org, features, logger, diff --git a/server/src/internal/api/entities/handlers/handleDeleteEntity.ts b/server/src/internal/api/entities/handlers/handleDeleteEntity.ts index 35f78eff1..e0e00b3d7 100644 --- a/server/src/internal/api/entities/handlers/handleDeleteEntity.ts +++ b/server/src/internal/api/entities/handlers/handleDeleteEntity.ts @@ -20,7 +20,7 @@ import { removeScheduledProduct } from "../../customers/handlers/handleCusProduc export const handleDeleteEntity = async (req: any, res: any) => { try { - const { orgId, env, db, logtail: logger, sb } = req; + const { orgId, env, db, logtail: logger } = req; const { customer_id, entity_id } = req.params; await handleCustomerRaceCondition({ @@ -32,8 +32,8 @@ export const handleDeleteEntity = async (req: any, res: any) => { logger, }); - const customer = await CusService.getWithProducts({ - sb: req.sb, + const customer = await CusService.getFull({ + db, idOrInternalId: customer_id, orgId: req.orgId, env: req.env, @@ -107,7 +107,6 @@ export const handleDeleteEntity = async (req: any, res: any) => { await adjustAllowance({ db, - sb, env, org, cusPrices: cusProducts.flatMap((p: any) => p.customer_prices), @@ -160,7 +159,6 @@ export const handleDeleteEntity = async (req: any, res: any) => { if (cusProduct.status == CusProductStatus.Scheduled) { await removeScheduledProduct({ db, - sb, cusProduct, cusProducts, org, diff --git a/server/src/internal/api/entities/handlers/handleGetEntity.ts b/server/src/internal/api/entities/handlers/handleGetEntity.ts index 8909bb030..50f9b0a95 100644 --- a/server/src/internal/api/entities/handlers/handleGetEntity.ts +++ b/server/src/internal/api/entities/handlers/handleGetEntity.ts @@ -16,7 +16,7 @@ export const handleGetEntity = async (req: any, res: any) => const customerId = req.params.customer_id as string; const expand = parseEntityExpand(req.query.expand); - let { orgId, env, sb, logtail: logger } = req; + let { orgId, env, db, logtail: logger } = req; let org = await OrgService.getFromReq(req); let apiVersion = orgToVersion({ @@ -27,7 +27,7 @@ export const handleGetEntity = async (req: any, res: any) => // const start = performance.now(); let { entities, customer, fullEntities, invoices } = await getEntityResponse({ - sb, + db, entityIds: [entityId], org, env, @@ -47,7 +47,7 @@ export const handleGetEntity = async (req: any, res: any) => ...entity, invoices: withInvoices ? invoicesToResponse({ - invoices, + invoices: invoices || [], logger, }) : undefined, diff --git a/server/src/internal/api/entitled/checkUtils.ts b/server/src/internal/api/entitled/checkUtils.ts index 9c1fac8cc..4f7de0c3b 100644 --- a/server/src/internal/api/entitled/checkUtils.ts +++ b/server/src/internal/api/entitled/checkUtils.ts @@ -19,7 +19,6 @@ import { DrizzleCli } from "@/db/initDrizzle.js"; export const getBooleanEntitledResult = async ({ db, - sb, customer_id, cusEnts, org, @@ -31,7 +30,6 @@ export const getBooleanEntitledResult = async ({ allFeatures, }: { db: DrizzleCli; - sb: SupabaseClient; customer_id: string; cusEnts: FullCustomerEntitlement[]; org: Organization; @@ -55,7 +53,6 @@ export const getBooleanEntitledResult = async ({ preview: withPreview ? await getCheckPreview({ db, - sb, allowed, balance: undefined, feature, diff --git a/server/src/internal/api/entitled/entitledRouter.ts b/server/src/internal/api/entitled/entitledRouter.ts index 8aaeef744..3b8e88a2a 100644 --- a/server/src/internal/api/entitled/entitledRouter.ts +++ b/server/src/internal/api/entitled/entitledRouter.ts @@ -196,12 +196,11 @@ const getCusEntsAndFeatures = async ({ logger, }: { req: any; - sb: SupabaseClient; logger: any; }) => { let { customer_id, feature_id, customer_data, entity_id } = req.body; - let { sb, env, db } = req; + let { env, db } = req; // 1. Get org and features const startTime = Date.now(); @@ -219,7 +218,6 @@ const getCusEntsAndFeatures = async ({ const customer = await getOrCreateCustomer({ db, - sb, org: req.org, env, customerId: customer_id, @@ -296,7 +294,7 @@ entitledRouter.post("", async (req: any, res: any) => { entity_id, } = req.body; - const { logtail: logger, sb, db } = req; + const { logtail: logger, db } = req; if (!customer_id) { throw new RecaseError({ @@ -350,7 +348,6 @@ entitledRouter.post("", async (req: any, res: any) => { const { cusEnts, feature, creditSystems, org, cusProducts, allFeatures } = await getCusEntsAndFeatures({ - sb, req, logger: req.logtail, }); @@ -375,7 +372,6 @@ entitledRouter.post("", async (req: any, res: any) => { withPreview: req.body.with_preview, cusProducts, allFeatures, - sb, }); } @@ -454,7 +450,6 @@ entitledRouter.post("", async (req: any, res: any) => { allowed, balance: balanceObj?.balance, feature: featureToUse!, - sb, cusProducts, raw: req.body.with_preview === "raw", allFeatures, diff --git a/server/src/internal/api/entitled/getCheckPreview.ts b/server/src/internal/api/entitled/getCheckPreview.ts index 07b71de5c..d15751994 100644 --- a/server/src/internal/api/entitled/getCheckPreview.ts +++ b/server/src/internal/api/entitled/getCheckPreview.ts @@ -19,7 +19,6 @@ import { SupabaseClient } from "@supabase/supabase-js"; export const getCheckPreview = async ({ db, - sb, allowed, balance, feature, @@ -28,7 +27,6 @@ export const getCheckPreview = async ({ allFeatures, }: { db: DrizzleCli; - sb: SupabaseClient; allowed: boolean; balance?: number; feature: Feature; diff --git a/server/src/internal/api/entitled/handlers/getAttachPreview.ts b/server/src/internal/api/entitled/handlers/getAttachPreview.ts index c5cac3cc4..48edee6e0 100644 --- a/server/src/internal/api/entitled/handlers/getAttachPreview.ts +++ b/server/src/internal/api/entitled/handlers/getAttachPreview.ts @@ -29,7 +29,6 @@ import { DrizzleCli } from "@/db/initDrizzle.js"; export const getAttachPreview = async ({ db, - sb, customer, org, env, @@ -40,7 +39,6 @@ export const getAttachPreview = async ({ shouldFormat = true, }: { db: DrizzleCli; - sb: SupabaseClient; customer: FullCustomer; org: Organization; env: AppEnv; @@ -172,7 +170,6 @@ export const getAttachPreview = async ({ if (isUpgrade) { return await getUpgradePreview({ db, - sb, customer, org, env, diff --git a/server/src/internal/api/entitled/handlers/handleProductCheck.ts b/server/src/internal/api/entitled/handlers/handleProductCheck.ts index 3e503fe0d..1ea017d1c 100644 --- a/server/src/internal/api/entitled/handlers/handleProductCheck.ts +++ b/server/src/internal/api/entitled/handlers/handleProductCheck.ts @@ -20,7 +20,7 @@ export const handleProductCheck = async ({ with_preview, entity_data, } = req.body; - const { orgId, sb, env, logtail: logger, db } = req; + const { orgId, env, logtail: logger, db } = req; let { org, features } = await getOrgAndFeatures({ req }); @@ -28,7 +28,6 @@ export const handleProductCheck = async ({ let [customer, product] = await Promise.all([ getOrCreateCustomer({ db, - sb, org, env, customerId: customer_id, @@ -80,7 +79,6 @@ export const handleProductCheck = async ({ product: product!, cusProducts, features, - sb, logger, shouldFormat: with_preview == "formatted", }) @@ -116,7 +114,6 @@ export const handleProductCheck = async ({ product: product!, cusProducts, features, - sb, logger, }) : undefined, diff --git a/server/src/internal/api/events/EventService.ts b/server/src/internal/api/events/EventService.ts index f8c898640..f2a622f51 100644 --- a/server/src/internal/api/events/EventService.ts +++ b/server/src/internal/api/events/EventService.ts @@ -80,24 +80,5 @@ export class EventService { ) .orderBy(desc(events.timestamp)) .limit(limit); - // const { data, error } = await sb - // .from("events") - // .select(fields ? fields.join(",") : "*") - // .eq("internal_customer_id", internalCustomerId) - // .eq("org_id", orgId) - // .eq("env", env) - // .order("timestamp", { ascending: false }) - // .limit(limit); - - // if (error) { - // throw new RecaseError({ - // message: "Failed to get events", - // code: ErrCode.InternalError, - // data: error, - // statusCode: StatusCodes.INTERNAL_SERVER_ERROR, - // }); - // } - - // return data; } } diff --git a/server/src/internal/api/events/eventRouter.ts b/server/src/internal/api/events/eventRouter.ts index fe84f6732..a0c5865d8 100644 --- a/server/src/internal/api/events/eventRouter.ts +++ b/server/src/internal/api/events/eventRouter.ts @@ -36,7 +36,6 @@ export const eventsRouter = Router(); const getEventAndCustomer = async ({ db, - sb, org, env, features, @@ -48,7 +47,6 @@ const getEventAndCustomer = async ({ entityData, }: { db: DrizzleCli; - sb: SupabaseClient; org: Organization; features: Feature[]; env: AppEnv; @@ -72,7 +70,6 @@ const getEventAndCustomer = async ({ // 2. Check if customer ID is valid customer = await getOrCreateCustomer({ db, - sb, org, env, customerId: customer_id, @@ -160,13 +157,12 @@ export const handleEventSent = async ({ }); } - const { sb, env, db } = req; + const { env, db } = req; const org = await OrgService.getFromReq(req); const features = await FeatureService.getFromReq(req); const { customer, event } = await getEventAndCustomer({ db, - sb, org, env, customer_id, diff --git a/server/src/internal/api/events/usageRouter.ts b/server/src/internal/api/events/usageRouter.ts index 4a5e0283f..e05a0109f 100644 --- a/server/src/internal/api/events/usageRouter.ts +++ b/server/src/internal/api/events/usageRouter.ts @@ -37,12 +37,11 @@ const getCusFeatureAndOrg = async ({ customerData: any; }) => { // 1. Get customer - const { db, sb } = req; + const { db } = req; let { org, features } = await getOrgAndFeatures({ req }); let [customer] = await Promise.all([ getOrCreateCustomer({ db, - sb, org, env: req.env, customerId, diff --git a/server/src/internal/api/features/handlers/handleDeleteFeature.ts b/server/src/internal/api/features/handlers/handleDeleteFeature.ts index 86cdad27f..6da74caba 100644 --- a/server/src/internal/api/features/handlers/handleDeleteFeature.ts +++ b/server/src/internal/api/features/handlers/handleDeleteFeature.ts @@ -12,7 +12,7 @@ export const handleDeleteFeature = async (req: any, res: any) => res, action: "Delete feature", handler: async (req: ExtendedRequest, res: ExtendedResponse) => { - const { db, sb, orgId, env } = req; + const { db, orgId } = req; let { featureId } = req.params; let features = await FeatureService.getFromReq(req); diff --git a/server/src/internal/api/features/handlers/handleUpdateFeature.ts b/server/src/internal/api/features/handlers/handleUpdateFeature.ts index 4554b9a03..2c4d69cb4 100644 --- a/server/src/internal/api/features/handlers/handleUpdateFeature.ts +++ b/server/src/internal/api/features/handlers/handleUpdateFeature.ts @@ -256,7 +256,7 @@ export const handleUpdateFeature = async (req: any, res: any) => handler: async (req: ExtendedRequest, res: ExtendedResponse) => { let featureId = req.params.feature_id; let data = req.body; - let { db, sb, orgId, env, logtail: logger } = req; + let { db, orgId, env, logtail: logger } = req; // 1. Get feature by ID let features = await FeatureService.getFromReq(req); diff --git a/server/src/internal/api/migrations/migrationRouter.ts b/server/src/internal/api/migrations/migrationRouter.ts index f8b184a63..8210c6df6 100644 --- a/server/src/internal/api/migrations/migrationRouter.ts +++ b/server/src/internal/api/migrations/migrationRouter.ts @@ -9,6 +9,7 @@ import { JobName } from "@/queue/JobName.js"; import { addTaskToQueue } from "@/queue/queueUtils.js"; import { pricesOnlyOneOff } from "@/internal/products/prices/priceUtils.js"; import { isFreeProduct } from "@/internal/products/productUtils.js"; +import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js"; export const migrationRouter = express.Router(); @@ -17,8 +18,8 @@ migrationRouter.post("", async (req: any, res: any) => { req, res, action: "migrate", - handler: async (req: any, res: any) => { - const { orgId, env, sb, db } = req; + handler: async (req: ExtendedRequest, res: ExtendedResponse) => { + const { orgId, env, db } = req; const { from_product_id, from_version, to_product_id, to_version } = req.body; @@ -74,7 +75,7 @@ migrationRouter.post("", async (req: any, res: any) => { }); await MigrationService.createJob({ - sb, + db, data: migrationJob, }); diff --git a/server/src/internal/api/products/handleDeleteProduct.ts b/server/src/internal/api/products/handleDeleteProduct.ts index 99b996195..96804ae61 100644 --- a/server/src/internal/api/products/handleDeleteProduct.ts +++ b/server/src/internal/api/products/handleDeleteProduct.ts @@ -11,7 +11,7 @@ export const handleDeleteProduct = (req: any, res: any) => res, action: "delete product", handler: async () => { - const { db, orgId, env, sb } = req; + const { db, orgId, env } = req; const { productId } = req.params; const product = await ProductService.get({ diff --git a/server/src/internal/api/products/handleUpdateProduct.ts b/server/src/internal/api/products/handleUpdateProduct.ts index b53baebd9..1ed094f3b 100644 --- a/server/src/internal/api/products/handleUpdateProduct.ts +++ b/server/src/internal/api/products/handleUpdateProduct.ts @@ -116,14 +116,11 @@ export const handleUpdateProductV2 = async (req: any, res: any) => action: "Update product", handler: async () => { const { productId } = req.params; - const { sb, orgId, env, logtail: logger, db } = req; + const { orgId, env, logtail: logger, db } = req; const [features, org, fullProduct, rewardPrograms] = await Promise.all([ FeatureService.getFromReq(req), - OrgService.getFullOrg({ - sb, - orgId, - }), + OrgService.getFromReq(req), ProductService.getFull({ db, idOrInternalId: productId, @@ -131,7 +128,7 @@ export const handleUpdateProductV2 = async (req: any, res: any) => env, }), RewardProgramService.getByProductId({ - sb, + db, productIds: [productId], orgId, env, @@ -176,7 +173,6 @@ export const handleUpdateProductV2 = async (req: any, res: any) => await handleVersionProductV2({ req, res, - sb, latestProduct: fullProduct, org, env, @@ -190,7 +186,6 @@ export const handleUpdateProductV2 = async (req: any, res: any) => await handleNewProductItems({ db, - sb, curPrices: fullProduct.prices, curEnts: fullProduct.entitlements, newItems: items, diff --git a/server/src/internal/api/products/handleVersionProduct.ts b/server/src/internal/api/products/handleVersionProduct.ts index d1eca8885..99237e043 100644 --- a/server/src/internal/api/products/handleVersionProduct.ts +++ b/server/src/internal/api/products/handleVersionProduct.ts @@ -18,7 +18,6 @@ import { validateProductItems } from "@/internal/products/product-items/validate export const handleVersionProductV2 = async ({ req, res, - sb, latestProduct, org, env, @@ -27,7 +26,6 @@ export const handleVersionProductV2 = async ({ }: { req: any; res: any; - sb: SupabaseClient; latestProduct: FullProduct; org: Organization; env: AppEnv; @@ -68,7 +66,6 @@ export const handleVersionProductV2 = async ({ await handleNewProductItems({ db, - sb, curPrices: latestProduct.prices, curEnts: latestProduct.entitlements, newItems: items, diff --git a/server/src/internal/api/products/handlers/handleCopyProduct.ts b/server/src/internal/api/products/handlers/handleCopyProduct.ts index 97b7bd458..0a549df7c 100644 --- a/server/src/internal/api/products/handlers/handleCopyProduct.ts +++ b/server/src/internal/api/products/handlers/handleCopyProduct.ts @@ -12,7 +12,7 @@ export const handleCopyProduct = async (req: any, res: any) => res, action: "Copy Product", handler: async (req, res) => { - let { db, sb, logtail: logger } = req; + let { db, logtail: logger } = req; const { productId: fromProductId } = req.params; const orgId = req.orgId; @@ -102,7 +102,6 @@ export const handleCopyProduct = async (req: any, res: any) => // // 2. Copy product await copyProduct({ db, - sb, product: fromFullProduct, toOrgId: orgId, toId, diff --git a/server/src/internal/api/products/handlers/handleCreateProduct.ts b/server/src/internal/api/products/handlers/handleCreateProduct.ts index 53dab6e4f..80e3a01ad 100644 --- a/server/src/internal/api/products/handlers/handleCreateProduct.ts +++ b/server/src/internal/api/products/handlers/handleCreateProduct.ts @@ -95,8 +95,8 @@ export const handleCreateProduct = async (req: Request, res: any) => res, action: "POST /products", handler: async (req, res) => { - let { free_trial, items } = req.body; - let { logtail: logger, orgId, env, sb, db } = req; + let { items } = req.body; + let { logtail: logger, orgId, env, db } = req; let { features, freeTrial, productData } = await validateCreateProduct({ req, @@ -113,7 +113,6 @@ export const handleCreateProduct = async (req: Request, res: any) => if (notNullish(items)) { await handleNewProductItems({ db, - sb, product, features, curPrices: [], diff --git a/server/src/internal/api/products/productRouter.ts b/server/src/internal/api/products/productRouter.ts index 5acdd1509..ca6fa0a95 100644 --- a/server/src/internal/api/products/productRouter.ts +++ b/server/src/internal/api/products/productRouter.ts @@ -36,7 +36,7 @@ productApiRouter.post("/:productId/copy", handleCopyProduct); productApiRouter.post("/all/init_stripe", async (req: any, res) => { try { - const { sb, orgId, env, logtail: logger, db } = req; + const { orgId, env, logtail: logger, db } = req; const [fullProducts, org] = await Promise.all([ ProductService.listFull({ diff --git a/server/src/internal/api/rewards/referralRouter.ts b/server/src/internal/api/rewards/referralRouter.ts index 08ab74000..01832ca90 100644 --- a/server/src/internal/api/rewards/referralRouter.ts +++ b/server/src/internal/api/rewards/referralRouter.ts @@ -23,23 +23,24 @@ referralRouter.post("/code", (req, res) => res, action: "get referral code", handler: async (req: any, res: any) => { - const { orgId, env, logtail: logger } = req; + const { orgId, env, logtail: logger, db } = req; const { program_id: rewardProgramId, customer_id: customerId } = req.body; - let rewardProgram = await RewardProgramService.getById({ - sb: req.sb, - id: rewardProgramId, - orgId, - env, - errorIfNotFound: true, - }); - - let customer = await CusService.get({ - db: req.db, - orgId, - env, - idOrInternalId: customerId, - }); + let [rewardProgram, customer] = await Promise.all([ + RewardProgramService.get({ + db, + id: rewardProgramId, + orgId, + env, + errorIfNotFound: true, + }), + CusService.get({ + db: req.db, + orgId, + env, + idOrInternalId: customerId, + }), + ]); if (!customer) { throw new RecaseError({ @@ -49,10 +50,18 @@ referralRouter.post("/code", (req, res) => }); } + if (!rewardProgram) { + throw new RecaseError({ + message: "Reward program not found", + statusCode: 404, + code: ErrCode.RewardProgramNotFound, + }); + } + // Get referral code by customer and reward trigger let referralCode = await RewardProgramService.getCodeByCustomerAndRewardProgram({ - sb: req.sb, + db, orgId, env, internalCustomerId: customer.internal_id, @@ -72,8 +81,8 @@ referralRouter.post("/code", (req, res) => created_at: Date.now(), }; - await RewardProgramService.createReferralCode({ - sb: req.sb, + referralCode = await RewardProgramService.createReferralCode({ + db, data: referralCode, }); } @@ -93,20 +102,20 @@ referralRouter.post("/redeem", (req, res) => res, action: "redeem referral code", handler: async (req: any, res: any) => { - const { orgId, env, logtail: logger } = req; + const { orgId, env, logtail: logger, db } = req; // const { referral_id: rewardTriggerId } = req.params; const { code, customer_id: customerId } = req.body; // 1. Get redeemed by customer, and referral code let [customer, referralCode, org] = await Promise.all([ CusService.get({ - db: req.db, + db, orgId, env, idOrInternalId: customerId, }), RewardProgramService.getReferralCode({ - sb: req.sb, + db, orgId, env, code, @@ -125,11 +134,14 @@ referralRouter.post("/redeem", (req, res) => // 2. Check that code has not reached max redemptions let redemptionCount = await RewardProgramService.getCodeRedemptionCount({ - sb: req.sb, + db, referralCodeId: referralCode.id, }); - if (redemptionCount >= referralCode.reward_program.max_redemptions) { + if ( + referralCode.reward_program.max_redemptions && + redemptionCount >= referralCode.reward_program.max_redemptions + ) { throw new RecaseError({ message: "Referral code has reached max redemptions", statusCode: 400, @@ -139,7 +151,7 @@ referralRouter.post("/redeem", (req, res) => // 3. Check that customer has not already redeemed a code in this referral program let existingRedemptions = await RewardRedemptionService.getByCustomer({ - sb: req.sb, + db, internalCustomerId: customer.internal_id, internalRewardProgramId: referralCode.internal_reward_program_id, }); @@ -193,7 +205,7 @@ referralRouter.post("/redeem", (req, res) => }; redemption = await RewardRedemptionService.insert({ - sb: req.sb, + db, rewardRedemption: redemption, }); @@ -205,7 +217,6 @@ referralRouter.post("/redeem", (req, res) => ) { redemption = await triggerRedemption({ db: req.db, - sb: req.sb, referralCode, org, env, @@ -235,17 +246,14 @@ redemptionRouter.get("/:redemptionId", (req, res) => res, action: "get redemption by id", handler: async (req: any, res: any) => { - const { orgId, env, logtail: logger } = req; + const { db } = req; const { redemptionId } = req.params; let redemption = await RewardRedemptionService.getById({ - sb: req.sb, + db, id: redemptionId, }); - // logger.info("Returning redemption"); - // logger.info(redemption); - res.status(200).json(redemption); }, }), diff --git a/server/src/internal/api/rewards/rewardProgramRouter.ts b/server/src/internal/api/rewards/rewardProgramRouter.ts index 3ad15c157..a738ef1f7 100644 --- a/server/src/internal/api/rewards/rewardProgramRouter.ts +++ b/server/src/internal/api/rewards/rewardProgramRouter.ts @@ -18,7 +18,7 @@ rewardProgramRouter.post("", (req, res) => res, action: "create reward trigger", handler: async (req: any, res: any) => { - const { orgId, env } = req; + const { orgId, env, db } = req; const rewardProgram = constructRewardProgram({ rewardProgramData: CreateRewardProgram.parse(req.body), orgId, @@ -38,7 +38,7 @@ rewardProgramRouter.post("", (req, res) => } let createdRewardProgram = await RewardProgramService.create({ - sb: req.sb, + db, data: rewardProgram, }); @@ -46,7 +46,7 @@ rewardProgramRouter.post("", (req, res) => return res.status(200).json(createdRewardProgram); }, - }) + }), ); rewardProgramRouter.delete("/:id", (req, res) => @@ -55,11 +55,11 @@ rewardProgramRouter.delete("/:id", (req, res) => res, action: "delete reward scheme", handler: async (req: any, res: any) => { - const { orgId, env } = req; + const { orgId, env, db } = req; const { id } = req.params; - let rewardProgram = await RewardProgramService.deleteById({ - sb: req.sb, + let rewardProgram = await RewardProgramService.delete({ + db, id, orgId, env, @@ -67,5 +67,5 @@ rewardProgramRouter.delete("/:id", (req, res) => return res.status(200).json(rewardProgram); }, - }) + }), ); diff --git a/server/src/internal/api/rewards/rewardRouter.ts b/server/src/internal/api/rewards/rewardRouter.ts index abc3ec747..10ea178ba 100644 --- a/server/src/internal/api/rewards/rewardRouter.ts +++ b/server/src/internal/api/rewards/rewardRouter.ts @@ -23,7 +23,7 @@ const rewardRouter = express.Router(); rewardRouter.post("", async (req: any, res: any) => { try { - const { db, sb, orgId, env, logtail: logger } = req; + const { db, orgId, env, logtail: logger } = req; const rewardBody = req.body; const rewardData = CreateRewardSchema.parse(rewardBody); @@ -90,7 +90,7 @@ rewardRouter.post("", async (req: any, res: any) => { } const insertedCoupon = await RewardService.insert({ - sb: req.sb, + db, data: newReward, }); console.log("✅ Reward successfully inserted into db"); @@ -109,22 +109,37 @@ rewardRouter.post("", async (req: any, res: any) => { rewardRouter.delete("/:id", async (req: any, res: any) => { try { const { id } = req.params; - const { orgId, env } = req; + const { orgId, env, db } = req; + const org = await OrgService.getFromReq(req); const stripeCli = createStripeCli({ org, env, }); + let reward = await RewardService.get({ + db, + idOrInternalId: id, + orgId, + env, + }); + + if (!reward) { + throw new RecaseError({ + message: `Reward ${id} not found`, + code: ErrCode.InvalidRequest, + }); + } + try { - await stripeCli.coupons.del(id); + await stripeCli.coupons.del(reward.id); } catch (error: any) { console.log(`Failed to delete coupon from stripe: ${error.message}`); } - await RewardService.deleteStrict({ - sb: req.sb, - internalId: id, + await RewardService.delete({ + db, + internalId: reward.internal_id, env, orgId, }); @@ -155,9 +170,9 @@ rewardRouter.post("/:internalId", async (req: any, res: any) => { env, }); - const reward = await RewardService.getByInternalId({ - sb: req.sb, - internalId, + const reward = await RewardService.get({ + db, + idOrInternalId: internalId, orgId, env, }); @@ -189,8 +204,8 @@ rewardRouter.post("/:internalId", async (req: any, res: any) => { // 3. Update coupon in db const updatedCoupon = await RewardService.update({ - sb: req.sb, - internalId, + db, + internalId: reward.internal_id, env, orgId, update: rewardBody, diff --git a/server/src/internal/customers/CusReadService.ts b/server/src/internal/customers/CusReadService.ts index 6b45b3422..f41548692 100644 --- a/server/src/internal/customers/CusReadService.ts +++ b/server/src/internal/customers/CusReadService.ts @@ -1,22 +1,19 @@ -import { SupabaseClient } from "@supabase/supabase-js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { Customer, customers } from "@autumn/shared"; +import { inArray } from "drizzle-orm"; export class CusReadService { static async getInInternalIds({ - sb, + db, internalIds, }: { - sb: SupabaseClient; + db: DrizzleCli; internalIds: string[]; }) { - const { data, error } = await sb - .from("customers") - .select("*") - .in("internal_id", internalIds); + const data = await db.query.customers.findMany({ + where: inArray(customers.internal_id, internalIds), + }); - if (error) { - throw error; - } - - return data; + return data as Customer[]; } } diff --git a/server/src/internal/customers/CusSearchService.ts b/server/src/internal/customers/CusSearchService.ts new file mode 100644 index 000000000..56ab55c2a --- /dev/null +++ b/server/src/internal/customers/CusSearchService.ts @@ -0,0 +1,302 @@ +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { Client } from "pg"; +import { AppEnv, customers, CusProductStatus } from "@autumn/shared"; +import { SupabaseClient } from "@supabase/supabase-js"; +import { + and, + desc, + eq, + ilike, + or, + count, + lt, + inArray, + isNotNull, + gt, + sql, +} from "drizzle-orm"; +import { customerProducts, products } from "@autumn/shared"; +const customerFields = { + internal_id: customers.internal_id, + id: customers.id, + name: customers.name, + email: customers.email, + created_at: customers.created_at, +}; + +const customerProductFields = { + id: customerProducts.id, + internal_product_id: customerProducts.internal_product_id, + product_id: customerProducts.product_id, + canceled_at: customerProducts.canceled_at, + status: customerProducts.status, + trial_ends_at: customerProducts.trial_ends_at, +}; + +const productFields = { + internal_id: products.internal_id, + id: products.id, + name: products.name, + version: products.version, +}; + +export class CusSearchService { + static async searchByProduct({ + db, + orgId, + env, + search, + filters, + pageSize = 50, + lastItem, + }: { + db: DrizzleCli; + orgId: string; + env: AppEnv; + search: string; + filters: any; + pageSize?: number; + lastItem?: { created_at: string; name: string; internal_id: string } | null; + }) { + // 1. Create base query to fetch all customerproducts + let activeProdFilter = or( + eq(customerProducts.status, CusProductStatus.Active), + eq(customerProducts.status, CusProductStatus.PastDue), + ); + + let filtersDrizzle = and( + filters.product_id + ? eq(customerProducts.product_id, filters.product_id) + : undefined, + filters.status ? eq(customerProducts.status, filters.status) : undefined, + filters.status === "canceled" + ? and(activeProdFilter, isNotNull(customerProducts.canceled_at)) + : undefined, + filters.status === "free_trial" + ? and( + eq(customerProducts.status, CusProductStatus.Active), + gt(customerProducts.trial_ends_at, Date.now()), + ) + : undefined, + ); + + let cusFilter = and( + eq(customers.org_id, orgId), + eq(customers.env, env), + + search + ? or( + ilike(customers.id, `%${search}%`), + ilike(customers.name, `%${search}%`), + ilike(customers.email, `%${search}%`), + ) + : undefined, + ); + + const [results, totalCountResult] = await Promise.all([ + db + .select({ + customer: customerFields, + customerProduct: customerProductFields, + product: productFields, + }) + .from(customerProducts) + .leftJoin( + customers, + eq(customerProducts.internal_customer_id, customers.internal_id), + ) + .leftJoin( + products, + eq(customerProducts.internal_product_id, products.internal_id), + ) + .where( + and( + activeProdFilter, + filtersDrizzle, + cusFilter, + lastItem && lastItem.internal_id + ? lt(customers.internal_id, lastItem.internal_id) + : undefined, + ), + ) + .orderBy(desc(customers.internal_id)) + .limit(pageSize), + + db + .select({ + totalCount: sql`count(distinct ${customers.internal_id})`.as( + "total_count", + ), + }) + .from(customerProducts) + .leftJoin( + customers, + eq(customerProducts.internal_customer_id, customers.internal_id), + ) + .leftJoin( + products, + eq(customerProducts.internal_product_id, products.internal_id), + ) + .where(and(activeProdFilter, filtersDrizzle, cusFilter)), + ]); + + // Process the results to group customer products by customer + const customerMap = new Map(); + + for (const row of results) { + const customerId = row.customer?.internal_id; + if (!customerId) continue; + + if (!customerMap.has(customerId)) { + customerMap.set(customerId, { + ...row.customer, + customer_products: [], + }); + } + + if (row.customerProduct && row.product) { + customerMap.get(customerId).customer_products.push({ + ...row.customerProduct, + product: row.product, + }); + } + } + + const processedData = Array.from(customerMap.values()); + + const totalCount = totalCountResult[0]?.totalCount || 0; + + return { data: processedData, count: totalCount }; + } + + static async search({ + db, + orgId, + env, + search, + pageSize = 50, + filters, + lastItem, + pageNumber, + }: { + db: DrizzleCli; + orgId: string; + env: AppEnv; + search: string; + lastItem?: { created_at: string; name: string; internal_id: string } | null; + filters: any; + pageSize?: number; + pageNumber: number; + }) { + if (filters.product_id || filters.status) { + return await this.searchByProduct({ + db, + orgId, + env, + search, + filters, + pageSize, + lastItem, + }); + } + + let filterClause = and( + eq(customers.org_id, orgId), + eq(customers.env, env), + search + ? or( + ilike(customers.id, `%${search}%`), + ilike(customers.name, `%${search}%`), + ilike(customers.email, `%${search}%`), + ) + : undefined, + ); + + // Create the base customer query as a subquery + const baseQuery = db + .select(customerFields) + .from(customers) + .where( + and( + filterClause, + lastItem && lastItem.internal_id + ? lt(customers.internal_id, lastItem.internal_id) + : undefined, + ), + ) + .orderBy(desc(customers.internal_id)) + .limit(pageSize) + .as("baseQuery"); + + // Get total count in parallel without pagination + const totalCountQuery = db + .select({ + count: sql`count(*)`.as("count"), + }) + .from(customers) + .where(filterClause); + + // Now join with customer products and products + const [results, totalCountResult] = await Promise.all([ + db + .select({ + // Customer fields + customer: { + internal_id: baseQuery.internal_id, + id: baseQuery.id, + name: baseQuery.name, + email: baseQuery.email, + created_at: baseQuery.created_at, + }, + // Customer product fields + customerProduct: customerProductFields, + // Product fields + product: productFields, + }) + .from(baseQuery) + .leftJoin( + customerProducts, + eq(baseQuery.internal_id, customerProducts.internal_customer_id), + ) + .leftJoin( + products, + eq(customerProducts.internal_product_id, products.internal_id), + ) + .orderBy(desc(baseQuery.internal_id)), + totalCountQuery, + ]); + + if (results.length === 0) { + return { data: [], count: 0 }; + } + + const totalCount = totalCountResult[0]?.count || 0; + + // Group the results by customer + const customerMap = new Map(); + + for (const row of results) { + const customerId = row.customer.internal_id; + + if (!customerMap.has(customerId)) { + customerMap.set(customerId, { + ...row.customer, + created_at: Number(row.customer.created_at), + customer_products: [], + }); + } + + // Add customer product if it exists + if (row.customerProduct && row.customerProduct.id) { + customerMap.get(customerId).customer_products.push({ + ...row.customerProduct, + product: row.product, + }); + } + } + + const finalResults = Array.from(customerMap.values()); + + return { data: finalResults, count: totalCount }; + } +} diff --git a/server/src/internal/customers/CusService.ts b/server/src/internal/customers/CusService.ts index 84e7fbbde..2fc5232e3 100644 --- a/server/src/internal/customers/CusService.ts +++ b/server/src/internal/customers/CusService.ts @@ -5,36 +5,23 @@ import { CusProductStatus, Customer, customers, + Entity, EntityExpand, FullCusProduct, + FullCustomer, } from "@autumn/shared"; import RecaseError from "@/utils/errorUtils.js"; import { ErrCode } from "@/errors/errCodes.js"; import { StatusCodes } from "http-status-codes"; import { Client } from "pg"; import { flipProductResults } from "../api/customers/cusUtils.js"; -import { sbWithRetry } from "@/external/supabaseUtils.js"; import { and, eq, or, sql } from "drizzle-orm"; import { DrizzleCli } from "@/db/initDrizzle.js"; - -const printCusProducts = (cusProducts: FullCusProduct[]) => { - for (let cusProduct of cusProducts) { - console.log(`Product: ${cusProduct.product.name}`); - for (let cusEnt of cusProduct.customer_entitlements) { - console.log( - `Entitlement: ${cusEnt.entitlement.feature_id}, Balance: ${cusEnt.balance}`, - ); - } - - for (let cusPrice of cusProduct.customer_prices) { - console.log(`cusPrice:`, cusPrice.id, cusPrice.price.id); - } - } -}; +import { getFullCusQuery } from "./getFullCusQuery.js"; export class CusService { - static async getWithProducts({ - sb, + static async getFull({ + db, idOrInternalId, orgId, env, @@ -47,8 +34,9 @@ export class CusService { entityId, expand, withSubs = false, + allowNotFound = false, }: { - sb: SupabaseClient; + db: DrizzleCli; idOrInternalId: string; orgId: string; env: AppEnv; @@ -57,34 +45,42 @@ export class CusService { entityId?: string; expand?: (CusExpand | EntityExpand)[]; withSubs?: boolean; - }) { - const { data, error } = await sb.rpc("get_cus_with_products", { - p_cus_id: idOrInternalId, - p_org_id: orgId, - p_env: env, - p_statuses: inStatuses, - p_with_entities: withEntities, - p_entity_id: entityId, - p_with_trials_used: expand?.includes(CusExpand.TrialsUsed), - p_with_subs: withSubs, - p_with_invoices: expand?.includes(CusExpand.Invoices), - }); + allowNotFound?: boolean; + }): Promise { + const includeInvoices = expand?.includes(CusExpand.Invoices) || false; + const withTrialsUsed = expand?.includes(CusExpand.TrialsUsed) || false; - if (error) { - throw error; + const query = getFullCusQuery( + idOrInternalId, + orgId, + env, + inStatuses, + includeInvoices, + withEntities, + withTrialsUsed, + withSubs, + entityId, + ); + + let result = await db.execute(query); + + if (!result || result.length == 0) { + if (allowNotFound) { + // @ts-ignore + return null; + } + + throw new RecaseError({ + message: `Customer ${idOrInternalId} not found`, + code: ErrCode.CustomerNotFound, + statusCode: StatusCodes.NOT_FOUND, + }); } - if (!data || !data.customer) { - return null; - } + let data = result[0]; + data.created_at = Number(data.created_at); - let { customer, products, entities, entity } = data; - - if (!products) { - products = []; - } - - for (let product of products) { + for (const product of data.customer_products as FullCusProduct[]) { if (!product.customer_prices) { product.customer_prices = []; } @@ -94,24 +90,11 @@ export class CusService { } } - let trialsUsed = data.trials_used; - if (trialsUsed) { - trialsUsed = trialsUsed.filter( - (trial: any, index: number, self: any) => - index === - self.findIndex((t: any) => t.product_id === trial.product_id), - ); - } + // data.invoices = data.invoices || []; + // data.subscriptions = data.subscriptions || []; + // data.trials_used = data.trials_used || []; - return { - ...customer, - customer_products: products, - entities: entities, - entity: entity, - trials_used: trialsUsed, - subscriptions: data.subscriptions, - invoices: data.invoices, - }; + return data as FullCustomer; } static async get({ @@ -240,8 +223,9 @@ export class CusService { if (lastItem) { query.or( - `"created_at".lt.${lastItem.created_at},` + - `and("created_at".eq.${lastItem.created_at},"internal_id".gt.${lastItem.internal_id})`, + `"internal_id".lt.${lastItem.internal_id}`, + // `"created_at".lt.${lastItem.created_at},` + + // `and("created_at".eq.${lastItem.created_at},"internal_id".gt.${lastItem.internal_id})`, customerPrefix && { foreignTable: "customers", referencedTable: "customers", @@ -249,27 +233,13 @@ export class CusService { ); } - // if (pageNumber) { - // const from = (pageNumber - 1) * pageSize; - // const to = from + pageSize - 1; - // query.range(from, to); - // } else if (lastItem) { - // query.or( - // `"created_at".lt.${lastItem.created_at},` + - // `and("created_at".eq.${lastItem.created_at},"internal_id".gt.${lastItem.internal_id})`, - // customerPrefix && { - // foreignTable: "customers", - // referencedTable: "customers", - // } - // ); - // } - if (customerPrefix) { - query.order(`customer(created_at)`, { ascending: false }); + query.order(`customer(internal_id)`, { ascending: false }); } else { - query - .order("created_at", { ascending: false }) - .order("internal_id", { ascending: true }); + query.order("internal_id", { ascending: false }); + // query + // .order("created_at", { ascending: false }) + // .order("internal_id", { ascending: true }); } query.limit(pageSize); @@ -413,6 +383,8 @@ export class CusService { return { data, count: totalCount }; } + // End of search customers + static async insert({ db, data }: { db: DrizzleCli; data: Customer }) { try { const results = await db @@ -435,25 +407,6 @@ export class CusService { } throw error; } - // const { data, error } = await sb - // .from("customers") - // .insert(customer) - // .select() - // .single(); - - // if (error) { - // if (error.code === "23505") { - // throw new RecaseError({ - // code: ErrCode.DuplicateCustomerId, - // message: "Customer ID already exists", - // statusCode: StatusCodes.BAD_REQUEST, - // data: error, - // }); - // } - // throw error; - // } - - // return data; } static async update({ @@ -506,31 +459,210 @@ export class CusService { return results; } + + static async deleteByOrgId({ + db, + orgId, + env, + }: { + db: DrizzleCli; + orgId: string; + env: AppEnv; + }) { + const results = await db + .delete(customers) + .where(and(eq(customers.org_id, orgId), eq(customers.env, env))) + .returning(); + + return results; + } } -// static async getCustomers( -// sb: SupabaseClient, -// orgId: string, -// env: AppEnv, -// page: number = 1, -// pageSize: number = 50, -// ) { -// const from = (page - 1) * pageSize; -// const to = from + pageSize - 1; +// static async getWithProductsDrizzle({ +// db, +// idOrInternalId, +// orgId, +// env, +// inStatuses = [ +// CusProductStatus.Active, +// CusProductStatus.PastDue, +// CusProductStatus.Scheduled, +// ], +// withEntities = false, +// entityId, +// expand, +// withSubs = false, +// }: { +// db: DrizzleCli; +// idOrInternalId: string; +// orgId: string; +// env: AppEnv; +// inStatuses?: CusProductStatus[]; +// withEntities?: boolean; +// entityId?: string; +// expand?: (CusExpand | EntityExpand)[]; +// withSubs?: boolean; +// }) { +// // 1. Call RPC function +// let data: { +// customer: Customer | null; +// products: FullCusProduct[] | null; +// entities: Entity[] | null; +// entity: Entity | null; +// trials_used: any[] | null; +// subscriptions: any[] | null; +// invoices: any[] | null; +// }; -// const { data, count, error } = await sb -// .from("customers") -// .select("*", { count: "exact" }) -// .eq("org_id", orgId) -// .eq("env", env) -// .order("created_at", { ascending: false }) -// .order("name", { ascending: true }) -// .order("internal_id", { ascending: true }) -// .range(from, to); +// try { +// const result = await db.execute(sql` +// SELECT * FROM get_cus_with_products( +// p_cus_id => ${idOrInternalId}::text, +// p_org_id => ${orgId}::text, +// p_env => ${env}::text, +// p_statuses => ARRAY[${sql.join( +// inStatuses.map((status) => sql`${status}`), +// sql`, `, +// )}]::text[], +// p_with_entities => ${withEntities}::boolean, +// p_entity_id => ${entityId || null}::text, +// p_with_trials_used => ${expand?.includes(CusExpand.TrialsUsed) || false}::boolean, +// p_with_subs => ${withSubs}::boolean, +// p_with_invoices => ${expand?.includes(CusExpand.Invoices) || false}::boolean +// ) +// `); + +// if (!result || result.length == 0 || !result[0].get_cus_with_products) { +// throw new RecaseError({ +// message: "Calling get_cus_with_products RPC returned wrong shape", +// code: ErrCode.GetCusWithProductsFailed, +// statusCode: StatusCodes.INTERNAL_SERVER_ERROR, +// data: result, +// }); +// } + +// data = result[0].get_cus_with_products as any; +// } catch (error) { +// throw error; +// } + +// if (!data || !data.customer) { +// return null; +// } + +// let { customer, products, entities, entity } = data; + +// if (!products) { +// products = []; +// } + +// for (let product of products) { +// if (!product.customer_prices) { +// product.customer_prices = []; +// } + +// if (!product.customer_entitlements) { +// product.customer_entitlements = []; +// } +// } + +// let trialsUsed = data.trials_used; +// if (trialsUsed) { +// trialsUsed = trialsUsed.filter( +// (trial: any, index: number, self: any) => +// index === +// self.findIndex((t: any) => t.product_id === trial.product_id), +// ); +// } + +// return { +// ...customer, +// customer_products: products, +// entities: entities, +// entity: entity, +// trials_used: trialsUsed, +// subscriptions: data.subscriptions, +// invoices: data.invoices, +// } as FullCustomer; +// } + +// static async getWithProducts({ +// sb, +// idOrInternalId, +// orgId, +// env, +// inStatuses = [ +// CusProductStatus.Active, +// CusProductStatus.PastDue, +// CusProductStatus.Scheduled, +// ], +// withEntities = false, +// entityId, +// expand, +// withSubs = false, +// }: { +// sb: SupabaseClient; +// idOrInternalId: string; +// orgId: string; +// env: AppEnv; +// inStatuses?: CusProductStatus[]; +// withEntities?: boolean; +// entityId?: string; +// expand?: (CusExpand | EntityExpand)[]; +// withSubs?: boolean; +// }) { +// const { data, error } = await sb.rpc("get_cus_with_products", { +// p_cus_id: idOrInternalId, +// p_org_id: orgId, +// p_env: env, +// p_statuses: inStatuses, +// p_with_entities: withEntities, +// p_entity_id: entityId, +// p_with_trials_used: expand?.includes(CusExpand.TrialsUsed), +// p_with_subs: withSubs, +// p_with_invoices: expand?.includes(CusExpand.Invoices), +// }); // if (error) { // throw error; // } -// return { data, count }; +// if (!data || !data.customer) { +// return null; +// } + +// let { customer, products, entities, entity } = data; + +// if (!products) { +// products = []; +// } + +// for (let product of products) { +// if (!product.customer_prices) { +// product.customer_prices = []; +// } + +// if (!product.customer_entitlements) { +// product.customer_entitlements = []; +// } +// } + +// let trialsUsed = data.trials_used; +// if (trialsUsed) { +// trialsUsed = trialsUsed.filter( +// (trial: any, index: number, self: any) => +// index === +// self.findIndex((t: any) => t.product_id === trial.product_id), +// ); +// } + +// return { +// ...customer, +// customer_products: products, +// entities: entities, +// entity: entity, +// trials_used: trialsUsed, +// subscriptions: data.subscriptions, +// invoices: data.invoices, +// }; // } diff --git a/server/src/internal/customers/add-product/createFullCusProduct.ts b/server/src/internal/customers/add-product/createFullCusProduct.ts index 0d139da1a..01e09433c 100644 --- a/server/src/internal/customers/add-product/createFullCusProduct.ts +++ b/server/src/internal/customers/add-product/createFullCusProduct.ts @@ -260,7 +260,6 @@ export const getExistingCusProduct = async ({ export const createFullCusProduct = async ({ db, - sb, attachParams, startsAt, subscriptionId, @@ -282,7 +281,6 @@ export const createFullCusProduct = async ({ sendWebhook = true, }: { db: DrizzleCli; - sb: SupabaseClient; attachParams: InsertCusProductParams; startsAt?: number; @@ -344,7 +342,6 @@ export const createFullCusProduct = async ({ !attachParams.isCustom ) { await updateOneTimeCusProduct({ - sb, db, attachParams, logger, @@ -453,7 +450,7 @@ export const createFullCusProduct = async ({ }); try { - if (sendWebhook) { + if (sendWebhook && !attachParams.fromMigration) { await addTaskToQueue({ jobName: JobName.SendProductsUpdatedWebhook, payload: constructProductsUpdatedData({ @@ -461,7 +458,6 @@ export const createFullCusProduct = async ({ org, env: customer.env, customerId: customer.id || null, - product: isDowngrade ? curCusProduct!.product : product, prices: isDowngrade ? curCusProduct!.customer_prices.map((cp) => cp.price) @@ -469,7 +465,6 @@ export const createFullCusProduct = async ({ entitlements: isDowngrade ? curCusProduct!.customer_entitlements.map((ce) => ce.entitlement) : entitlements, - freeTrial, scenario, }), diff --git a/server/src/internal/customers/add-product/createOneTimeCusProduct.ts b/server/src/internal/customers/add-product/createOneTimeCusProduct.ts index e4efb5941..dfb13051c 100644 --- a/server/src/internal/customers/add-product/createOneTimeCusProduct.ts +++ b/server/src/internal/customers/add-product/createOneTimeCusProduct.ts @@ -21,7 +21,6 @@ import { DrizzleCli } from "@/db/initDrizzle.js"; const updateOneOffExistingEntitlement = async ({ db, - sb, cusEnt, entitlement, org, @@ -31,7 +30,6 @@ const updateOneOffExistingEntitlement = async ({ logger, }: { db: DrizzleCli; - sb: SupabaseClient; cusEnt: FullCustomerEntitlement; entitlement: EntitlementWithFeature; org: Organization; @@ -79,12 +77,10 @@ const updateOneOffExistingEntitlement = async ({ export const updateOneTimeCusProduct = async ({ db, - sb, attachParams, logger, }: { db: DrizzleCli; - sb: SupabaseClient; attachParams: InsertCusProductParams; logger: any; }) => { @@ -112,7 +108,6 @@ export const updateOneTimeCusProduct = async ({ if (existingCusEnt) { await updateOneOffExistingEntitlement({ db, - sb, cusEnt: existingCusEnt, entitlement, org: attachParams.org, diff --git a/server/src/internal/customers/add-product/handleAddFreeProduct.ts b/server/src/internal/customers/add-product/handleAddFreeProduct.ts index 283909ab0..bb9b13fef 100644 --- a/server/src/internal/customers/add-product/handleAddFreeProduct.ts +++ b/server/src/internal/customers/add-product/handleAddFreeProduct.ts @@ -26,7 +26,7 @@ export const handleAddFreeProduct = async ({ for (const product of products) { await createFullCusProduct({ db: req.db, - sb: req.sb, + attachParams: attachToInsertParams(attachParams, product), subscriptionId: undefined, billLaterOnly: false, diff --git a/server/src/internal/customers/add-product/handleAddProduct.ts b/server/src/internal/customers/add-product/handleAddProduct.ts index e0c720e7a..ca8659018 100644 --- a/server/src/internal/customers/add-product/handleAddProduct.ts +++ b/server/src/internal/customers/add-product/handleAddProduct.ts @@ -51,7 +51,6 @@ import { getInvoiceItems } from "../invoices/invoiceUtils.js"; import { DrizzleCli } from "@/db/initDrizzle.js"; export const handleBillNowPrices = async ({ - sb, attachParams, res, req, @@ -60,7 +59,6 @@ export const handleBillNowPrices = async ({ shouldPreview = false, disableMerge = false, }: { - sb: any; attachParams: AttachParams; res: any; req: any; @@ -128,7 +126,7 @@ export const handleBillNowPrices = async ({ } subscription = await createStripeSub({ - sb, + db: req.db, stripeCli, customer, org, @@ -156,7 +154,7 @@ export const handleBillNowPrices = async ({ error.code === ErrCode.StripeGetPaymentMethodFailed ) { await handleCreateCheckout({ - sb, + db: req.db, res, attachParams, req, @@ -175,7 +173,6 @@ export const handleBillNowPrices = async ({ batchInsert.push( createFullCusProduct({ db: req.db, - sb, attachParams: attachToInsertParams(attachParams, product), subscriptionIds: subscriptions.map((s) => s.id), subscriptionId: @@ -206,7 +203,7 @@ export const handleBillNowPrices = async ({ }); await InvoiceService.createInvoiceFromStripe({ - sb, + db: req.db, stripeInvoice: invoice, internalCustomerId: customer.internal_id, internalEntityId: attachParams.internalEntityId, @@ -260,14 +257,12 @@ export const handleBillNowPrices = async ({ export const handleOneOffPrices = async ({ req, - sb, attachParams, res, fromRequest = true, shouldPreview = false, }: { req: any; - sb: any; attachParams: AttachParams; res: any; fromRequest?: boolean; @@ -396,12 +391,11 @@ export const handleOneOffPrices = async ({ logger, }); - console.log("Error code: ", error?.code); if (!paid) { await stripeCli.invoices.voidInvoice(stripeInvoice.id); if (fromRequest && org.config.checkout_on_failed_payment) { await handleCreateCheckout({ - sb, + db: req.db, req, res, attachParams, @@ -420,7 +414,7 @@ export const handleOneOffPrices = async ({ batchInsert.push( createFullCusProduct({ db: req.db, - sb, + attachParams: attachToInsertParams(attachParams, product), lastInvoiceId: stripeInvoice.id, }), @@ -430,7 +424,7 @@ export const handleOneOffPrices = async ({ logger.info(" 5. Creating invoice from stripe"); await InvoiceService.createInvoiceFromStripe({ - sb, + db: req.db, stripeInvoice: stripeInvoice, internalCustomerId: customer.internal_id, internalEntityId: attachParams.internalEntityId, @@ -471,7 +465,6 @@ export const handleAddProduct = async ({ }: { req: { db: DrizzleCli; - sb: SupabaseClient; logtail: any; }; res: any; @@ -503,7 +496,6 @@ export const handleAddProduct = async ({ // 1. Handle one-off payment products if (pricesOnlyOneOff(prices)) { await handleOneOffPrices({ - sb: req.sb, req, res, attachParams, @@ -516,7 +508,6 @@ export const handleAddProduct = async ({ // 2. Get one-off + fixed cycle prices if (prices.length > 0) { await handleBillNowPrices({ - sb: req.sb, attachParams, req, res, @@ -536,7 +527,6 @@ export const handleAddProduct = async ({ batchInsert.push( createFullCusProduct({ db: req.db, - sb: req.sb, attachParams: attachToInsertParams(attachParams, product), subscriptionId: undefined, billLaterOnly: true, diff --git a/server/src/internal/customers/add-product/handleCreateCheckout.ts b/server/src/internal/customers/add-product/handleCreateCheckout.ts index ad5a72699..5ffe5f4da 100644 --- a/server/src/internal/customers/add-product/handleCreateCheckout.ts +++ b/server/src/internal/customers/add-product/handleCreateCheckout.ts @@ -14,14 +14,15 @@ import { getNextStartOfMonthUnix } from "@/internal/products/prices/billingInter import { APIVersion } from "@autumn/shared"; import { SuccessCode } from "@autumn/shared"; import { notNullish } from "@/utils/genUtils.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; export const handleCreateCheckout = async ({ - sb, + db, req, res, attachParams, }: { - sb: SupabaseClient; + db: DrizzleCli; req: any; res: any; attachParams: AttachParams; @@ -55,7 +56,7 @@ export const handleCreateCheckout = async ({ // Insert metadata const metaId = await createCheckoutMetadata({ - sb, + db, attachParams, }); @@ -85,8 +86,6 @@ export const handleCreateCheckout = async ({ ? undefined : checkoutParams.allow_promotion_codes || true; - console.log("Items: ", items); - const checkout = await stripeCli.checkout.sessions.create({ customer: customer.processor.id, line_items: items, @@ -134,31 +133,3 @@ export const handleCreateCheckout = async ({ } return; }; - -// OLD BILLING CYCLE ANCHOR LOGIC -// const nextBillingDateUnix = addBillingIntervalUnix( -// Date.now(), -// itemSets[0].interval -// ); -// console.log( -// "Next billing date", -// format(new Date(nextBillingDateUnix), "dd MMM yyyy HH:mm:ss") -// ); -// console.log( -// "Target unix", -// format(new Date(attachParams.billingAnchor), "dd MMM yyyy HH:mm:ss") -// ); - -// billingCycleAnchorUnixSeconds = subtractFromUnixTillAligned({ -// targetUnix: attachParams.billingAnchor, -// originalUnix: nextBillingDateUnix, -// }); - -// console.log( -// "Billing cycle anchor", -// format(new Date(billingCycleAnchorUnixSeconds), "dd MMM yyyy HH:mm:ss") -// ); - -// billingCycleAnchorUnixSeconds = Math.floor( -// billingCycleAnchorUnixSeconds / 1000 -// ); diff --git a/server/src/internal/customers/add-product/handleExistingProduct.ts b/server/src/internal/customers/add-product/handleExistingProduct.ts index 108914599..3d3194190 100644 --- a/server/src/internal/customers/add-product/handleExistingProduct.ts +++ b/server/src/internal/customers/add-product/handleExistingProduct.ts @@ -163,7 +163,7 @@ export const handleExistingProduct = async ({ invoiceOnly?: boolean; isCustom?: boolean; }): Promise<{ curCusProduct: FullCusProduct | null; done: boolean }> => { - const { db, sb, logtail: logger } = req; + const { db, logtail: logger } = req; const { products, cusProducts } = attachParams; if (products.length > 1) { @@ -211,7 +211,6 @@ export const handleExistingProduct = async ({ if (curMainProduct?.product.id === product.id) { return await handleSameMainProduct({ db, - sb, curMainProduct, curScheduledProduct, attachParams, @@ -225,7 +224,7 @@ export const handleExistingProduct = async ({ if (curSameProduct && product.is_add_on) { return await handleSameAddOnProduct({ - sb, + db, curSameProduct, curMainProduct: curMainProduct || null, attachParams, diff --git a/server/src/internal/customers/add-product/handleSameProduct.ts b/server/src/internal/customers/add-product/handleSameProduct.ts index 005a3d481..825ca57ac 100644 --- a/server/src/internal/customers/add-product/handleSameProduct.ts +++ b/server/src/internal/customers/add-product/handleSameProduct.ts @@ -59,14 +59,12 @@ export const getOptionsToUpdate = ( const updateFeatureQuantity = async ({ db, - sb, org, customer, curCusProduct, optionsToUpdate, }: { db: DrizzleCli; - sb: SupabaseClient; org: Organization; customer: Customer; curCusProduct: FullCusProduct; @@ -85,7 +83,7 @@ const updateFeatureQuantity = async ({ for (const options of optionsToUpdate) { const { new: newOptions, old: oldOptions } = options; const subToUpdate = await getUsageBasedSub({ - sb: sb, + db, stripeCli: stripeCli, subIds: curCusProduct.subscription_ids || [], feature: { @@ -213,7 +211,6 @@ export const hasEntitlementsChanged = ({ export const handleSameMainProduct = async ({ db, - sb, curScheduledProduct, curMainProduct, attachParams, @@ -222,7 +219,6 @@ export const handleSameMainProduct = async ({ res, }: { db: DrizzleCli; - sb: SupabaseClient; curScheduledProduct: any; curMainProduct: FullCusProduct; attachParams: AttachParams; @@ -320,7 +316,6 @@ export const handleSameMainProduct = async ({ if (curScheduledProduct) { await cancelFutureProductSchedule({ db, - sb, org, stripeCli, cusProducts: attachParams.cusProducts!, @@ -364,7 +359,6 @@ export const handleSameMainProduct = async ({ if (optionsToUpdate.length > 0) { await updateFeatureQuantity({ db, - sb, org, customer, curCusProduct: curMainProduct, diff --git a/server/src/internal/customers/add-product/handleSameProduct/handleSameAddOn.ts b/server/src/internal/customers/add-product/handleSameProduct/handleSameAddOn.ts index 79ca12143..794196e0a 100644 --- a/server/src/internal/customers/add-product/handleSameProduct/handleSameAddOn.ts +++ b/server/src/internal/customers/add-product/handleSameProduct/handleSameAddOn.ts @@ -5,15 +5,16 @@ import { FullCusProduct, ErrCode } from "@autumn/shared"; import { SupabaseClient } from "@supabase/supabase-js"; import { AttachParams } from "../../products/AttachParams.js"; import { getOptionsToUpdate } from "../handleSameProduct.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; export const handleSameAddOnProduct = async ({ - sb, + db, curSameProduct, curMainProduct, attachParams, res, }: { - sb: SupabaseClient; + db: DrizzleCli; curSameProduct: FullCusProduct; curMainProduct: FullCusProduct | null; attachParams: AttachParams; diff --git a/server/src/internal/customers/add-product/initCusEnt.ts b/server/src/internal/customers/add-product/initCusEnt.ts index 8e002cb00..8d55be629 100644 --- a/server/src/internal/customers/add-product/initCusEnt.ts +++ b/server/src/internal/customers/add-product/initCusEnt.ts @@ -339,7 +339,7 @@ export const initCusEntitlement = ({ unlimited: isBooleanFeature ? null : entitlement.allowance_type === AllowanceType.Unlimited, - balance: newBalance, + balance: newBalance || 0, entities: newEntities, usage_allowed: usageAllowed, next_reset_at: nextResetAtValue, diff --git a/server/src/internal/customers/change-product/billRemainingUsages.ts b/server/src/internal/customers/change-product/billRemainingUsages.ts index 1ba50fe28..da84acbbc 100644 --- a/server/src/internal/customers/change-product/billRemainingUsages.ts +++ b/server/src/internal/customers/change-product/billRemainingUsages.ts @@ -128,7 +128,6 @@ const invoiceForUsageImmediately = async ({ customer, org, logger, - sb, curCusProduct, attachParams, newSubs, @@ -138,7 +137,6 @@ const invoiceForUsageImmediately = async ({ customer: any; org: any; logger: any; - sb: SupabaseClient; curCusProduct: FullCusProduct; attachParams: AttachParams; newSubs: Stripe.Subscription[]; @@ -249,7 +247,7 @@ const invoiceForUsageImmediately = async ({ if (newInvoice) { await InvoiceService.createInvoiceFromStripe({ - sb, + db, stripeInvoice: finalizedInvoice, internalCustomerId: customer.internal_id, internalEntityId: curCusProduct.internal_entity_id || undefined, @@ -276,8 +274,8 @@ const invoiceForUsageImmediately = async ({ } else { // Update invoice await InvoiceService.updateByStripeId({ - sb, - stripeInvoiceId: invoice.id, + db, + stripeId: invoice.id, updates: { total: Number((finalizedInvoice.total / 100).toFixed(2)), }, @@ -316,7 +314,6 @@ const getRemainingUsagesPreview = async ({ export const billForRemainingUsages = async ({ db, logger, - sb, attachParams, curCusProduct, newSubs, @@ -325,7 +322,6 @@ export const billForRemainingUsages = async ({ }: { db: DrizzleCli; logger: any; - sb: any; attachParams: AttachParams; curCusProduct: FullCusProduct; newSubs: Stripe.Subscription[]; @@ -420,7 +416,6 @@ export const billForRemainingUsages = async ({ customer, org, logger, - sb, curCusProduct, attachParams, newSubs, diff --git a/server/src/internal/customers/change-product/changeProductUtils.ts b/server/src/internal/customers/change-product/changeProductUtils.ts index 7f5eec987..9dac0f63c 100644 --- a/server/src/internal/customers/change-product/changeProductUtils.ts +++ b/server/src/internal/customers/change-product/changeProductUtils.ts @@ -33,7 +33,6 @@ export const cancelScheduledProductIfExists = async ({ // 1. Cancel future product schedule await cancelFutureProductSchedule({ db: req.db, - sb: req.sb, org, cusProducts: attachParams.cusProducts!, product: curScheduledProduct.product as any, diff --git a/server/src/internal/customers/change-product/handleDowngrade.ts b/server/src/internal/customers/change-product/handleDowngrade.ts index 557304ef9..f2ce4da58 100644 --- a/server/src/internal/customers/change-product/handleDowngrade.ts +++ b/server/src/internal/customers/change-product/handleDowngrade.ts @@ -24,15 +24,16 @@ import { SupabaseClient } from "@supabase/supabase-js"; import { SuccessCode } from "@autumn/shared"; import { cancelCurSubs } from "./handleDowngrade/cancelCurSubs.js"; import { getScheduleIdsFromCusProducts } from "./scheduleUtils.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; const scheduleStripeSubscription = async ({ - sb, + db, attachParams, stripeCli, itemSet, endOfBillingPeriod, }: { - sb: SupabaseClient; + db: DrizzleCli; attachParams: AttachParams; stripeCli: Stripe; itemSet: ItemSet; @@ -73,7 +74,7 @@ const scheduleStripeSubscription = async ({ }); await SubService.createSub({ - sb: sb, + db, sub: { id: generateId("sub"), stripe_id: null, @@ -177,7 +178,7 @@ export const handleDowngrade = async ({ stripeCli, cusProducts: [curCusProduct, attachParams.curScheduledProduct], itemSet: itemSet, - sb: req.sb, + db: req.db, org: attachParams.org, env: attachParams.customer.env, }); @@ -204,11 +205,11 @@ export const handleDowngrade = async ({ ); let scheduleId = await scheduleStripeSubscription({ + db: req.db, attachParams, stripeCli, itemSet, endOfBillingPeriod: latestPeriodEnd, - sb: req.sb, }); scheduledIds.push(scheduleId); @@ -244,7 +245,6 @@ export const handleDowngrade = async ({ const newProductFree = isFreeProduct(attachParams.prices); await createFullCusProduct({ db: req.db, - sb: req.sb, attachParams: attachToInsertParams(attachParams, product), startsAt: latestPeriodEnd * 1000, subscriptionScheduleIds: scheduledIds, @@ -280,9 +280,3 @@ export const handleDowngrade = async ({ }); } }; - -// await removePreviousScheduledProducts({ -// sb: req.sb, -// stripeCli, -// attachParams, -// }); diff --git a/server/src/internal/customers/change-product/handleUpgrade.ts b/server/src/internal/customers/change-product/handleUpgrade.ts index cca73b551..f6cd7a98a 100644 --- a/server/src/internal/customers/change-product/handleUpgrade.ts +++ b/server/src/internal/customers/change-product/handleUpgrade.ts @@ -47,6 +47,7 @@ import { import { differenceInSeconds } from "date-fns"; import { SuccessCode } from "@autumn/shared"; import { formatUnixToDateTime, notNullish } from "@/utils/genUtils.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; export enum ProrationBehavior { Immediately = "immediately", @@ -56,7 +57,7 @@ export enum ProrationBehavior { // UPGRADE FUNCTIONS export const handleStripeSubUpdate = async ({ - sb, + db, stripeCli, curCusProduct, attachParams, @@ -67,7 +68,7 @@ export const handleStripeSubUpdate = async ({ prorationBehavior = ProrationBehavior.Immediately, shouldPreview = false, }: { - sb: any; + db: DrizzleCli; stripeCli: Stripe; curCusProduct: FullCusProduct; attachParams: AttachParams; @@ -120,7 +121,7 @@ export const handleStripeSubUpdate = async ({ // 3. Update current subscription let newSubs = []; const subUpdateRes = await updateStripeSubscription({ - sb, + db, stripeCli, subscriptionId: firstSub.id, trialEnd, @@ -178,7 +179,7 @@ export const handleStripeSubUpdate = async ({ stripeCli, cusProducts: [curCusProduct, attachParams.curScheduledProduct], itemSet, - sb, + db, org: attachParams.org, env: attachParams.customer.env, }); @@ -187,7 +188,7 @@ export const handleStripeSubUpdate = async ({ // what's happening here... await attachParamsToInvoice({ - sb, + db, attachParams, invoiceId: subUpdate.latest_invoice as string, logger, @@ -233,7 +234,7 @@ export const handleStripeSubUpdate = async ({ } const newSub = (await createStripeSub({ - sb, + db, stripeCli, customer: attachParams.customer, org: attachParams.org, @@ -287,7 +288,6 @@ const handleOnlyEntsChanged = async ({ await createFullCusProduct({ db: req.db, - sb: req.sb, attachParams: attachToInsertParams(attachParams, attachParams.products[0]), subscriptionIds: curCusProduct.subscription_ids || [], disableFreeTrial: false, @@ -330,7 +330,9 @@ export const handleUpgrade = async ({ newVersion = false, updateSameProduct = false, }: { - req: any; + req: { + db: DrizzleCli; + } & any; res: any; attachParams: AttachParams; curCusProduct: FullCusProduct; @@ -437,7 +439,7 @@ export const handleUpgrade = async ({ remainingExistingSubIds, newSubs, }: any = await handleStripeSubUpdate({ - sb: req.sb, + db: req.db, curCusProduct, stripeCli, attachParams, @@ -451,7 +453,6 @@ export const handleUpgrade = async ({ logger.info("2. Bill for remaining usages"); await billForRemainingUsages({ db: req.db, - sb: req.sb, attachParams, curCusProduct, newSubs, @@ -489,7 +490,6 @@ export const handleUpgrade = async ({ await createFullCusProduct({ db: req.db, - sb: req.sb, attachParams: attachToInsertParams(attachParams, products[0]), subscriptionIds: newSubIds, @@ -521,7 +521,7 @@ export const handleUpgrade = async ({ }); await InvoiceService.createInvoiceFromStripe({ - sb: req.sb, + db: req.db, stripeInvoice, internalCustomerId: customer.internal_id, internalEntityId: attachParams.internalEntityId, diff --git a/server/src/internal/customers/change-product/scheduleUtils.ts b/server/src/internal/customers/change-product/scheduleUtils.ts index 2a2af46d5..a22a0144b 100644 --- a/server/src/internal/customers/change-product/scheduleUtils.ts +++ b/server/src/internal/customers/change-product/scheduleUtils.ts @@ -65,7 +65,6 @@ export const getScheduleIdsFromCusProducts = ({ // CANCELLING FUTURE PRODUCT export const cancelFutureProductSchedule = async ({ db, - sb, org, stripeCli, cusProducts, @@ -79,7 +78,6 @@ export const cancelFutureProductSchedule = async ({ sendWebhook = true, }: { db: DrizzleCli; - sb: SupabaseClient; org: Organization; stripeCli: Stripe; cusProducts: FullCusProduct[]; @@ -169,7 +167,7 @@ export const cancelFutureProductSchedule = async ({ cusProducts: [curMainProduct, curScheduledProduct], stripeCli: stripeCli, itemSet: null, - sb: sb, + db, org: org, env: env, }); @@ -256,7 +254,6 @@ export const cancelFutureProductSchedule = async ({ if (otherCusProductsWithSameSub.length > 0) { await addCurMainProductToSchedule({ db, - sb, org, env, stripeCli, diff --git a/server/src/internal/customers/change-product/scheduleUtils/cancelScheduledFreeProduct.ts b/server/src/internal/customers/change-product/scheduleUtils/cancelScheduledFreeProduct.ts index ab502e8ab..198f4a5f5 100644 --- a/server/src/internal/customers/change-product/scheduleUtils/cancelScheduledFreeProduct.ts +++ b/server/src/internal/customers/change-product/scheduleUtils/cancelScheduledFreeProduct.ts @@ -48,7 +48,6 @@ export const getOtherCusProductsOnSub = async ({ // If other cus products on schedule, add cur main product regular items to schedule... export const addCurMainProductToSchedule = async ({ db, - sb, org, env, stripeCli, @@ -58,7 +57,6 @@ export const addCurMainProductToSchedule = async ({ logger, }: { db: DrizzleCli; - sb: SupabaseClient; org: Organization; env: AppEnv; stripeCli: Stripe; @@ -87,7 +85,7 @@ export const addCurMainProductToSchedule = async ({ cusProducts: [], stripeCli: stripeCli, itemSet: null, - sb: sb, + db, org: org, env: env, }); diff --git a/server/src/internal/customers/change-product/scheduleUtils/updateScheduleWithNewItems.ts b/server/src/internal/customers/change-product/scheduleUtils/updateScheduleWithNewItems.ts index 190147a40..503a9ca1e 100644 --- a/server/src/internal/customers/change-product/scheduleUtils/updateScheduleWithNewItems.ts +++ b/server/src/internal/customers/change-product/scheduleUtils/updateScheduleWithNewItems.ts @@ -1,13 +1,13 @@ +import Stripe from "stripe"; import { SubService } from "@/internal/subscriptions/SubService.js"; import { ItemSet } from "@/utils/models/ItemSet.js"; import { FullCusProduct, Organization } from "@autumn/shared"; -import { SupabaseClient } from "@supabase/supabase-js"; import { AppEnv } from "autumn-js"; -import Stripe from "stripe"; import { getFilteredScheduleItems } from "./getFilteredScheduleItems.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; export const updateScheduledSubWithNewItems = async ({ - sb, + db, scheduleObj, newItems, cusProducts, @@ -16,7 +16,7 @@ export const updateScheduledSubWithNewItems = async ({ org, env, }: { - sb: SupabaseClient; + db: DrizzleCli; scheduleObj: any; newItems: any[]; cusProducts: (FullCusProduct | null | undefined)[]; @@ -40,7 +40,7 @@ export const updateScheduledSubWithNewItems = async ({ .concat( ...newItems.map((item: any) => ({ price: item.price, - })) + })), ); await stripeCli.subscriptionSchedules.update(schedule.id, { @@ -55,7 +55,7 @@ export const updateScheduledSubWithNewItems = async ({ // Update sub schedule ID if (itemSet) { await SubService.addUsageFeatures({ - sb, + db, scheduleId: scheduleObj.schedule.id, usageFeatures: itemSet.usageFeatures, orgId: org.id, diff --git a/server/src/internal/customers/cusUtils/cusResponseUtils.ts b/server/src/internal/customers/cusUtils/cusResponseUtils.ts index 4681addca..ae1e2b9d1 100644 --- a/server/src/internal/customers/cusUtils/cusResponseUtils.ts +++ b/server/src/internal/customers/cusUtils/cusResponseUtils.ts @@ -8,6 +8,7 @@ import { FullCusProduct, FullCustomerEntitlement, Organization, + Subscription, } from "@autumn/shared"; import Stripe from "stripe"; import { fullCusProductToCusPrices } from "../products/cusProductUtils.js"; @@ -24,7 +25,7 @@ export const getCusProductsResponse = async ({ }: { cusProducts: FullCusProduct[]; entities: Entity[]; - subs: Stripe.Subscription[]; + subs: (Stripe.Subscription | Subscription)[]; org: Organization; apiVersion: number; }) => { @@ -63,7 +64,7 @@ export const getCusFeaturesResponse = async ({ }); let features = cusEnts.map( - (cusEnt: FullCustomerEntitlement) => cusEnt.entitlement.feature + (cusEnt: FullCustomerEntitlement) => cusEnt.entitlement.feature, ); let entList: any = balances.map((b) => { diff --git a/server/src/internal/customers/cusUtils/getOrCreateCustomer.ts b/server/src/internal/customers/cusUtils/getOrCreateCustomer.ts index 2776ae0e5..e545553a0 100644 --- a/server/src/internal/customers/cusUtils/getOrCreateCustomer.ts +++ b/server/src/internal/customers/cusUtils/getOrCreateCustomer.ts @@ -21,7 +21,6 @@ import { DrizzleCli } from "@/db/initDrizzle.js"; export const getOrCreateCustomer = async ({ db, - sb, org, features, customerId, @@ -42,7 +41,6 @@ export const getOrCreateCustomer = async ({ entityData, }: { db: DrizzleCli; - sb: SupabaseClient; org: Organization; features: Feature[]; env: AppEnv; @@ -59,8 +57,8 @@ export const getOrCreateCustomer = async ({ let customer; if (!skipGet) { - customer = await CusService.getWithProducts({ - sb, + customer = await CusService.getFull({ + db, idOrInternalId: customerId, orgId: org.id, env, @@ -68,6 +66,7 @@ export const getOrCreateCustomer = async ({ withEntities, entityId, expand, + allowNotFound: true, }); } @@ -83,14 +82,13 @@ export const getOrCreateCustomer = async ({ fingerprint: customerData?.fingerprint, metadata: customerData?.metadata || {}, }, - sb, org, env, logger, }); - customer = await CusService.getWithProducts({ - sb, + customer = await CusService.getFull({ + db, idOrInternalId: customerId || customer!.internal_id, orgId: org.id, env, @@ -101,8 +99,8 @@ export const getOrCreateCustomer = async ({ }); } catch (error: any) { if (error?.data?.code == "23505") { - customer = await CusService.getWithProducts({ - sb, + customer = await CusService.getFull({ + db, idOrInternalId: customerId, orgId: org.id, env, @@ -129,7 +127,6 @@ export const getOrCreateCustomer = async ({ let newEntities = await createEntities({ db, - sb, org, customerId, createEntityData: { diff --git a/server/src/internal/customers/entitlements/CusEntitlementService.ts b/server/src/internal/customers/entitlements/CusEntitlementService.ts index f3c3721ef..445e52819 100644 --- a/server/src/internal/customers/entitlements/CusEntitlementService.ts +++ b/server/src/internal/customers/entitlements/CusEntitlementService.ts @@ -12,20 +12,18 @@ import { FullCusEntWithProduct, FullCustomerEntitlement, } from "@autumn/shared"; -import { customerEntitlements } from "@shared/models/cusProductModels/cusEntModels/cusEntTable.js"; +import { customerEntitlements } from "@autumn/shared"; import { SupabaseClient } from "@supabase/supabase-js"; import { StatusCodes } from "http-status-codes"; import { Client } from "pg"; import { eq, lt, and, sql } from "drizzle-orm"; -import { customerProducts } from "@shared/models/cusProductModels/cusProductTable.js"; +import { customerProducts } from "@autumn/shared"; export class CusEntService { static async getByFeature({ - // sb, db, internalFeatureId, }: { - // sb: SupabaseClient; db: DrizzleCli; internalFeatureId: string; }) { @@ -172,86 +170,3 @@ export class CusEntService { return data; } } - -// static async getActiveResetPassed({ -// sb, -// customDateUnix, -// }: { -// sb: SupabaseClient; -// customDateUnix?: number; -// }) { -// const { data, error } = await sb -// .from("customer_entitlements") -// .select( -// "*, customer_product:customer_products!inner(*), entitlement:entitlements(*)", -// ) -// .eq("customer_product.status", "active") -// .lt("next_reset_at", customDateUnix ? customDateUnix : Date.now()); - -// if (error) { -// throw error; -// } - -// return data; -// } - -// static async update({ -// sb, -// id, -// updates, -// }: { -// sb: SupabaseClient; -// id: string; -// updates: Partial; -// }) { -// const { data, error } = await sb -// .from("customer_entitlements") -// .update(updates) -// .eq("id", id) -// .select(); - -// if (error) { -// throw error; -// } - -// return data; -// } - -// static async getByIdStrict({ -// sb, -// id, -// orgId, -// env, -// withCusProduct = false, -// }: { -// sb: SupabaseClient; -// id: string; -// orgId: string; -// env: string; -// withCusProduct?: boolean; -// }) { -// let selectQuery = `*, entitlement:entitlements!inner(*, feature:features!inner(*)), customer:customers!inner(*)${ -// withCusProduct ? ", customer_product:customer_products!inner(*)" : "" -// }`; - -// const { data, error } = await sb -// .from("customer_entitlements") -// .select(selectQuery as "*") // hack to kill generic string error -// .eq("id", id) -// .eq("customer.org_id", orgId) -// .eq("customer.env", env) -// .single(); - -// if (error) { -// if (error.code === "PGRST116") { -// throw new RecaseError({ -// message: "Customer entitlement not found", -// code: ErrCode.CustomerEntitlementNotFound, -// statusCode: StatusCodes.NOT_FOUND, -// }); -// } -// throw error; -// } - -// return data as FullCustomerEntitlement; -// } diff --git a/server/src/internal/customers/getFullCusQuery.ts b/server/src/internal/customers/getFullCusQuery.ts new file mode 100644 index 000000000..8fea4980a --- /dev/null +++ b/server/src/internal/customers/getFullCusQuery.ts @@ -0,0 +1,308 @@ +import { AppEnv } from "@autumn/shared"; +import { CusProductStatus } from "@autumn/shared"; +import { sql, SQL } from "drizzle-orm"; + +/** + To check: + 1. If entityId provided but not found, invoices returns invoices for customer, not empty? + + 2. + */ + +const buildCusProductsCTE = (inStatuses?: CusProductStatus[]) => { + const withCusPrices = () => { + return sql` + SELECT json_agg(price_data) + FROM ( + SELECT + cpr.*, + row_to_json(p) AS price + FROM customer_prices cpr + JOIN prices p ON cpr.price_id = p.id + WHERE cpr.customer_product_id = cp.id + ) AS price_data + `; + }; + + const withCusEntitlements = () => { + // SELECT COALESCE( + // json_agg(entitlement_data) FILTER (WHERE entitlement_data IS NOT NULL), + // '[]'::json + // ) + return sql` + SELECT json_agg(entitlement_data) + FROM ( + SELECT + ce.*, + ( + SELECT row_to_json(ent_with_feature) + FROM ( + SELECT + e.*, + row_to_json(f) AS feature + FROM entitlements e + JOIN features f ON e.internal_feature_id = f.internal_id + WHERE e.id = ce.entitlement_id + ) AS ent_with_feature + ) AS entitlement + FROM customer_entitlements ce + WHERE ce.customer_product_id = cp.id + ) AS entitlement_data + `; + }; + + const withFreeTrial = () => { + return sql` + SELECT row_to_json(ft) + FROM free_trials ft + WHERE ft.id = cp.free_trial_id + `; + }; + + const withStatusFilter = () => { + return inStatuses + ? sql`AND cp.status = ANY(ARRAY[${sql.join( + inStatuses.map((status) => sql`${status}`), + sql`, `, + )}])` + : sql``; + }; + + return sql` + customer_products_with_prices AS ( + SELECT + cp.*, + row_to_json(prod) AS product, + (${withCusPrices()}) AS customer_prices, + (${withCusEntitlements()}) AS customer_entitlements, + (${withFreeTrial()}) AS free_trial + + FROM customer_products cp + JOIN products prod ON cp.internal_product_id = prod.internal_id + + WHERE cp.internal_customer_id = (SELECT internal_id FROM customer_record) + ${withStatusFilter()} + ) +`; +}; + +const buildEntitiesCTE = (withEntities: boolean) => { + if (!withEntities) { + return sql``; + } + + return sql` + customer_entities AS ( + SELECT + COALESCE( + json_agg(row_to_json(e)) FILTER (WHERE e.id IS NOT NULL), + '[]'::json + ) AS entities + FROM entities e + WHERE e.internal_customer_id = (SELECT internal_id FROM customer_record) + ) + `; +}; + +const buildEntityCTE = (entityId?: string) => { + if (!entityId) { + return sql``; + } + + return sql` + entity_record AS ( + SELECT * FROM entities e + WHERE e.internal_customer_id = (SELECT internal_id FROM customer_record) + AND ( + e.id = ${entityId} OR e.internal_id = ${entityId} + ) + LIMIT 1 + ) + `; +}; + +const buildTrialsUsedCTE = ( + withTrialsUsed: boolean, + orgId: string, + env: AppEnv, +) => { + if (!withTrialsUsed) { + return sql``; + } + + return sql` + customer_trials_used AS ( + SELECT + COALESCE( + json_agg(json_build_object( + 'product_id', p.id, + 'fingerprint', c.fingerprint, + 'customer_id', c.id + )) FILTER (WHERE p.id IS NOT NULL), + '[]'::json + ) AS trials_used + FROM customer_products cp + JOIN products p ON cp.internal_product_id = p.internal_id + JOIN customers c ON cp.internal_customer_id = c.internal_id + WHERE (c.id = (SELECT id FROM customer_record) OR (c.fingerprint IS NOT NULL AND c.fingerprint = (SELECT fingerprint FROM customer_record))) + AND p.org_id = ${orgId} + AND p.env = ${env} + AND cp.free_trial_id IS NOT NULL + ) + `; +}; + +const buildSubscriptionsCTE = ( + withSubs: boolean, + inStatuses?: CusProductStatus[], +) => { + if (!withSubs) { + return sql``; + } + + return sql` + customer_subscriptions AS ( + SELECT + COALESCE( + json_agg(row_to_json(s)) FILTER (WHERE s.stripe_id IS NOT NULL), + '[]'::json + ) AS subscriptions + FROM subscriptions s + WHERE EXISTS ( + SELECT 1 FROM customer_products_with_prices cpwp + WHERE cpwp.subscription_ids @> ARRAY[s.stripe_id] + ) + ) + `; +}; + +const buildInvoicesCTE = (hasEntityCTE: boolean) => { + let entityFilter = hasEntityCTE + ? sql`AND ( + NOT EXISTS (SELECT 1 FROM entity_record) + OR i.internal_entity_id = (SELECT internal_id FROM entity_record LIMIT 1) + )` + : sql``; + + return sql` + customer_invoices AS ( + SELECT + COALESCE( + json_agg(row_to_json(i)) FILTER (WHERE i.id IS NOT NULL), + '[]'::json + ) AS invoices + FROM invoices i + WHERE i.internal_customer_id = (SELECT internal_id FROM customer_record) + ${entityFilter} + LIMIT 10 + ) + `; +}; + +export const getFullCusQuery = ( + idOrInternalId: string, + orgId: string, + env: AppEnv, + inStatuses: CusProductStatus[], + includeInvoices: boolean, + withEntities: boolean, + withTrialsUsed: boolean, + withSubs: boolean, + entityId?: string, +) => { + const sqlChunks: SQL[] = []; + + // Step 1: Get customer record + sqlChunks.push(sql` + WITH customer_record AS ( + SELECT * FROM customers c + WHERE ( + c.id = ${idOrInternalId} OR c.internal_id = ${idOrInternalId} + ) + AND c.org_id = ${orgId} + AND c.env = ${env} + ORDER BY (c.id = ${idOrInternalId}) DESC + LIMIT 1 + ) + `); + + // Step 2: Get entities + if (withEntities) { + sqlChunks.push(sql`, `); + sqlChunks.push(buildEntitiesCTE(withEntities)); + } + + // Step 3: Get entity + if (entityId) { + sqlChunks.push(sql`, `); + sqlChunks.push(buildEntityCTE(entityId)); + } + + // Add customer products CTE + sqlChunks.push(sql`, `); + sqlChunks.push(buildCusProductsCTE(inStatuses)); + + // Conditionally add trials used CTE + if (withTrialsUsed) { + sqlChunks.push(sql`, `); + sqlChunks.push(buildTrialsUsedCTE(withTrialsUsed, orgId, env)); + } + + // Conditionally add subscriptions CTE + if (withSubs) { + sqlChunks.push(sql`, `); + sqlChunks.push(buildSubscriptionsCTE(withSubs, inStatuses)); + } + + // Conditionally add invoices CTE + if (includeInvoices) { + sqlChunks.push(sql`, `); + sqlChunks.push(buildInvoicesCTE(!!entityId)); + } + + // Build final SELECT + const selectFieldsChunks: SQL[] = []; + selectFieldsChunks.push(sql` + cr.*, + COALESCE( + (SELECT json_agg(cpwp) FROM customer_products_with_prices cpwp), + '[]'::json + ) AS customer_products + `); + + // Add entities to SELECT if withEntities is true + if (withEntities) { + selectFieldsChunks.push(sql`, + (SELECT entities FROM customer_entities) AS entities`); + } + + // Add entity to SELECT if entityId is provided + if (entityId) { + selectFieldsChunks.push(sql`, + (SELECT row_to_json(er) FROM entity_record er LIMIT 1) AS entity`); + } + + // Add trials used to SELECT if withTrialsUsed is true + if (withTrialsUsed) { + selectFieldsChunks.push(sql`, + (SELECT trials_used FROM customer_trials_used) AS trials_used`); + } + + // Add subscriptions to SELECT if withSubs is true + if (withSubs) { + selectFieldsChunks.push(sql`, + (SELECT subscriptions FROM customer_subscriptions) AS subscriptions`); + } + + if (includeInvoices) { + selectFieldsChunks.push(sql`, + (SELECT invoices FROM customer_invoices) AS invoices`); + } + + sqlChunks.push(sql` + SELECT ${sql.join(selectFieldsChunks, sql``)} + FROM customer_record cr + `); + + return sql.join(sqlChunks, sql``); +}; diff --git a/server/src/internal/customers/internalCusRouter.ts b/server/src/internal/customers/internalCusRouter.ts index e6ba208a1..99039f9c2 100644 --- a/server/src/internal/customers/internalCusRouter.ts +++ b/server/src/internal/customers/internalCusRouter.ts @@ -46,48 +46,22 @@ export const cusRouter = Router(); // } // }); -cusRouter.post("/search", async (req: any, res: any) => { - const { pg, sb, orgId, env } = req; - const { search, page, filters } = req.body; - - const pageInt = parseInt(page as string) || 1; - const cleanedQuery = search ? search.trim().toLowerCase() : ""; - - try { - const { data: customers, count } = await CusService.searchCustomers({ - sb, - pg, - orgId: orgId, - env, - search: cleanedQuery, - pageNumber: pageInt, - filters, - }); - - // console.log("customers", customers); - res.status(200).json({ customers, totalCount: count }); - } catch (error) { - // handleRequestError({ req, error, res, action: "search customers" }); - handleFrontendReqError({ req, error, res, action: "search customers" }); - } -}); - cusRouter.get("/:customer_id/data", async (req: any, res: any) => { try { - const { db, sb, org, features, env } = req; + const { db, org, features, env } = req; const { customer_id } = req.params; const orgId = req.orgId; const [coupons, products, customer] = await Promise.all([ - RewardService.getAll({ - sb, + RewardService.list({ + db, orgId: orgId, env, }), ProductService.listFull({ db, orgId, env, returnAll: true }), - CusService.getWithProducts({ - sb, + CusService.getFull({ + db, orgId, env, idOrInternalId: customer_id, @@ -114,8 +88,6 @@ cusRouter.get("/:customer_id/data", async (req: any, res: any) => { limit: 10, }); - console.log("events", events); - let fullCustomer = customer as any; let cusProducts = fullCustomer.customer_products; fullCustomer.products = fullCustomer.customer_products; @@ -155,7 +127,7 @@ cusRouter.get("/:customer_id/data", async (req: any, res: any) => { } } - for (const invoice of invoices) { + for (const invoice of invoices || []) { invoice.product_ids = invoice.product_ids.sort(); invoice.internal_product_ids = invoice.internal_product_ids.sort(); } @@ -208,7 +180,7 @@ cusRouter.get("/:customer_id/data", async (req: any, res: any) => { cusRouter.get("/:customer_id/referrals", async (req: any, res: any) => { try { - const { sb, org, env, db } = req; + const { env, db } = req; const { customer_id } = req.params; const orgId = req.orgId; @@ -230,13 +202,13 @@ cusRouter.get("/:customer_id/referrals", async (req: any, res: any) => { // Get all redemptions for this customer let [referred, redeemed] = await Promise.all([ RewardRedemptionService.getByReferrer({ - sb, + db, internalCustomerId: internalCustomer.internal_id, withCustomer: true, limit: 100, }), RewardRedemptionService.getByCustomer({ - sb, + db, internalCustomerId: internalCustomer.internal_id, withReferralCode: true, limit: 100, @@ -248,16 +220,18 @@ cusRouter.get("/:customer_id/referrals", async (req: any, res: any) => { ); let redeemedCustomers = await CusReadService.getInInternalIds({ - sb, + db, internalIds: redeemedCustomerIds, }); for (const redemption of redeemed) { - redemption.referral_code.customer = redeemedCustomers.find( - (customer: any) => - customer.internal_id === - redemption.referral_code.internal_customer_id, - ); + if (redemption.referral_code) { + redemption.referral_code.customer = redeemedCustomers.find( + (customer: any) => + customer.internal_id === + redemption.referral_code!.internal_customer_id, + ); + } } res.status(200).send({ @@ -278,13 +252,13 @@ cusRouter.get( "/:customer_id/product/:product_id", async (req: any, res: any) => { try { - const { sb, org, env, db } = req; + const { 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, + const customer = await CusService.getFull({ + db, orgId, env, idOrInternalId: customer_id, diff --git a/server/src/internal/customers/invoices/InvoiceItemService.ts b/server/src/internal/customers/invoices/InvoiceItemService.ts deleted file mode 100644 index 24dbee8b2..000000000 --- a/server/src/internal/customers/invoices/InvoiceItemService.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { InvoiceItem, AppEnv, CusProduct } from "@autumn/shared"; - -import { SupabaseClient } from "@supabase/supabase-js"; - -export class InvoiceItemService { - static async getLatestInvoiceItem({ - sb, - cusPriceId, - periodStart, - }: { - sb: SupabaseClient; - cusPriceId: string; - periodStart: number; - }) { - // 1. Fetch latest invoice item - let { data, error } = await sb - .from("invoice_items") - .select("*") - .eq("customer_price_id", cusPriceId) - .order("created_at", { ascending: false }) - .gte("period_start", periodStart) - .limit(1); - - if (error) { - throw error; - } - - if (!data || data.length == 0) { - return null; - } - - return data[0]; - } - - static async getNotAddedToStripe({ - sb, - cusPriceId, - }: { - sb: SupabaseClient; - cusPriceId: string; - }) { - let { data, error } = await sb - .from("invoice_items") - .select("*") - .eq("customer_price_id", cusPriceId) - .eq("added_to_stripe", false); - - if (error) { - throw error; - } - - if (!data || data.length == 0) { - return null; - } - - if (data.length > 1) { - console.log("❗️ More than one invoice item not added to stripe"); - console.log(data); - } - - return data[0]; - } - - static async update({ - sb, - invoiceItemId, - updates, - }: { - sb: SupabaseClient; - invoiceItemId: string; - updates: any; - }) { - let { data, error } = await sb - .from("invoice_items") - .update(updates) - .eq("id", invoiceItemId); - - if (error) { - throw error; - } - - return data; - } - - static async insert({ - sb, - data, - }: { - sb: SupabaseClient; - data: InvoiceItem | InvoiceItem[]; - }) { - let { error } = await sb.from("invoice_items").insert(data); - - if (error) { - throw error; - } - } -} diff --git a/server/src/internal/customers/invoices/InvoiceService.ts b/server/src/internal/customers/invoices/InvoiceService.ts index 36e822d64..903b9407e 100644 --- a/server/src/internal/customers/invoices/InvoiceService.ts +++ b/server/src/internal/customers/invoices/InvoiceService.ts @@ -11,10 +11,13 @@ import { } from "@autumn/shared"; import Stripe from "stripe"; import { generateId } from "@/utils/genUtils.js"; -// import { Autumn } from "@/external/autumn/autumnCli.js"; + import { getInvoiceDiscounts } from "@/external/stripe/stripeInvoiceUtils.js"; import { createLogtailWithContext } from "@/external/logtail/logtailUtils.js"; import { Autumn } from "autumn-js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { invoices } from "@autumn/shared"; +import { and, desc, eq } from "drizzle-orm"; export const processInvoice = ({ invoice, @@ -52,88 +55,49 @@ export const processInvoice = ({ }; export class InvoiceService { - static async getByInternalCustomerId({ - sb, + static async list({ + db, internalCustomerId, internalEntityId, limit = 100, }: { - sb: SupabaseClient; + db: DrizzleCli; internalCustomerId: string; internalEntityId?: string; limit?: number; }) { - let query = sb - .from("invoices") - .select("*") - .eq("internal_customer_id", internalCustomerId); - - if (internalEntityId) { - query = query.eq("internal_entity_id", internalEntityId); - } - - query = query.order("created_at", { ascending: false }).limit(limit); - - const { data, error } = await query; - - if (error) { - throw error; - } - - return data; + return (await db.query.invoices.findMany({ + where: and( + eq(invoices.internal_customer_id, internalCustomerId), + internalEntityId + ? eq(invoices.internal_entity_id, internalEntityId) + : undefined, + ), + orderBy: [desc(invoices.created_at)], + limit, + })) as Invoice[]; } - static async createInvoice({ - sb, - invoice, + static async getByStripeId({ + db, + stripeId, }: { - sb: SupabaseClient; - invoice: Invoice; + db: DrizzleCli; + stripeId: string; }) { - const { error } = await sb.from("invoices").insert(invoice); - if (error) { - throw error; - } - } + const invoice = await db.query.invoices.findFirst({ + where: eq(invoices.stripe_id, stripeId), + }); - static async getById({ sb, id }: { sb: SupabaseClient; id: string }) { - const { data, error } = await sb - .from("invoices") - .select("*") - .eq("id", id) - .single(); - - if (error) { - throw error; + if (!invoice) { + return null; } - return data; - } - - static async getInvoiceByStripeId({ - sb, - stripeInvoiceId, - }: { - sb: SupabaseClient; - stripeInvoiceId: string; - }) { - const { data, error } = await sb - .from("invoices") - .select("*") - .eq("stripe_id", stripeInvoiceId) - .single(); - - if (error) { - if (error.code === "PGRST116") { - return null; - } - throw error; - } - return data; + return invoice as Invoice; } static async createInvoiceFromStripe({ - sb, + db, stripeInvoice, internalCustomerId, internalEntityId, @@ -144,7 +108,7 @@ export class InvoiceService { sendRevenueEvent = true, items = [], }: { - sb: SupabaseClient; + db: DrizzleCli; stripeInvoice: Stripe.Invoice; internalCustomerId: string; internalEntityId?: string | null; @@ -188,17 +152,16 @@ export class InvoiceService { items: items, }; - const { error } = await sb.from("invoices").insert(invoice); - - if (error) { + try { + await db.insert(invoices).values(invoice as any); + } catch (error: any) { if (error.code == "23505") { console.log(" 🧐 Invoice already exists"); - - // Update invoice status return; + } else { + console.error(" ❌ Error inserting Stripe invoice: ", error); + throw error; } - console.log(" ❌ Error inserting Stripe invoice: ", error); - return; } console.log(" ✅ Created invoice from stripe"); @@ -225,21 +188,24 @@ export class InvoiceService { } static async updateByStripeId({ - sb, - stripeInvoiceId, + db, + stripeId, updates, }: { - sb: SupabaseClient; - stripeInvoiceId: string; + db: DrizzleCli; + stripeId: string; updates: Partial; }) { - const { error } = await sb - .from("invoices") - .update(updates) - .eq("stripe_id", stripeInvoiceId); + const results = await db + .update(invoices) + .set(updates as any) + .where(eq(invoices.stripe_id, stripeId)) + .returning(); - if (error) { - throw error; + if (results.length === 0) { + return null; } + + return results[0] as Invoice; } } diff --git a/server/src/internal/customers/invoices/invoiceItemUtils.ts b/server/src/internal/customers/invoices/invoiceItemUtils.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/server/src/internal/customers/invoices/invoiceUtils.ts b/server/src/internal/customers/invoices/invoiceUtils.ts index 86f8b8ce8..2d7fc7bbf 100644 --- a/server/src/internal/customers/invoices/invoiceUtils.ts +++ b/server/src/internal/customers/invoices/invoiceUtils.ts @@ -1,26 +1,19 @@ -import { SupabaseClient } from "@supabase/supabase-js"; +import Stripe from "stripe"; import { AttachParams } from "../products/AttachParams.js"; import { InvoiceService, processInvoice } from "./InvoiceService.js"; -import Stripe from "stripe"; import { createStripeCli } from "@/external/stripe/utils.js"; import { getStripeExpandedInvoice } from "@/external/stripe/stripeInvoiceUtils.js"; -import { - Feature, - Invoice, - InvoiceItem, - Price, - PriceType, - UsagePriceConfig, -} from "@autumn/shared"; +import { Invoice, InvoiceItem, Price, UsagePriceConfig } from "@autumn/shared"; +import { DrizzleCli } from "@/db/initDrizzle.js"; export const attachParamsToInvoice = async ({ - sb, + db, attachParams, invoiceId, stripeInvoice, logger, }: { - sb: SupabaseClient; + db: DrizzleCli; attachParams: AttachParams; invoiceId: string; stripeInvoice?: Stripe.Invoice; @@ -40,15 +33,15 @@ export const attachParamsToInvoice = async ({ } // Create or update - let invoice = await InvoiceService.getInvoiceByStripeId({ - sb, - stripeInvoiceId: invoiceId, + let invoice = await InvoiceService.getByStripeId({ + db, + stripeId: invoiceId, }); if (invoice) { await InvoiceService.updateByStripeId({ - sb, - stripeInvoiceId: invoiceId, + db, + stripeId: invoiceId, updates: { product_ids: attachParams.products.map((p) => p.id), internal_product_ids: attachParams.products.map((p) => p.internal_id), @@ -56,7 +49,7 @@ export const attachParamsToInvoice = async ({ }); } else { await InvoiceService.createInvoiceFromStripe({ - sb, + db, stripeInvoice, internalCustomerId: attachParams.customer.internal_id, internalEntityId: attachParams.internalEntityId, @@ -83,40 +76,10 @@ export const invoicesToResponse = ({ invoice: i, withItems: false, features: [], - }) + }), ); }; -export const getInvoicesForResponse = async ({ - sb, - - internalCustomerId, - internalEntityId, - limit = 20, -}: { - sb: SupabaseClient; - internalCustomerId: string; - internalEntityId?: string; - limit?: number; -}) => { - let invoices = await InvoiceService.getByInternalCustomerId({ - sb, - internalCustomerId, - internalEntityId, - limit, - }); - - const processedInvoices = invoices.map((i) => - processInvoice({ - invoice: i, - withItems: false, - features: [], - }) - ); - - return processedInvoices; -}; - export const getInvoiceItems = async ({ stripeInvoice, prices, @@ -134,7 +97,7 @@ export const getInvoiceItems = async ({ (p) => p.config?.stripe_price_id === line.price?.id || (p.config as UsagePriceConfig)?.stripe_product_id === - line.price?.product + line.price?.product, ); if (!price) { @@ -154,7 +117,7 @@ export const getInvoiceItems = async ({ } catch (error) { logger.error( `Failed to get invoice items for invoice ${stripeInvoice.id}`, - error + error, ); return []; } diff --git a/server/src/internal/customers/previews/getUpgradePreview.ts b/server/src/internal/customers/previews/getUpgradePreview.ts index d9cccc43f..20bc996c5 100644 --- a/server/src/internal/customers/previews/getUpgradePreview.ts +++ b/server/src/internal/customers/previews/getUpgradePreview.ts @@ -119,14 +119,12 @@ const formatMessage = ({ const createStripeProductAndPrices = async ({ db, - sb, org, env, product, logger, }: { db: DrizzleCli; - sb: SupabaseClient; org: Organization; env: AppEnv; product: FullProduct; @@ -165,7 +163,6 @@ const createStripeProductAndPrices = async ({ export const getUpgradePreview = async ({ db, - sb, customer, org, env, @@ -175,7 +172,6 @@ export const getUpgradePreview = async ({ logger, }: { db: DrizzleCli; - sb: SupabaseClient; customer: Customer; org: Organization; env: AppEnv; @@ -187,7 +183,6 @@ export const getUpgradePreview = async ({ // Create stripe product / prices if not exist await createStripeProductAndPrices({ db, - sb, org, env, product, @@ -213,7 +208,7 @@ export const getUpgradePreview = async ({ }; let updatePreview = (await handleStripeSubUpdate({ - sb: null, + db, stripeCli, curCusProduct: curMainProduct, attachParams, @@ -234,7 +229,6 @@ export const getUpgradePreview = async ({ (await billForRemainingUsages({ db, logger: console, - sb: null, attachParams, curCusProduct: curMainProduct, newSubs: stripeSubs, diff --git a/server/src/internal/customers/prices/CusPriceService.ts b/server/src/internal/customers/prices/CusPriceService.ts index 09b541498..faa1c5f56 100644 --- a/server/src/internal/customers/prices/CusPriceService.ts +++ b/server/src/internal/customers/prices/CusPriceService.ts @@ -4,7 +4,7 @@ import { FullCustomerEntitlement, FullCustomerPrice, } from "@autumn/shared"; -import { customerPrices } from "@shared/models/cusProductModels/cusPriceModels/cusPriceTable.js"; +import { customerPrices } from "@autumn/shared"; import { eq } from "drizzle-orm"; @@ -43,4 +43,21 @@ export class CusPriceService { await db.insert(customerPrices).values(data as any); } + + static async getByCustomerProductId({ + db, + customerProductId, + }: { + db: DrizzleCli; + customerProductId: string; + }) { + const data = await db.query.customerPrices.findMany({ + where: eq(customerPrices.customer_product_id, customerProductId), + with: { + price: true, + }, + }); + + return data as FullCustomerPrice[]; + } } diff --git a/server/src/internal/customers/products/AttachParams.ts b/server/src/internal/customers/products/AttachParams.ts index 2a82e394b..34e7e15d5 100644 --- a/server/src/internal/customers/products/AttachParams.ts +++ b/server/src/internal/customers/products/AttachParams.ts @@ -50,6 +50,8 @@ export type AttachParams = { checkoutSessionParams?: any; apiVersion?: number; scenario?: AttachScenario; + + fromMigration?: boolean; }; export type InsertCusProductParams = { @@ -80,6 +82,7 @@ export type InsertCusProductParams = { entityId?: string; internalEntityId?: string; + fromMigration?: boolean; }; export const AttachResultSchema = z.object({ diff --git a/server/src/internal/customers/products/CusProdReadService.ts b/server/src/internal/customers/products/CusProdReadService.ts index 33057e720..69e7f3d6e 100644 --- a/server/src/internal/customers/products/CusProdReadService.ts +++ b/server/src/internal/customers/products/CusProdReadService.ts @@ -1,29 +1,14 @@ -import { CusProductStatus, ErrCode } from "@autumn/shared"; +import { CusProductStatus } from "@autumn/shared"; import { DrizzleCli } from "@/db/initDrizzle.js"; -import { customerProducts } from "@shared/models/cusProductModels/cusProductTable.js"; -import { - eq, - and, - isNotNull, - sql, - countDistinct, - count, - or, - inArray, -} from "drizzle-orm"; -import { StatusCodes } from "http-status-codes"; -import RecaseError from "@/utils/errorUtils.js"; -import { SupabaseClient } from "@supabase/supabase-js"; -import assert from "assert"; +import { customerProducts } from "@autumn/shared"; +import { eq, isNotNull, sql, countDistinct, count } from "drizzle-orm"; export class CusProdReadService { static getCounts = async ({ db, internalProductId, - sb, }: { db: DrizzleCli; - sb: SupabaseClient; internalProductId: string; }) => { let result = await db @@ -75,51 +60,3 @@ export class CusProdReadService { return result[0]; }; } - -// let customQuery = db -// .select({ count: countDistinct(customerProducts.internal_customer_id) }) -// .from(customerProducts) -// .where( -// and( -// eq(customerProducts.internal_product_id, internalProductId), -// eq(customerProducts.is_custom, true), -// inArray(customerProducts.status, statuses), -// ), -// ) -// .as("custom"); - -// let trialingQuery = db -// .select({ count: countDistinct(customerProducts.internal_customer_id) }) -// .from(customerProducts) -// .where( -// and( -// eq(customerProducts.internal_product_id, internalProductId), -// isNotNull(customerProducts.trial_ends_at), -// sql`${customerProducts.trial_ends_at} > (EXTRACT(EPOCH FROM NOW()) * 1000)::bigint`, -// inArray(customerProducts.status, statuses), -// ), -// ) -// .as("trialing"); - -// let allQuery = db -// .select({ count: countDistinct(customerProducts.internal_customer_id) }) -// .from(customerProducts) -// .where(eq(customerProducts.internal_product_id, internalProductId)) -// .as("all"); - -// let { data: result, error } = await sb.rpc("get_product_stats", { -// p_internal_id: internalProductId, -// }); - -// if (error) { -// console.error("Error getting counts", error); -// throw error; -// } - -// return { -// active: result.f1, -// canceled: result.f2, -// custom: result.f3, -// trialing: result.f4, -// all: result.f5, -// }; diff --git a/server/src/internal/customers/products/CusProductService.ts b/server/src/internal/customers/products/CusProductService.ts index 3ffc23cee..c8c2f4c9e 100644 --- a/server/src/internal/customers/products/CusProductService.ts +++ b/server/src/internal/customers/products/CusProductService.ts @@ -1,28 +1,19 @@ import { DrizzleCli } from "@/db/initDrizzle.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; -import { isOneOff } from "@/internal/products/productUtils.js"; import RecaseError from "@/utils/errorUtils.js"; import { AppEnv, CusProduct, CusProductStatus, + customers, ErrCode, FullCusProduct, - Organization, products, } from "@autumn/shared"; -import { customerProducts } from "@shared/models/cusProductModels/cusProductTable.js"; -import { SupabaseClient } from "@supabase/supabase-js"; -import { - and, - arrayContained, - arrayContains, - eq, - inArray, - or, - sql, -} from "drizzle-orm"; + +import { customerProducts } from "@autumn/shared"; + +import { and, arrayContains, eq, inArray, or, sql } from "drizzle-orm"; export const ACTIVE_STATUSES = [ CusProductStatus.Active, @@ -368,13 +359,11 @@ export class CusProductService { } static async getByScheduleId({ - // sb, db, scheduleId, orgId, env, }: { - // sb: SupabaseClient; db: DrizzleCli; scheduleId: string; orgId: string; @@ -412,20 +401,6 @@ export class CusProductService { orgId, env, }); - - // const { data, error } = await sb - // .from("customer_products") - // .select("*, product:products!inner(*), customer:customers!inner(*)") - // // .eq("processor->>subscription_schedule_id", scheduleId) - // .contains("scheduled_ids", [scheduleId]) - // .eq("customer.org_id", orgId) - // .eq("customer.env", env); - - // if (error) { - // throw error; - // } - - // return data; } static async update({ @@ -482,36 +457,6 @@ export class CusProductService { })) as FullCusProduct[]; return fullUpdated as FullCusProduct[]; - - // const query = sb - // .from("customer_products") - // .update(updates) - // // .eq("status", CusProductStatus.Active) - // // .eq("processor->>subscription_id", stripeSubId) - // .or( - // `processor->>'subscription_id'.eq.'${stripeSubId}', subscription_ids.cs.{${stripeSubId}}`, - // ); - - // const { data: updated, error } = await query.select( - // `*, - // product:products!inner(*), - // customer:customers!inner(*), - // customer_entitlements:customer_entitlements!inner( - // *, entitlement:entitlements!inner( - // *, feature:features!inner(*) - // ) - // ), - // customer_prices:customer_prices( - // *, price:prices(*) - // ) - // `, - // ); - - // if (error) { - // throw error; - // } - - // return updated; } static async delete({ @@ -526,53 +471,58 @@ export class CusProductService { .where(eq(customerProducts.id, cusProductId)) .returning(); } + + static async getByFingerprint({ + db, + freeTrialId, + fingerprint, + }: { + db: DrizzleCli; + freeTrialId: string; + fingerprint: string; + }) { + let data = await db + .select() + .from(customerProducts) + .innerJoin( + customers, + eq(customerProducts.internal_customer_id, customers.internal_id), + ) + .where( + and( + eq(customers.fingerprint, fingerprint), + eq(customerProducts.free_trial_id, freeTrialId), + ), + ); + + return data; + + // const { data, error } = await sb + // .from("customer_products") + // .select("*, customer:customers!inner(*)") + // .eq("free_trial_id", freeTrialId) + // .eq("customer.fingerprint", fingerprint); + } + + static async getByTrialAndCustomer({ + db, + freeTrialId, + internalCustomerId, + }: { + db: DrizzleCli; + freeTrialId: string; + internalCustomerId: string; + }) { + let data = await db.query.customerProducts.findMany({ + where: and( + eq(customerProducts.free_trial_id, freeTrialId), + eq(customerProducts.internal_customer_id, internalCustomerId), + ), + with: { + customer: true, + }, + }); + + return data; + } } - -// static async getByStripeSubId({ -// sb, -// stripeSubId, -// orgId, -// env, -// inStatuses, -// withCusEnts = false, -// withCusPrices = false, -// }: { -// sb: SupabaseClient; -// stripeSubId: string; -// orgId: string; -// env: AppEnv; -// inStatuses?: string[]; -// withCusEnts?: boolean; -// withCusPrices?: boolean; -// }) { -// const query = sb -// .from("customer_products") -// .select( -// `*, product:products(*), customer:customers!inner(*)${ -// withCusEnts -// ? ", customer_entitlements:customer_entitlements(*, entitlement:entitlements!inner(*, feature:features!inner(*)))" -// : "" -// }${ -// withCusPrices -// ? ", customer_prices:customer_prices(*, price:prices!inner(*))" -// : "" -// }` as "*", -// ) -// .or( -// `processor->>'subscription_id'.eq.'${stripeSubId}', subscription_ids.cs.{${stripeSubId}}`, -// ) -// .eq("customer.org_id", orgId) -// .eq("customer.env", env); - -// if (inStatuses) { -// query.in("status", inStatuses); -// } - -// const { data, error } = await query; - -// if (error) { -// throw error; -// } - -// return data; -// } diff --git a/server/src/internal/customers/products/attachUtils.ts b/server/src/internal/customers/products/attachUtils.ts index dcbeab99d..373f32d35 100644 --- a/server/src/internal/customers/products/attachUtils.ts +++ b/server/src/internal/customers/products/attachUtils.ts @@ -153,7 +153,6 @@ const getProducts = async ({ const getCustomerAndProducts = async ({ db, - sb, org, features, customerId, @@ -168,7 +167,6 @@ const getCustomerAndProducts = async ({ entityData, }: { db: DrizzleCli; - sb: SupabaseClient; org: Organization; features: Feature[]; customerData?: CustomerData; @@ -184,7 +182,6 @@ const getCustomerAndProducts = async ({ const [customer, products] = await Promise.all([ getOrCreateCustomer({ db, - sb, org, features, env, @@ -288,7 +285,6 @@ export const getFullCusProductData = async ({ db, org, features, - sb, customerId, customerData, productId, @@ -306,7 +302,6 @@ export const getFullCusProductData = async ({ db: DrizzleCli; org: Organization; features: Feature[]; - sb: SupabaseClient; customerId: string; customerData?: Customer; productId?: string; @@ -326,7 +321,6 @@ export const getFullCusProductData = async ({ db, org, features, - sb, customerId, customerData, productId, @@ -344,7 +338,7 @@ export const getFullCusProductData = async ({ let freeTrialProduct = products.find((p) => notNullish(p.free_trial)); if (freeTrialProduct) { freeTrial = await getFreeTrialAfterFingerprint({ - sb, + db, freeTrial: freeTrialProduct.free_trial, fingerprint: customer.fingerprint, internalCustomerId: customer.internal_id, @@ -412,7 +406,6 @@ export const getFullCusProductData = async ({ let { prices, entitlements } = await handleNewProductItems({ db, - sb, curPrices, curEnts, newItems: itemsInput, @@ -431,7 +424,7 @@ export const getFullCusProductData = async ({ }); const uniqueFreeTrial = await getFreeTrialAfterFingerprint({ - sb, + db, freeTrial: freeTrial, fingerprint: customer.fingerprint, internalCustomerId: customer.internal_id, diff --git a/server/src/internal/customers/products/cusProductUtils.ts b/server/src/internal/customers/products/cusProductUtils.ts index d93b046c0..54a251e9b 100644 --- a/server/src/internal/customers/products/cusProductUtils.ts +++ b/server/src/internal/customers/products/cusProductUtils.ts @@ -54,14 +54,12 @@ export const isActiveStatus = (status: CusProductStatus) => { // 1. Cancel cusProductSubscriptions export const cancelCusProductSubscriptions = async ({ - sb, cusProduct, org, env, excludeIds, expireImmediately = true, }: { - sb: SupabaseClient; cusProduct: FullCusProduct; org: Organization; env: AppEnv; @@ -128,7 +126,6 @@ export const activateDefaultProduct = async ({ productGroup, customer, org, - sb, env, curCusProduct, }: { @@ -136,7 +133,6 @@ export const activateDefaultProduct = async ({ productGroup: string; customer: Customer; org: Organization; - sb: SupabaseClient; env: AppEnv; curCusProduct?: FullCusProduct; }) => { @@ -163,7 +159,6 @@ export const activateDefaultProduct = async ({ await createFullCusProduct({ db, - sb, attachParams: { org, customer, @@ -184,13 +179,11 @@ export const activateDefaultProduct = async ({ export const expireAndActivate = async ({ db, - sb, env, cusProduct, org, }: { db: DrizzleCli; - sb: SupabaseClient; env: AppEnv; cusProduct: FullCusProduct; org: Organization; @@ -207,14 +200,12 @@ export const expireAndActivate = async ({ productGroup: cusProduct.product.group, customer: cusProduct.customer, org, - sb, env, }); }; export const activateFutureProduct = async ({ db, - sb, cusProduct, subscription, org, @@ -222,7 +213,6 @@ export const activateFutureProduct = async ({ logger = console, }: { db: DrizzleCli; - sb: SupabaseClient; cusProduct: FullCusProduct; subscription: Stripe.Subscription; org: Organization; diff --git a/server/src/internal/dev/ApiKeyService.ts b/server/src/internal/dev/ApiKeyService.ts index e10837e73..437fd4e9d 100644 --- a/server/src/internal/dev/ApiKeyService.ts +++ b/server/src/internal/dev/ApiKeyService.ts @@ -62,7 +62,7 @@ export class ApiKeyService { env, }; - console.log("result", result); + // console.log("result", result); return result; } @@ -80,6 +80,7 @@ export class ApiKeyService { orderBy: [desc(apiKeys.id)], }); } + static async insert({ db, apiKey }: { db: DrizzleCli; apiKey: ApiKey }) { await db.insert(apiKeys).values(apiKey); } diff --git a/server/src/internal/dev/api-keys/publicKeyUtils.ts b/server/src/internal/dev/api-keys/publicKeyUtils.ts index b6b6fde95..4ee1f4be7 100644 --- a/server/src/internal/dev/api-keys/publicKeyUtils.ts +++ b/server/src/internal/dev/api-keys/publicKeyUtils.ts @@ -1,3 +1,4 @@ +import { DrizzleCli } from "@/db/initDrizzle.js"; import { CacheType } from "@/external/caching/cacheActions.js"; import { getAPIKeyCache } from "@/external/caching/cacheUtils.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; @@ -6,37 +7,30 @@ import { AppEnv } from "@autumn/shared"; import { SupabaseClient } from "@supabase/supabase-js"; export const verifyPublicKey = async ({ - sb, + db, pkey, env, }: { - sb: SupabaseClient; + db: DrizzleCli; pkey: string; env: AppEnv; }) => { - const start = performance.now(); let data = await getAPIKeyCache({ action: CacheType.PublicKey, key: pkey, fn: async () => await OrgService.getFromPkeyWithFeatures({ - sb, + db, pkey, env, }), }); - const end = performance.now(); if (!data) { return null; } let org = structuredClone(data); - try { - console.log( - `verify pkey took ${(end - start).toFixed(2)}ms, org: ${org.slug}` - ); - } catch (error) {} delete org.features; return { diff --git a/server/src/internal/features/FeatureService.ts b/server/src/internal/features/FeatureService.ts index 2e667f1a4..44d508e72 100644 --- a/server/src/internal/features/FeatureService.ts +++ b/server/src/internal/features/FeatureService.ts @@ -41,11 +41,7 @@ export class FeatureService { orgId: req.orgId, env: req.env, }); - // const features = await FeatureService.getFeatures({ - // sb: req.sb, - // orgId: req.orgId, - // env: req.env, - // }); + return features as Feature[]; } @@ -179,4 +175,18 @@ export class FeatureService { }); return deletedFeatures[0] as Feature; } + + static async deleteByOrgId({ + db, + orgId, + env, + }: { + db: DrizzleCli; + orgId: string; + env: AppEnv; + }) { + await db + .delete(features) + .where(and(eq(features.org_id, orgId), eq(features.env, env))); + } } diff --git a/server/src/internal/metadata/MetadataService.ts b/server/src/internal/metadata/MetadataService.ts index 4a7c58795..a55f0fb7d 100644 --- a/server/src/internal/metadata/MetadataService.ts +++ b/server/src/internal/metadata/MetadataService.ts @@ -1,48 +1,23 @@ -import { AutumnMetadata } from "@autumn/shared"; -import { SupabaseClient } from "@supabase/supabase-js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnMetadata, metadata } from "@autumn/shared"; +import { eq } from "drizzle-orm"; export class MetadataService { - static async insert(sb: SupabaseClient, metadata: AutumnMetadata) { - const { data, error } = await sb.from("metadata").insert(metadata); - - if (error) { - throw error; - } - - return data; + static async insert({ db, data }: { db: DrizzleCli; data: AutumnMetadata }) { + await db.insert(metadata).values(data); } - static async getMetadata(sb: SupabaseClient, id: string) { - const { data, error } = await sb - .from("metadata") - .select("*") - .eq("id", id) - .single(); + static async get({ db, id }: { db: DrizzleCli; id: string }) { + const data = await db + .select() + .from(metadata) + .where(eq(metadata.id, id)) + .limit(1); - if (error) { - if (error.code === "PGRST116") { - return null; - } - throw error; + if (data.length === 0) { + return null; } - return data; - } - - static async getById(sb: SupabaseClient, id: string) { - const { data, error } = await sb - .from("metadata") - .select("*") - .eq("id", id) - .single(); - - if (error) { - if (error.code === "PGRST116") { - return null; - } - throw error; - } - - return data; + return data[0] as AutumnMetadata; } } diff --git a/server/src/internal/metadata/metadataUtils.ts b/server/src/internal/metadata/metadataUtils.ts index 92ed4d4df..dbe730364 100644 --- a/server/src/internal/metadata/metadataUtils.ts +++ b/server/src/internal/metadata/metadataUtils.ts @@ -1,17 +1,16 @@ +import Stripe from "stripe"; import { AutumnMetadata } from "@autumn/shared"; - import { generateId } from "@/utils/genUtils.js"; import { addDays } from "date-fns"; import { MetadataService } from "./MetadataService.js"; -import { SupabaseClient } from "@supabase/supabase-js"; -import Stripe from "stripe"; import { AttachParams } from "../customers/products/AttachParams.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; export const createCheckoutMetadata = async ({ - sb, + db, attachParams, }: { - sb: SupabaseClient; + db: DrizzleCli; attachParams: AttachParams; }) => { const metaId = generateId("meta"); @@ -30,14 +29,14 @@ export const createCheckoutMetadata = async ({ }, }; - await MetadataService.insert(sb, metadata); + await MetadataService.insert({ db, data: metadata }); return metaId; }; export const getMetadataFromCheckoutSession = async ( checkoutSession: Stripe.Checkout.Session, - sb: SupabaseClient, + db: DrizzleCli, ) => { const metadataId = checkoutSession.metadata?.autumn_metadata_id; @@ -45,7 +44,10 @@ export const getMetadataFromCheckoutSession = async ( return null; } - const metadata = await MetadataService.getById(sb, metadataId); + const metadata = await MetadataService.get({ + db, + id: metadataId, + }); if (!metadata) { return null; diff --git a/server/src/internal/migrations/MigrationService.ts b/server/src/internal/migrations/MigrationService.ts index 8d1c9e03f..ebf7b212b 100644 --- a/server/src/internal/migrations/MigrationService.ts +++ b/server/src/internal/migrations/MigrationService.ts @@ -1,153 +1,117 @@ import { AppEnv, - MigrationCustomerStatus, + migrationErrors, + MigrationJob, MigrationJobStep, } from "@autumn/shared"; -import { SupabaseClient } from "@supabase/supabase-js"; + +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { migrationJobs } from "@autumn/shared"; +import RecaseError from "@/utils/errorUtils.js"; +import { ErrCode } from "@autumn/shared"; +import { and, eq, ne } from "drizzle-orm"; export class MigrationService { - static async createJob({ sb, data }: { sb: SupabaseClient; data: any }) { - let { data: insertedData, error } = await sb - .from("migration_jobs") - .insert(data) - .select() - .single(); + static async createJob({ db, data }: { db: DrizzleCli; data: MigrationJob }) { + let result = await db.insert(migrationJobs).values(data).returning(); - if (error) { - throw error; + if (result.length === 0) { + throw new RecaseError({ + message: "Failed to create migration job", + code: ErrCode.InsertMigrationJobFailed, + }); } - return insertedData; + return result[0]; } static async updateJob({ - sb, + db, migrationJobId, updates, }: { - sb: SupabaseClient; + db: DrizzleCli; migrationJobId: string; updates: any; }) { - let { data: updatedData, error } = await sb - .from("migration_jobs") - .update({ + let results = await db + .update(migrationJobs) + .set({ ...updates, updated_at: Date.now(), }) - .eq("id", migrationJobId) - .select() - .single(); + .where(eq(migrationJobs.id, migrationJobId)) + .returning(); - if (error) { - throw error; + if (results.length === 0) { + return null; } - return updatedData; + return results[0]; } - static async getJob({ sb, id }: { sb: SupabaseClient; id: string }) { - let { data: job, error } = await sb - .from("migration_jobs") - .select("*") - .eq("id", id) - .single(); + static async getJob({ db, id }: { db: DrizzleCli; id: string }) { + let job = await db.query.migrationJobs.findFirst({ + where: eq(migrationJobs.id, id), + }); - if (error) { - throw error; + if (!job) { + throw new RecaseError({ + message: `Migration job ${id} not found`, + code: ErrCode.MigrationJobNotFound, + }); } - return job; + return job as MigrationJob; } static async getExistingJobs({ - sb, + db, orgId, env, }: { - sb: SupabaseClient; + db: DrizzleCli; orgId: string; env: AppEnv; }) { - let { data: jobs, error } = await sb - .from("migration_jobs") - .select("*") - .eq("org_id", orgId) - .eq("env", env) - .neq("current_step", MigrationJobStep.Failed) - .neq("current_step", MigrationJobStep.Finished); + let jobs = await db.query.migrationJobs.findMany({ + where: and( + eq(migrationJobs.org_id, orgId), + eq(migrationJobs.env, env), + ne(migrationJobs.current_step, MigrationJobStep.Failed), + ne(migrationJobs.current_step, MigrationJobStep.Finished), + ), + }); - if (error) { - throw error; - } - - return jobs; - } - static async insertCustomers({ - sb, - data, - }: { - sb: SupabaseClient; - data: any; - }) { - let { error } = await sb.from("migration_customers").insert(data); - - if (error) { - throw error; - } - - return; + return jobs as MigrationJob[]; } - static getBatch = async ({ - sb, - migrationJobId, - batchSize = 10, - }: { - sb: SupabaseClient; - migrationJobId: string; - batchSize?: number; - }) => { - let { data: migrationCustomers, error } = await sb - .from("migration_customers") - .select("*") - .eq("migration_job_id", migrationJobId) - .eq("status", MigrationCustomerStatus.Pending) - .order("internal_customer_id") - .limit(batchSize); + static async insertError({ db, data }: { db: DrizzleCli; data: any }) { + let result = await db.insert(migrationErrors).values(data).returning(); - if (error) { - throw error; + if (result.length === 0) { + throw new RecaseError({ + message: "Failed to insert migration error", + code: ErrCode.InsertMigrationErrorFailed, + }); } - return migrationCustomers; - }; - - static async insertError({ sb, data }: { sb: SupabaseClient; data: any }) { - let { error } = await sb.from("migration_errors").insert(data); - - if (error) { - throw error; - } - - return; + return result[0]; } static async getErrors({ - sb, + db, migrationJobId, }: { - sb: SupabaseClient; + db: DrizzleCli; migrationJobId: string; }) { - let { data: errors, error } = await sb - .from("migration_errors") - .select("*, customer:customers(*)") - .eq("migration_job_id", migrationJobId); - - if (error) { - throw error; - } + let errors = await db.query.migrationErrors.findMany({ + where: eq(migrationErrors.migration_job_id, migrationJobId), + with: { + customer: true, + }, + }); return errors; } diff --git a/server/src/internal/migrations/migrationSteps/getMigrationCustomers.ts b/server/src/internal/migrations/migrationSteps/getMigrationCustomers.ts index 0b1468b0c..adc50cc72 100644 --- a/server/src/internal/migrations/migrationSteps/getMigrationCustomers.ts +++ b/server/src/internal/migrations/migrationSteps/getMigrationCustomers.ts @@ -6,13 +6,15 @@ import { } from "@autumn/shared"; import { MigrationService } from "../MigrationService.js"; import RecaseError from "@/utils/errorUtils.js"; -import { SupabaseClient } from "@supabase/supabase-js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { customerProducts } from "@autumn/shared"; +import { and, asc, eq, gt, inArray } from "drizzle-orm"; const getAllCustomersOnProduct = async ({ - sb, + db, internalProductId, }: { - sb: any; // Replace 'any' with your actual Supabase client type + db: DrizzleCli; internalProductId: string; }) => { let allData: any[] = []; @@ -20,22 +22,24 @@ const getAllCustomersOnProduct = async ({ let lastId: string | null = null; while (true) { - let query = sb - .from("customer_products") - .select("*, customer:customers!inner(*)") - .eq("internal_product_id", internalProductId) - .in("status", [CusProductStatus.Active, CusProductStatus.PastDue]) - .order("id", { ascending: true }) - .limit(PAGE_SIZE); - - if (lastId) { - query = query.gt("id", lastId); - } - - const { data, error } = await query; - - // If query error - if (error) { + let data; + try { + data = await db.query.customerProducts.findMany({ + where: and( + eq(customerProducts.internal_product_id, internalProductId), + inArray(customerProducts.status, [ + CusProductStatus.Active, + CusProductStatus.PastDue, + ]), + lastId ? gt(customerProducts.id, lastId) : undefined, + ), + with: { + customer: true, + }, + orderBy: [asc(customerProducts.id)], + limit: PAGE_SIZE, + }); + } catch (error) { throw new RecaseError({ message: "Error getting customers on product", code: ErrCode.GetCusProductsFailed, @@ -47,7 +51,7 @@ const getAllCustomersOnProduct = async ({ let filtered = data.reduce((acc: any[], curr: any) => { const existingIndex = acc.findIndex( - (item) => item.customer.id === curr.customer.id + (item) => item.customer.id === curr.customer.id, ); if (existingIndex === -1) { acc.push(curr); @@ -69,44 +73,44 @@ const getAllCustomersOnProduct = async ({ }; export const getMigrationCustomers = async ({ - sb, + db, migrationJobId, fromProduct, logger, }: { - sb: SupabaseClient; + db: DrizzleCli; migrationJobId: string; fromProduct: Product; logger: any; }) => { await MigrationService.updateJob({ - sb, + db, migrationJobId, updates: { current_step: MigrationJobStep.GetCustomers, }, }); - let { cusProducts, error } = await getAllCustomersOnProduct({ - sb, + let { cusProducts } = await getAllCustomersOnProduct({ + db, internalProductId: fromProduct.internal_id, }); let totalCount = cusProducts.length; let canceledCount = cusProducts.filter( - (cusProd) => cusProd.canceled_at !== null + (cusProd) => cusProd.canceled_at !== null, ).length; let customCount = cusProducts.filter((cusProd) => cusProd.is_custom).length; let filteredCusProducts = cusProducts.filter( - (cusProd) => cusProd.canceled_at === null && !cusProd.is_custom + (cusProd) => cusProd.canceled_at === null && !cusProd.is_custom, ); let customers = filteredCusProducts.map((cusProd) => cusProd.customer); await MigrationService.updateJob({ - sb, + db, migrationJobId, updates: { step_details: { diff --git a/server/src/internal/migrations/migrationSteps/migrateCustomer.ts b/server/src/internal/migrations/migrationSteps/migrateCustomer.ts index bf78dc60c..d234374d8 100644 --- a/server/src/internal/migrations/migrationSteps/migrateCustomer.ts +++ b/server/src/internal/migrations/migrationSteps/migrateCustomer.ts @@ -18,7 +18,6 @@ import { BillingType, UsagePriceConfig, Feature, - ErrCode, } from "@autumn/shared"; import { SupabaseClient } from "@supabase/supabase-js"; import { MigrationService } from "../MigrationService.js"; @@ -27,12 +26,10 @@ import { getBillingType } from "@/internal/products/prices/priceUtils.js"; import { FeatureOptions } from "@autumn/shared"; import { DrizzleCli } from "@/db/initDrizzle.js"; import { CusProductService } from "@/internal/customers/products/CusProductService.js"; -import { StatusCodes } from "http-status-codes"; export const migrateCustomer = async ({ db, migrationJob, - sb, customer, org, logger, @@ -44,7 +41,6 @@ export const migrateCustomer = async ({ }: { db: DrizzleCli; migrationJob: MigrationJob; - sb: SupabaseClient; customer: Customer; org: Organization; env: AppEnv; @@ -88,6 +84,7 @@ export const migrateCustomer = async ({ optionsList: curCusProduct.options, entities, cusProducts, + fromMigration: true, }; // Get prepaid prices @@ -115,7 +112,7 @@ export const migrateCustomer = async ({ await handleUpgrade({ req: { - sb, + db, orgId, env, logtail: logger, @@ -145,7 +142,7 @@ export const migrateCustomer = async ({ } await MigrationService.insertError({ - sb, + db, data: constructMigrationError({ migrationJobId: migrationJob.id, internalCustomerId: customer.internal_id, diff --git a/server/src/internal/migrations/migrationSteps/migrateCustomers.ts b/server/src/internal/migrations/migrationSteps/migrateCustomers.ts index 0028b2977..a83c78418 100644 --- a/server/src/internal/migrations/migrationSteps/migrateCustomers.ts +++ b/server/src/internal/migrations/migrationSteps/migrateCustomers.ts @@ -22,7 +22,6 @@ import { DrizzleCli } from "@/db/initDrizzle.js"; export const migrateCustomers = async ({ db, - sb, migrationJob, fromProduct, toProduct, @@ -31,7 +30,6 @@ export const migrateCustomers = async ({ features, }: { db: DrizzleCli; - sb: SupabaseClient; migrationJob: MigrationJob; fromProduct: FullProduct; toProduct: FullProduct; @@ -39,10 +37,8 @@ export const migrateCustomers = async ({ customers: Customer[]; features: Feature[]; }) => { - // console.log(`Migrating ${customers.length} customers`); - // return; await MigrationService.updateJob({ - sb, + db, migrationJobId: migrationJob.id, updates: { current_step: MigrationJobStep.MigrateCustomers, @@ -52,8 +48,8 @@ export const migrateCustomers = async ({ let batchCount = 0; let { org_id: orgId, env } = migrationJob; - let org = await OrgService.getFullOrg({ - sb, + let org = await OrgService.get({ + db, orgId, }); @@ -88,7 +84,6 @@ export const migrateCustomers = async ({ migrateCustomer({ db, migrationJob, - sb, customer, org, logger, @@ -112,7 +107,7 @@ export const migrateCustomers = async ({ // Get current number of customers migrated let curMigrationJob = await MigrationService.getJob({ - sb, + db, id: migrationJob.id, }); let curSucceeded = @@ -123,7 +118,7 @@ export const migrateCustomers = async ({ 0; await MigrationService.updateJob({ - sb, + db, migrationJobId: migrationJob.id, updates: { step_details: { @@ -148,7 +143,7 @@ export const migrateCustomers = async ({ let migrationDetails: any = {}; try { let errors = await MigrationService.getErrors({ - sb, + db, migrationJobId: migrationJob.id, }); @@ -164,12 +159,12 @@ export const migrateCustomers = async ({ } let curMigrationJob = await MigrationService.getJob({ - sb, + db, id: migrationJob.id, }); await MigrationService.updateJob({ - sb, + db, migrationJobId: migrationJob.id, updates: { current_step: MigrationJobStep.Finished, @@ -181,7 +176,7 @@ export const migrateCustomers = async ({ }); await sendMigrationEmail({ - sb, + db, migrationJobId: migrationJob.id, org, }); diff --git a/server/src/internal/migrations/migrationSteps/sendMigrationEmail.ts b/server/src/internal/migrations/migrationSteps/sendMigrationEmail.ts index de519ee35..9495c9c59 100644 --- a/server/src/internal/migrations/migrationSteps/sendMigrationEmail.ts +++ b/server/src/internal/migrations/migrationSteps/sendMigrationEmail.ts @@ -1,20 +1,19 @@ -import { SupabaseClient } from "@supabase/supabase-js"; import { MigrationService } from "../MigrationService.js"; import { sendTextEmail } from "@/external/resend/resendUtils.js"; import { MigrationJobStep, Organization } from "@autumn/shared"; -import { createClerkCli } from "@/external/clerkUtils.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; export const sendMigrationEmail = async ({ - sb, + db, migrationJobId, org, }: { - sb: SupabaseClient; + db: DrizzleCli; migrationJobId: string; org: Organization; }) => { let migrationJob = await MigrationService.getJob({ - sb, + db, id: migrationJobId, }); diff --git a/server/src/internal/migrations/runMigrationTask.ts b/server/src/internal/migrations/runMigrationTask.ts index a35a58bc0..a76bcfd75 100644 --- a/server/src/internal/migrations/runMigrationTask.ts +++ b/server/src/internal/migrations/runMigrationTask.ts @@ -11,12 +11,10 @@ export const runMigrationTask = async ({ db, payload, logger, - sb, }: { db: DrizzleCli; payload: any; logger: any; - sb: any; }) => { const { migrationJobId } = payload; @@ -24,7 +22,7 @@ export const runMigrationTask = async ({ logger.info(`Running migration task, ID: ${migrationJobId}`); const migrationJob = await MigrationService.getJob({ - sb, + db, id: migrationJobId, }); @@ -48,7 +46,7 @@ export const runMigrationTask = async ({ // STEP 1: GET ALL CUSTOMERS AND INSERT INTO MIGRATIONS... let customers = await getMigrationCustomers({ - sb, + db, migrationJobId, fromProduct, logger, @@ -65,7 +63,6 @@ export const runMigrationTask = async ({ // STEP 2: MIGRATE CUSTOMERS.. await migrateCustomers({ db, - sb, migrationJob, fromProduct, toProduct, @@ -79,7 +76,7 @@ export const runMigrationTask = async ({ logger.error(`Migration failed: ${migrationJobId}`); logger.error(error); await MigrationService.updateJob({ - sb, + db, migrationJobId, updates: { current_step: MigrationJobStep.Failed, diff --git a/server/src/internal/orgs/OrgService.ts b/server/src/internal/orgs/OrgService.ts index 4b17f7979..707c97f59 100644 --- a/server/src/internal/orgs/OrgService.ts +++ b/server/src/internal/orgs/OrgService.ts @@ -1,5 +1,12 @@ import RecaseError from "@/utils/errorUtils.js"; -import { AppEnv, ErrCode, Organization, OrgConfigSchema } from "@autumn/shared"; +import { + AppEnv, + ErrCode, + Feature, + features, + Organization, + OrgConfigSchema, +} from "@autumn/shared"; import { SupabaseClient } from "@supabase/supabase-js"; import { getApiVersion } from "@/utils/versionUtils.js"; import { clearOrgCache } from "./orgUtils/clearOrgCache.js"; @@ -23,11 +30,6 @@ export class OrgService { } return await this.get({ db: req.db, orgId: req.orgId }); - - // return await this.getFullOrg({ - // sb: req.sb, - // orgId: req.orgId, - // }); } // Drizzle get @@ -77,132 +79,76 @@ export class OrgService { } static async getWithFeatures({ - sb, + db, orgId, env, }: { - sb: SupabaseClient; + db: DrizzleCli; orgId: string; env: AppEnv; }) { - const { data, error } = await sb - .from("organizations") - .select("*, features(*)") - .eq("id", orgId) - .eq("features.env", env) - .single(); + const result = (await db.query.organizations.findFirst({ + where: eq(organizations.id, orgId), + with: { + features: { + where: eq(features.env, env), + }, + }, + })) as Organization & { + features: Feature[]; + }; - if (error) { - throw new Error("Error getting orgs from supabase"); + if (!result) { + throw new RecaseError({ + message: `Organization ${orgId} not found`, + code: ErrCode.OrgNotFound, + statusCode: 404, + }); } - let org = structuredClone(data); - delete org.features; - return { org, features: data.features || [] }; - } - - static async getOrgs({ sb }: { sb: SupabaseClient }) { - const { data, error } = await sb.from("organizations").select("*"); - if (error) { - throw new Error("Error getting orgs from supabase"); - } - return data; + let org = structuredClone(result); + delete (org as any).features; + return { org, features: result.features || [] }; } static async getFromPkeyWithFeatures({ - sb, + db, pkey, env, }: { - sb: SupabaseClient; + db: DrizzleCli; pkey: string; env: AppEnv; }) { - let fieldName = env === AppEnv.Sandbox ? "test_pkey" : "live_pkey"; - const { data, error } = await sb - .from("organizations") - .select("*, features(*)") - .eq(fieldName, pkey) - .eq("features.env", env) - .single(); - - if (error) { - if (error.code === "PGRST116") { - return null; - } - - throw new RecaseError({ - message: "Error getting org from supabase", - code: ErrCode.OrgNotFound, - statusCode: 404, - data: error, - }); - } - - return data; - } - - static async getFullOrg({ - sb, - orgId, - }: { - sb: SupabaseClient; - orgId: string; - }) { - const { data, error } = await sb - .from("organizations") - .select("*") - .eq("id", orgId) - .select() - .single(); - - if (error) { - if (error.code === "PGRST116") { - throw new RecaseError({ - message: "Failed to get org from supabase", - code: ErrCode.OrgNotFound, - statusCode: 404, - data: error, - }); - } else { - throw error; - } - } - - let config = data.config || {}; - let apiVersion = getApiVersion({ - createdAt: data.created_at, + let org = await db.query.organizations.findFirst({ + where: + env === AppEnv.Sandbox + ? eq(organizations.test_pkey, pkey) + : eq(organizations.live_pkey, pkey), + with: { + features: { + where: eq(features.env, env), + }, + }, }); - return { - ...data, - config: OrgConfigSchema.parse(config), - api_version: apiVersion, + return org as Organization & { + features: Feature[]; }; } - static async getBySlug({ sb, slug }: { sb: SupabaseClient; slug: string }) { - const { data, error } = await sb - .from("organizations") - .select("*") - .eq("slug", slug) - .select() - .single(); + static async getBySlug({ db, slug }: { db: DrizzleCli; slug: string }) { + const result = await db.query.organizations.findFirst({ + where: eq(organizations.slug, slug), + }); - if (error) { - if (error.code === "PGRST116") { - return null; - } - - throw new RecaseError({ - message: "Failed to get org from supabase", - code: ErrCode.OrgNotFound, - statusCode: 404, - }); + if (!result) { + return null; } - return data; + return result as Organization; } + static async insert({ db, org }: { db: DrizzleCli; org: any }) { await db.insert(organizations).values(org); } diff --git a/server/src/internal/orgs/onboarding/onboardingRouter.ts b/server/src/internal/orgs/onboarding/onboardingRouter.ts index b1515c3e7..56602fcdc 100644 --- a/server/src/internal/orgs/onboarding/onboardingRouter.ts +++ b/server/src/internal/orgs/onboarding/onboardingRouter.ts @@ -22,7 +22,7 @@ onboardingRouter.post("", async (req: Request, res: any) => res, action: "onboarding", handler: async (req: ExtendedRequest, res: ExtendedResponse) => { - const { db, sb, logtail: logger, org } = req; + const { db, logtail: logger, org } = req; const { token } = req.body; if (!token) { @@ -73,7 +73,6 @@ onboardingRouter.post("", async (req: Request, res: any) => let { products, prices, ents } = await parseChatProducts({ db, - sb, logger, orgId: org.id, features: [...curFeatures, ...backendFeatures], diff --git a/server/src/internal/orgs/onboarding/parseChatProducts.ts b/server/src/internal/orgs/onboarding/parseChatProducts.ts index b01c0a5ab..0d4813315 100644 --- a/server/src/internal/orgs/onboarding/parseChatProducts.ts +++ b/server/src/internal/orgs/onboarding/parseChatProducts.ts @@ -1,7 +1,6 @@ import { DrizzleCli } from "@/db/initDrizzle.js"; import { handleNewProductItems } from "@/internal/products/product-items/productItemInitUtils.js"; import { constructProduct } from "@/internal/products/productUtils.js"; -import { generateId } from "@/utils/genUtils.js"; import { AppEnv, CreateProductSchema, @@ -12,19 +11,15 @@ import { Product, ProductV2, } from "@autumn/shared"; -import { SupabaseClient } from "@supabase/supabase-js"; -import { z } from "zod"; export const parseChatProducts = async ({ db, - sb, logger, features, orgId, chatProducts, }: { db: DrizzleCli; - sb: SupabaseClient; logger: any; features: Feature[]; orgId: string; @@ -46,7 +41,6 @@ export const parseChatProducts = async ({ let { prices, entitlements } = await handleNewProductItems({ db, - sb, curPrices: [], curEnts: [], newItems: product.items, diff --git a/server/src/internal/orgs/orgUtils/clearOrgCache.ts b/server/src/internal/orgs/orgUtils/clearOrgCache.ts index e96636d96..66bb70c17 100644 --- a/server/src/internal/orgs/orgUtils/clearOrgCache.ts +++ b/server/src/internal/orgs/orgUtils/clearOrgCache.ts @@ -5,13 +5,11 @@ import { CacheType } from "@/external/caching/cacheActions.js"; import { DrizzleCli } from "@/db/initDrizzle.js"; export const clearOrgCache = async ({ - // sb, db, orgId, env, logger = console, }: { - // sb: SupabaseClient; db: DrizzleCli; orgId: string; env?: AppEnv; diff --git a/server/src/internal/products/ProductService.ts b/server/src/internal/products/ProductService.ts index e24a37ebc..9c439334a 100644 --- a/server/src/internal/products/ProductService.ts +++ b/server/src/internal/products/ProductService.ts @@ -3,15 +3,12 @@ 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"; @@ -41,7 +38,44 @@ const parseFreeTrials = ({ }; export class ProductService { - // GET + static async getByFeature({ + db, + internalFeatureId, + }: { + db: DrizzleCli; + internalFeatureId: string; + }) { + 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[]; + + parseFreeTrials({ products: fullProducts }); + + let latestProducts = getLatestProducts(fullProducts); + + return latestProducts; + } + static async getByInternalId({ db, internalId, @@ -300,41 +334,17 @@ export class ProductService { ); } - static async getByFeature({ + static async deleteByOrgId({ db, - internalFeatureId, + orgId, + env, }: { db: DrizzleCli; - internalFeatureId: string; + orgId: string; + env: AppEnv; }) { - 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[]; - - parseFreeTrials({ products: fullProducts }); - - let latestProducts = getLatestProducts(fullProducts); - - return latestProducts; + await db + .delete(products) + .where(and(eq(products.org_id, orgId), eq(products.env, env))); } } diff --git a/server/src/internal/products/free-trials/FreeTrialService.ts b/server/src/internal/products/free-trials/FreeTrialService.ts index ea8d95306..4c1c9f575 100644 --- a/server/src/internal/products/free-trials/FreeTrialService.ts +++ b/server/src/internal/products/free-trials/FreeTrialService.ts @@ -25,22 +25,18 @@ export class FreeTrialService { } static async update({ - sb, + db, freeTrialId, update, }: { - sb: SupabaseClient; + db: DrizzleCli; freeTrialId: string; update: Partial; }) { - const { error } = await sb - .from("free_trials") - .update(update) - .eq("id", freeTrialId); - - if (error) { - throw error; - } + await db + .update(freeTrials) + .set(update) + .where(eq(freeTrials.id, freeTrialId)); } static async delete({ db, id }: { db: DrizzleCli; id: string }) { diff --git a/server/src/internal/products/free-trials/freeTrialUtils.ts b/server/src/internal/products/free-trials/freeTrialUtils.ts index b83588d07..b28d71f69 100644 --- a/server/src/internal/products/free-trials/freeTrialUtils.ts +++ b/server/src/internal/products/free-trials/freeTrialUtils.ts @@ -19,6 +19,7 @@ import { } from "date-fns"; import { FreeTrialService } from "./FreeTrialService.js"; import { DrizzleCli } from "@/db/initDrizzle.js"; +import { CusProductService } from "@/internal/customers/products/CusProductService.js"; export const validateAndInitFreeTrial = ({ freeTrial, @@ -86,23 +87,19 @@ export const freeTrialToNumDays = (freeTrial: FreeTrial | null) => { }; export const trialFingerprintExists = async ({ - sb, + db, freeTrialId, fingerprint, }: { - sb: SupabaseClient; + db: DrizzleCli; freeTrialId: string; - fingerprint: string | null; + fingerprint: string; }) => { - const { data, error } = await sb - .from("customer_products") - .select("*, customer:customers!inner(*)") - .eq("free_trial_id", freeTrialId) - .eq("customer.fingerprint", fingerprint); - - if (error) { - throw error; - } + const data = await CusProductService.getByFingerprint({ + db, + freeTrialId, + fingerprint, + }); if (data && data.length > 0) { return true; @@ -112,23 +109,19 @@ export const trialFingerprintExists = async ({ }; export const trialWithCustomerExists = async ({ - sb, + db, internalCustomerId, freeTrialId, }: { - sb: SupabaseClient; + db: DrizzleCli; internalCustomerId: string; freeTrialId: string; }) => { - const { data, error } = await sb - .from("customer_products") - .select("*, customer:customers!inner(*)") - .eq("internal_customer_id", internalCustomerId) - .eq("free_trial_id", freeTrialId); - - if (error) { - throw error; - } + const data = await CusProductService.getByFingerprint({ + db, + freeTrialId, + fingerprint: internalCustomerId, + }); if (data && data.length > 0) { return true; @@ -138,13 +131,13 @@ export const trialWithCustomerExists = async ({ }; export const getFreeTrialAfterFingerprint = async ({ - sb, + db, freeTrial, fingerprint, internalCustomerId, multipleAllowed, }: { - sb: SupabaseClient; + db: DrizzleCli; freeTrial: FreeTrial | null | undefined; fingerprint: string | null | undefined; internalCustomerId: string; @@ -159,7 +152,7 @@ export const getFreeTrialAfterFingerprint = async ({ let uniqueFreeTrial: FreeTrial | null = freeTrial; if (uniqueFreeTrial.unique_fingerprint && fingerprint) { let exists = await trialFingerprintExists({ - sb, + db, fingerprint, freeTrialId: uniqueFreeTrial.id, }); @@ -173,7 +166,7 @@ export const getFreeTrialAfterFingerprint = async ({ if (uniqueFreeTrial) { // Check if same customer exists let exists = await trialWithCustomerExists({ - sb, + db, internalCustomerId, freeTrialId: uniqueFreeTrial.id, }); diff --git a/server/src/internal/products/internalProductRouter.ts b/server/src/internal/products/internalProductRouter.ts index 9333d4db8..8160f2178 100644 --- a/server/src/internal/products/internalProductRouter.ts +++ b/server/src/internal/products/internalProductRouter.ts @@ -20,7 +20,7 @@ export const productRouter = Router({ mergeParams: true }); productRouter.get("/data", async (req: any, res) => { try { - let { db, sb } = req; + let { db } = req; const [products, features, org, coupons, rewardPrograms] = await Promise.all([ @@ -32,8 +32,8 @@ productRouter.get("/data", async (req: any, res) => { }), FeatureService.getFromReq(req), OrgService.getFromReq(req), - RewardService.getAll({ sb, orgId: req.orgId, env: req.env }), - RewardProgramService.getAll({ sb, orgId: req.orgId, env: req.env }), + RewardService.list({ db, orgId: req.orgId, env: req.env }), + RewardProgramService.list({ db, orgId: req.orgId, env: req.env }), ]); res.status(200).json({ @@ -62,7 +62,7 @@ productRouter.get("/data", async (req: any, res) => { productRouter.get("/counts", async (req: any, res) => { try { - let { db, sb } = req; + let { db } = req; let products = await ProductService.listFull({ db, orgId: req.orgId, @@ -74,7 +74,6 @@ productRouter.get("/counts", async (req: any, res) => { products.map(async (product) => { return CusProdReadService.getCounts({ db, - sb, internalProductId: product.internal_id, }); }), @@ -99,13 +98,11 @@ productRouter.get("/counts", async (req: any, res) => { } }); -// Get stripe products - productRouter.get("/:productId/data", async (req: any, res) => { try { const { productId } = req.params; const { version } = req.query; - const { sb, db, orgId, env } = req; + const { db, orgId, env } = req; const [product, features, org, numVersions, existingMigrations] = await Promise.all([ @@ -125,7 +122,7 @@ productRouter.get("/:productId/data", async (req: any, res) => { env, }), MigrationService.getExistingJobs({ - sb, + db, orgId, env, }), @@ -180,7 +177,7 @@ productRouter.get("/:productId/data", async (req: any, res) => { productRouter.get("/:productId/count", async (req: any, res) => { try { - const { db, orgId, env, sb } = req; + const { db, orgId, env } = req; const { productId } = req.params; const { version } = req.query; @@ -205,7 +202,6 @@ productRouter.get("/:productId/count", async (req: any, res) => { // Get counts from postgres const counts = await CusProdReadService.getCounts({ db, - sb, internalProductId: product.internal_id, }); diff --git a/server/src/internal/products/prices/PriceService.ts b/server/src/internal/products/prices/PriceService.ts index bdd9a831d..6e658238e 100644 --- a/server/src/internal/products/prices/PriceService.ts +++ b/server/src/internal/products/prices/PriceService.ts @@ -20,6 +20,10 @@ export class PriceService { } static async getInIds({ db, ids }: { db: DrizzleCli; ids: string[] }) { + if (!ids || ids.length === 0) { + return []; + } + return (await db.query.prices.findMany({ where: inArray(prices.id, ids), with: { diff --git a/server/src/internal/products/product-items/productItemInitUtils.ts b/server/src/internal/products/product-items/productItemInitUtils.ts index 51f9cf9b6..6a9f92dc8 100644 --- a/server/src/internal/products/product-items/productItemInitUtils.ts +++ b/server/src/internal/products/product-items/productItemInitUtils.ts @@ -17,7 +17,6 @@ import { DrizzleCli } from "@/db/initDrizzle.js"; const updateDbPricesAndEnts = async ({ db, - sb, newPrices, newEnts, updatedPrices, @@ -26,7 +25,6 @@ const updateDbPricesAndEnts = async ({ deletedEnts, }: { db: DrizzleCli; - sb: SupabaseClient; newPrices: Price[]; newEnts: Entitlement[]; updatedPrices: Price[]; @@ -108,7 +106,6 @@ const updateDbPricesAndEnts = async ({ const handleCustomProductItems = async ({ db, - sb, newPrices, newEnts, updatedPrices, @@ -118,7 +115,6 @@ const handleCustomProductItems = async ({ features, }: { db: DrizzleCli; - sb: SupabaseClient; newPrices: Price[]; newEnts: Entitlement[]; updatedPrices: Price[]; @@ -148,7 +144,6 @@ const handleCustomProductItems = async ({ export const handleNewProductItems = async ({ db, - sb, curPrices, curEnts, newItems, @@ -160,7 +155,6 @@ export const handleNewProductItems = async ({ saveToDb = true, }: { db: DrizzleCli; - sb: SupabaseClient; curPrices: Price[]; curEnts: Entitlement[]; newItems: ProductItem[]; @@ -273,7 +267,6 @@ export const handleNewProductItems = async ({ if ((isCustom || newVersion) && saveToDb) { return handleCustomProductItems({ db, - sb, newPrices, newEnts, updatedPrices, @@ -287,7 +280,6 @@ export const handleNewProductItems = async ({ if (saveToDb) { await updateDbPricesAndEnts({ db, - sb, newPrices, newEnts, updatedPrices, diff --git a/server/src/internal/products/productUtils.ts b/server/src/internal/products/productUtils.ts index 8ef160ab3..80b0ee8d3 100644 --- a/server/src/internal/products/productUtils.ts +++ b/server/src/internal/products/productUtils.ts @@ -303,7 +303,6 @@ export const attachToInsertParams = ( // COPY PRODUCT export const copyProduct = async ({ db, - sb, product, toOrgId, toId, @@ -313,7 +312,6 @@ export const copyProduct = async ({ fromFeatures, }: { db: DrizzleCli; - sb: SupabaseClient; product: FullProduct; toOrgId: string; toEnv: AppEnv; diff --git a/server/src/internal/rewards/RewardProgramService.ts b/server/src/internal/rewards/RewardProgramService.ts index 5ca4c2cba..6d80efdae 100644 --- a/server/src/internal/rewards/RewardProgramService.ts +++ b/server/src/internal/rewards/RewardProgramService.ts @@ -1,257 +1,268 @@ +import { and, arrayContains, count, eq, inArray } from "drizzle-orm"; import RecaseError from "@/utils/errorUtils.js"; -import { ErrCode, RewardProgram, RewardTriggerEvent } from "@autumn/shared"; +import { + ErrCode, + Reward, + RewardProgram, + rewardPrograms, + RewardTriggerEvent, +} from "@autumn/shared"; import { ReferralCode } from "@autumn/shared"; +import { DrizzleCli } from "@/db/initDrizzle.js"; +import { referralCodes, rewardRedemptions } from "@autumn/shared"; export class RewardProgramService { - static async get({ sb, internalId }: { sb: any; internalId: string }) { - const { data, error } = await sb - .from("reward_programs") - .select("*, reward:rewards!inner(*)") - .eq("internal_id", internalId); - - if (error) { - throw error; - } - - return data[0]; - } - - static async getById({ - sb, + static async get({ + db, id, orgId, env, errorIfNotFound = false, }: { - sb: any; + db: DrizzleCli; id: string; orgId: string; env: string; errorIfNotFound?: boolean; }) { - const { data, error } = await sb - .from("reward_programs") - .select() - .eq("id", id) - .eq("org_id", orgId) - .eq("env", env); + let result = await db.query.rewardPrograms.findFirst({ + where: and( + eq(rewardPrograms.id, id), + eq(rewardPrograms.org_id, orgId), + eq(rewardPrograms.env, env), + ), + }); - if (error) { - throw error; - } - - if (data.length === 0) { + if (!result) { if (errorIfNotFound) { throw new RecaseError({ - message: "Referral not found", - code: ErrCode.ReferralNotFound, + message: "Reward program not found", + code: ErrCode.RewardNotFound, }); } return null; } - return data[0]; + return result as RewardProgram; } - static async getAll({ - sb, + static async list({ + db, orgId, env, }: { - sb: any; + db: DrizzleCli; orgId: string; env: string; }) { - const { data, error } = await sb - .from("reward_programs") - .select() - .eq("org_id", orgId) - .eq("env", env); + let result = await db.query.rewardPrograms.findMany({ + where: and(eq(rewardPrograms.org_id, orgId), eq(rewardPrograms.env, env)), + }); - if (error) { - throw error; - } - - return data; + return result as RewardProgram[]; } static async getByProductId({ - sb, + db, productIds, orgId, env, }: { - sb: any; + db: DrizzleCli; productIds: string[]; orgId: string; env: string; }) { - const { data, error } = await sb - .from("reward_programs") - .select("*") - .eq("org_id", orgId) - .eq("env", env) - .eq("when", RewardTriggerEvent.Checkout) - .contains("product_ids", productIds); + let result = await db.query.rewardPrograms.findMany({ + where: and( + eq(rewardPrograms.org_id, orgId), + eq(rewardPrograms.env, env), + eq(rewardPrograms.when, RewardTriggerEvent.Checkout), + arrayContains(rewardPrograms.product_ids, productIds), + ), + }); - if (error) { - throw error; - } - - return data; - } - - static async create({ - sb, - data, - }: { - sb: any; - - data: RewardProgram | RewardProgram[]; - }) { - const { data: insertedData, error } = await sb - .from("reward_programs") - .insert(data) - .select() - .single(); - - if (error) { - throw error; - } - - return insertedData; - } - - static async deleteById({ - sb, - id, - orgId, - env, - }: { - sb: any; - id: string; - orgId: string; - env: string; - }) { - const { data, error } = await sb - .from("reward_programs") - .delete() - .eq("id", id) - .eq("org_id", orgId) - .eq("env", env) - .select() - .single(); - - if (error) { - throw error; - } - - return data; - } - - // REFERRAL CODE FUNCTIONS - static async getReferralCode({ - sb, - orgId, - env, - code, - withRewardProgram = false, - }: { - sb: any; - orgId: string; - env: string; - code: string; - withRewardProgram?: boolean; - }) { - const { data, error } = await sb - .from("referral_codes") - .select( - withRewardProgram - ? "*, reward_program:reward_programs!inner(*, reward:rewards!inner(*))" - : "*" - ) - .eq("code", code) - .eq("org_id", orgId) - .eq("env", env) - .single(); - - if (error) { - throw error; - } - - return data; + return result as RewardProgram[]; } static async getCodeByCustomerAndRewardProgram({ - sb, + db, orgId, env, internalCustomerId, internalRewardProgramId, }: { - sb: any; + db: DrizzleCli; orgId: string; env: string; internalCustomerId: string; internalRewardProgramId: string; }) { - const { data, error } = await sb - .from("referral_codes") - .select("*") - .eq("internal_customer_id", internalCustomerId) - .eq("internal_reward_program_id", internalRewardProgramId) - .eq("org_id", orgId) - .eq("env", env); + let result = await db.query.referralCodes.findFirst({ + where: and( + eq(referralCodes.internal_customer_id, internalCustomerId), + eq(referralCodes.internal_reward_program_id, internalRewardProgramId), + eq(referralCodes.org_id, orgId), + eq(referralCodes.env, env), + ), + }); - if (error) { - throw error; - } - - if (data.length === 0) { + if (!result) { return null; } - return data[0]; + return result as ReferralCode; + } + + static async create({ + db, + data, + }: { + db: DrizzleCli; + data: RewardProgram | RewardProgram[]; + }) { + let result = await db + .insert(rewardPrograms) + .values(data as any) + .returning(); + + if (result.length === 0) { + throw new RecaseError({ + message: "Failed to create reward program", + code: ErrCode.InsertRewardProgramFailed, + }); + } + + return result[0] as RewardProgram; + } + + static async delete({ + db, + id, + orgId, + env, + }: { + db: DrizzleCli; + id: string; + orgId: string; + env: string; + }) { + let result = await db + .delete(rewardPrograms) + .where( + and( + eq(rewardPrograms.id, id), + eq(rewardPrograms.org_id, orgId), + eq(rewardPrograms.env, env), + ), + ) + .returning(); + + if (result.length === 0) { + throw new RecaseError({ + message: "Reward program not found", + code: ErrCode.RewardNotFound, + }); + } + + return result[0] as RewardProgram; + } + + // REFERRAL CODE FUNCTIONS + static async getReferralCode({ + db, + orgId, + env, + code, + withRewardProgram = false, + }: { + db: DrizzleCli; + orgId: string; + env: string; + code: string; + withRewardProgram?: boolean; + }) { + let result = await db.query.referralCodes.findFirst({ + where: and( + eq(referralCodes.code, code), + eq(referralCodes.org_id, orgId), + eq(referralCodes.env, env), + ), + with: withRewardProgram + ? { + reward_program: { + with: { + reward: true, + }, + }, + } + : undefined, + }); + + if (!result) { + throw new RecaseError({ + message: "Referral code not found", + code: ErrCode.ReferralCodeNotFound, + statusCode: 404, + }); + } + + return result as ReferralCode & { + reward_program: RewardProgram & { + reward: Reward; + }; + }; } static async createReferralCode({ - sb, + db, data, }: { - sb: any; + db: DrizzleCli; data: ReferralCode; }) { - const { data: insertedData, error } = await sb - .from("referral_codes") - .insert(data) - .select() - .single(); + let result = await db.insert(referralCodes).values(data).returning(); - if (error) { - throw error; + if (result.length === 0) { + throw new RecaseError({ + message: "Failed to create referral code", + code: ErrCode.InsertReferralCodeFailed, + }); } - return insertedData; + return result[0] as ReferralCode; } static async getCodeRedemptionCount({ - sb, + db, referralCodeId, }: { - sb: any; + db: DrizzleCli; referralCodeId: string; }) { - const { data, error, count } = await sb - .from("reward_redemptions") - .select("*, reward_program:reward_programs!inner(*)", { count: "exact" }) - .eq("referral_code_id", referralCodeId) - .eq("triggered", true); + let result = await db + .select({ count: count() }) + .from(rewardRedemptions) + .where( + and( + eq(rewardRedemptions.referral_code_id, referralCodeId), + eq(rewardRedemptions.triggered, true), + ), + ); - if (error) { - throw error; - } + return result[0].count; - return count; + // const { data, error, count } = await sb + // .from("reward_redemptions") + // .select("*, reward_program:reward_programs!inner(*)", { count: "exact" }) + // .eq("referral_code_id", referralCodeId) + // .eq("triggered", true); + + // if (error) { + // throw error; + // } + + // return count; } } diff --git a/server/src/internal/rewards/RewardRedemptionService.ts b/server/src/internal/rewards/RewardRedemptionService.ts index df955086e..fcb34aba0 100644 --- a/server/src/internal/rewards/RewardRedemptionService.ts +++ b/server/src/internal/rewards/RewardRedemptionService.ts @@ -1,23 +1,37 @@ +import { DrizzleCli } from "@/db/initDrizzle.js"; +import RecaseError from "@/utils/errorUtils.js"; import { notNullish } from "@/utils/genUtils.js"; -import { RewardRedemption, RewardTriggerEvent } from "@autumn/shared"; +import { + customers, + ErrCode, + referralCodes, + rewardPrograms, + RewardRedemption, + rewardRedemptions, + rewards, + RewardTriggerEvent, +} from "@autumn/shared"; +import { and, eq } from "drizzle-orm"; export class RewardRedemptionService { - static async getById({ sb, id }: { sb: any; id: string }) { - const { data, error } = await sb - .from("reward_redemptions") - .select("*") - .eq("id", id) - .single(); + static async getById({ db, id }: { db: DrizzleCli; id: string }) { + const data = await db.query.rewardRedemptions.findFirst({ + where: eq(rewardRedemptions.id, id), + }); - if (error) { - throw error; + if (!data) { + throw new RecaseError({ + code: ErrCode.RewardRedemptionNotFound, + message: `Reward redemption ${id} not found`, + statusCode: 404, + }); } return data; } static async getByCustomer({ - sb, + db, internalCustomerId, triggered, withReferralCode = false, @@ -26,7 +40,7 @@ export class RewardRedemptionService { triggerWhen, limit, }: { - sb: any; + db: DrizzleCli; internalCustomerId: string; triggered?: boolean; withReferralCode?: boolean; @@ -35,164 +49,210 @@ export class RewardRedemptionService { triggerWhen?: RewardTriggerEvent; limit?: number; }) { - let query = sb - .from("reward_redemptions") - .select( - ` - * - ${ - withRewardProgram - ? ", reward_program:reward_programs!inner(*, reward:rewards!inner(*))" - : "" - } - ${withReferralCode ? ", referral_code:referral_codes!inner(*)" : ""} - ` - ) - .eq("internal_customer_id", internalCustomerId); + const data = await db.query.rewardRedemptions.findMany({ + where: and( + eq(rewardRedemptions.internal_customer_id, internalCustomerId), + internalRewardProgramId + ? eq( + rewardRedemptions.internal_reward_program_id, + internalRewardProgramId, + ) + : undefined, + triggered ? eq(rewardRedemptions.triggered, triggered) : undefined, + ), + with: { + reward_program: { + with: { + reward: true, + }, + }, + referral_code: true, + }, + limit: limit ?? 100, + }); - if (notNullish(internalRewardProgramId)) { - query = query.eq("internal_reward_program_id", internalRewardProgramId); - } + return data as any; - if (notNullish(triggered)) { - query = query.eq("triggered", triggered); - } + // let query = sb + // .from("reward_redemptions") + // .select( + // ` + // * + // ${ + // withRewardProgram + // ? ", reward_program:reward_programs!inner(*, reward:rewards!inner(*))" + // : "" + // } + // ${withReferralCode ? ", referral_code:referral_codes!inner(*)" : ""} + // `, + // ) + // .eq("internal_customer_id", internalCustomerId); - if (notNullish(limit)) { - query = query.limit(limit); - } + // if (notNullish(internalRewardProgramId)) { + // query = query.eq("internal_reward_program_id", internalRewardProgramId); + // } - const { data, error } = await query; + // if (notNullish(triggered)) { + // query = query.eq("triggered", triggered); + // } - if (error) { - throw error; - } + // if (notNullish(limit)) { + // query = query.limit(limit); + // } - return data; + // const { data, error } = await query; + + // if (error) { + // throw error; + // } + + // return data; } static async getByReferrer({ - sb, + db, internalCustomerId, withCustomer = false, limit = 100, }: { - sb: any; + db: DrizzleCli; internalCustomerId: string; withCustomer?: boolean; limit?: number; }) { - const { data, error } = await sb - .from("reward_redemptions") - .select( - ` - *, referral_code:referral_codes!inner(*) - ${withCustomer ? ", customer:customers!inner(*)" : ""} - ` + const data = await db + .select() + .from(rewardRedemptions) + .innerJoin( + referralCodes, + eq(rewardRedemptions.referral_code_id, referralCodes.id), ) - .eq("referral_code.internal_customer_id", internalCustomerId) + .innerJoin( + customers, + eq(rewardRedemptions.internal_customer_id, customers.internal_id), + ) + + .where(eq(referralCodes.internal_customer_id, internalCustomerId)) .limit(limit); - if (error) { - throw error; - } + let processed = data.map((d) => ({ + ...d.reward_redemptions, + referral_code: d.referral_codes, + customer: d.customers, + })); - return data; - } + return processed; - static async getByCodeAndCustomer({ - sb, - orgId, - env, - code, - internalCustomerId, - }: { - sb: any; - orgId: string; - env: string; - code: string; - internalCustomerId: string; - }) { - const { data, error } = await sb - .from("reward_redemptions") - .select("*") - .eq("code", code) - .eq("internal_customer_id", internalCustomerId); + // const { data, error } = await sb + // .from("reward_redemptions") + // .select( + // ` + // *, referral_code:referral_codes!inner(*) + // ${withCustomer ? ", customer:customers!inner(*)" : ""} + // `, + // ) + // .eq("referral_code.internal_customer_id", internalCustomerId) + // .limit(limit); - if (error) { - throw error; - } + // if (error) { + // throw error; + // } - if (data.length === 0) { - return null; - } - - return data[0]; + // return data; } static async insert({ - sb, + db, rewardRedemption, }: { - sb: any; + db: DrizzleCli; rewardRedemption: RewardRedemption; }) { - const { data, error } = await sb - .from("reward_redemptions") - .insert(rewardRedemption) - .select() - .single(); + const data = await db + .insert(rewardRedemptions) + .values(rewardRedemption) + .returning(); - if (error) { - throw error; + if (data.length === 0) { + throw new RecaseError({ + code: ErrCode.InsertRewardRedemptionFailed, + message: `Failed to insert reward redemption`, + statusCode: 500, + }); } - return data; + return data[0] as RewardRedemption; } static async update({ - sb, + db, id, updates, }: { - sb: any; + db: DrizzleCli; id: string; updates: any; }) { - const { data, error } = await sb - .from("reward_redemptions") - .update(updates) - .eq("id", id) - .select() - .single(); + const data = await db + .update(rewardRedemptions) + .set(updates) + .where(eq(rewardRedemptions.id, id)) + .returning(); - if (error) { - throw error; + if (data.length === 0) { + throw new RecaseError({ + code: "REWARD_REDEMPTION_NOT_FOUND", + message: `Reward redemption ${id} not found`, + }); } - return data; + return data[0] as RewardRedemption; } static async getUnappliedRedemptions({ - sb, + db, internalCustomerId, }: { - sb: any; + db: DrizzleCli; internalCustomerId: string; }) { - const { data, error } = await sb - .from("reward_redemptions") - .select( - "*, referral_code:referral_codes!inner(*), reward_program:reward_programs!inner(*, reward:rewards!inner(*))" + const data = await db + .select() + .from(rewardRedemptions) + .innerJoin( + referralCodes, + eq(rewardRedemptions.referral_code_id, referralCodes.id), ) - .eq("referral_code.internal_customer_id", internalCustomerId) - .eq("triggered", true) - .eq("applied", false); + .innerJoin( + rewardPrograms, + eq( + rewardRedemptions.internal_reward_program_id, + rewardPrograms.internal_id, + ), + ) + .innerJoin( + rewards, + eq(rewards.internal_id, rewardPrograms.internal_reward_id), + ) + .where( + and( + eq(referralCodes.internal_customer_id, internalCustomerId), + eq(rewardRedemptions.triggered, true), + eq(rewardRedemptions.applied, false), + ), + ); - if (error) { - throw error; - } + if (data.length == 0) return []; - return data; + let processed = data.map((d) => ({ + ...d.reward_redemptions, + referral_code: d.referral_codes, + reward_program: { + ...d.reward_programs, + reward: d.rewards, + }, + })); + + return processed; } } diff --git a/server/src/internal/rewards/RewardService.ts b/server/src/internal/rewards/RewardService.ts index ce569fdda..028434922 100644 --- a/server/src/internal/rewards/RewardService.ts +++ b/server/src/internal/rewards/RewardService.ts @@ -1,151 +1,134 @@ -import { generateId } from "@/utils/genUtils.js"; -import { AppEnv, Reward } from "@autumn/shared"; -import { SupabaseClient } from "@supabase/supabase-js"; +import { AppEnv, ErrCode, Reward, rewards } from "@autumn/shared"; +import { DrizzleCli } from "@/db/initDrizzle.js"; +import RecaseError from "@/utils/errorUtils.js"; +import { and, desc, eq, or } from "drizzle-orm"; export class RewardService { + static async get({ + db, + idOrInternalId, + orgId, + env, + }: { + db: DrizzleCli; + idOrInternalId: string; + orgId: string; + env: AppEnv; + }) { + let result = await db.query.rewards.findFirst({ + where: and( + or( + eq(rewards.id, idOrInternalId), + eq(rewards.internal_id, idOrInternalId), + ), + eq(rewards.org_id, orgId), + eq(rewards.env, env), + ), + }); + + if (!result) { + return null; + } + + return result as Reward; + } + static async insert({ - sb, + db, data, }: { - sb: SupabaseClient; + db: DrizzleCli; data: Reward | Reward[]; }) { - const { data: insertedData, error } = await sb - .from("rewards") - .insert(data) - .select(); - - if (error) { - throw error; - } - return insertedData; + let results = await db.insert(rewards).values(data as Reward); + return results as Reward[]; } - static async getAll({ - sb, + static async list({ + db, orgId, env, }: { - sb: SupabaseClient; + db: DrizzleCli; orgId: string; env: AppEnv; }) { - const { data, error } = await sb - .from("rewards") - .select() - .eq("org_id", orgId) - .eq("env", env); + let results = await db.query.rewards.findMany({ + where: and(eq(rewards.org_id, orgId), eq(rewards.env, env)), + orderBy: [desc(rewards.internal_id)], + }); - if (error) { - throw error; - } - return data; + return results as Reward[]; } - static async deleteStrict({ - sb, + static async delete({ + db, internalId, env, orgId, }: { - sb: SupabaseClient; + db: DrizzleCli; internalId: string; env: AppEnv; orgId: string; }) { - const { error } = await sb - .from("rewards") - .delete() - .eq("internal_id", internalId) - .eq("env", env) - .eq("org_id", orgId); - if (error) { - throw error; - } - } - - static async getById({ - sb, - id, - orgId, - env, - }: { - sb: SupabaseClient; - id: string; - orgId: string; - env: AppEnv; - }) { - const { data, error } = await sb - .from("rewards") - .select() - .eq("id", id) - .eq("org_id", orgId) - .eq("env", env) - .single(); - - if (error) { - if (error.code == "PGRST116") { - return null; - } - throw error; - } - return data; - } - - static async getByInternalId({ - sb, - internalId, - orgId, - env, - }: { - sb: SupabaseClient; - internalId: string; - orgId: string; - env: AppEnv; - }) { - const { data, error } = await sb - .from("rewards") - .select() - .eq("internal_id", internalId) - .eq("org_id", orgId) - .eq("env", env) - .single(); - - if (error) { - if (error.code == "PGRST116") { - return null; - } - throw error; - } - return data; + await db + .delete(rewards) + .where( + and( + eq(rewards.internal_id, internalId), + eq(rewards.env, env), + eq(rewards.org_id, orgId), + ), + ); } static async update({ - sb, + db, internalId, env, orgId, update, }: { - sb: SupabaseClient; + db: DrizzleCli; internalId: string; env: AppEnv; orgId: string; update: Partial; }) { - const { data, error } = await sb - .from("rewards") - .update(update) - .eq("internal_id", internalId) - .eq("env", env) - .eq("org_id", orgId) - .select() - .single(); + let result = await db + .update(rewards) + .set(update) + .where( + and( + eq(rewards.internal_id, internalId), + eq(rewards.env, env), + eq(rewards.org_id, orgId), + ), + ) + .returning(); - if (error) { - throw error; + if (result.length === 0) { + throw new RecaseError({ + message: `Reward ${internalId} not found`, + code: ErrCode.InvalidRequest, + }); } - return data; + return result[0] as Reward; + } + + static async deleteByOrgId({ + db, + orgId, + env, + }: { + db: DrizzleCli; + orgId: string; + env: AppEnv; + }) { + await db + .delete(rewards) + .where(and(eq(rewards.org_id, orgId), eq(rewards.env, env))); } } diff --git a/server/src/internal/rewards/referralUtils.ts b/server/src/internal/rewards/referralUtils.ts index f1579153d..6bab40a2f 100644 --- a/server/src/internal/rewards/referralUtils.ts +++ b/server/src/internal/rewards/referralUtils.ts @@ -39,7 +39,6 @@ export const generateReferralCode = () => { // Trigger reward export const triggerRedemption = async ({ db, - sb, referralCode, org, env, @@ -48,7 +47,6 @@ export const triggerRedemption = async ({ redemption, }: { db: DrizzleCli; - sb: any; org: any; env: AppEnv; logger: any; @@ -102,7 +100,7 @@ export const triggerRedemption = async ({ } let updatedRedemption = await RewardRedemptionService.update({ - sb, + db, id: redemption.id, updates: { applied, @@ -116,7 +114,6 @@ export const triggerRedemption = async ({ }; export const triggerFreeProduct = async ({ - sb, db, referralCode, redeemer, @@ -126,7 +123,6 @@ export const triggerFreeProduct = async ({ env, logger, }: { - sb: any; db: DrizzleCli; referralCode: ReferralCode; redeemer: Customer; @@ -202,7 +198,6 @@ export const triggerFreeProduct = async ({ await createFullCusProduct({ db, - sb, attachParams: redeemerAttachParams, }); logger.info(`✅ Added ${fullProduct.name} to redeemer`); @@ -211,7 +206,6 @@ export const triggerFreeProduct = async ({ if (addToReferrer) { await createFullCusProduct({ db, - sb, attachParams: { ...attachParams, customer: referrer, @@ -222,7 +216,7 @@ export const triggerFreeProduct = async ({ } await RewardRedemptionService.update({ - sb, + db, id: redemption.id, updates: { triggered: true, diff --git a/server/src/internal/rewards/triggerCheckoutReward.ts b/server/src/internal/rewards/triggerCheckoutReward.ts index 0a45e82f8..0c6cd17d2 100644 --- a/server/src/internal/rewards/triggerCheckoutReward.ts +++ b/server/src/internal/rewards/triggerCheckoutReward.ts @@ -7,12 +7,10 @@ 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; }) => { @@ -27,7 +25,7 @@ export const runTriggerCheckoutReward = async ({ // 1. Check if redemption exists let redemptions = await RewardRedemptionService.getByCustomer({ - sb, + db, internalCustomerId: customer.internal_id, // customer that redeemed code withRewardProgram: true, triggered: false, @@ -75,7 +73,7 @@ export const runTriggerCheckoutReward = async ({ // Get redemption count let redemptionCount = await RewardProgramService.getCodeRedemptionCount({ - sb, + db, referralCodeId: referralCode.id, }); @@ -90,7 +88,6 @@ export const runTriggerCheckoutReward = async ({ if (rewardCat === RewardCategory.FreeProduct) { await triggerFreeProduct({ db, - sb, referralCode, redeemer: customer, rewardProgram: reward_program, @@ -102,7 +99,6 @@ export const runTriggerCheckoutReward = async ({ } else { await triggerRedemption({ db, - sb, referralCode, org, env, diff --git a/server/src/internal/subscriptions/SubService.ts b/server/src/internal/subscriptions/SubService.ts index 122841aa0..67db0f180 100644 --- a/server/src/internal/subscriptions/SubService.ts +++ b/server/src/internal/subscriptions/SubService.ts @@ -1,38 +1,34 @@ -import { SupabaseClient } from "@supabase/supabase-js"; -import { AppEnv, Subscription } from "@autumn/shared"; +import { AppEnv, ErrCode, Subscription, subscriptions } from "@autumn/shared"; import { generateId } from "@/utils/genUtils.js"; import Stripe from "stripe"; +import { DrizzleCli } from "@/db/initDrizzle.js"; +import RecaseError from "@/utils/errorUtils.js"; +import { and, eq, inArray } from "drizzle-orm"; export class SubService { - static async createSub({ - sb, - sub, - }: { - sb: SupabaseClient; - sub: Subscription; - }) { - let { data, error } = await sb - .from("subscriptions") - .insert(sub) - .select() - .single(); + static async createSub({ db, sub }: { db: DrizzleCli; sub: Subscription }) { + let data = await db.insert(subscriptions).values(sub).returning(); - if (error) { - throw error; + if (data.length === 0) { + throw new RecaseError({ + code: ErrCode.InsertSubscriptionFailed, + message: "Failed to create subscription", + statusCode: 500, + }); } - return data; + return data[0] as Subscription; } static async addUsageFeatures({ - sb, + db, stripeId, scheduleId, usageFeatures, orgId, env, }: { - sb: SupabaseClient; + db: DrizzleCli; stripeId?: string; scheduleId?: string; usageFeatures: string[]; @@ -43,24 +39,21 @@ export class SubService { throw new Error("Either stripeId or scheduleId must be provided"); } - let query = sb.from("subscriptions").select("*"); - if (stripeId) { - query = query.eq("stripe_id", stripeId); - } else if (scheduleId) { - query = query.eq("stripe_schedule_id", scheduleId); - } - - let { data, error: curSubsError } = await query; - - if (curSubsError || !data) { - throw curSubsError; - } + let data = await db + .select() + .from(subscriptions) + .where( + and( + stripeId ? eq(subscriptions.stripe_id, stripeId) : undefined, + scheduleId + ? eq(subscriptions.stripe_schedule_id, scheduleId) + : undefined, + ), + ); if (data.length == 0) { - // throw new Error("Subscription not found"); - // From old plan return await SubService.createSub({ - sb, + db, sub: { id: generateId("sub"), created_at: Date.now(), @@ -76,171 +69,109 @@ export class SubService { } let curSub = data[0]; - let { data: updatedSub, error } = await sb - .from("subscriptions") - .update({ + let updateResult = await db + .update(subscriptions) + .set({ usage_features: [ - ...new Set([...curSub.usage_features, ...usageFeatures]), + ...new Set([...(curSub.usage_features || []), ...usageFeatures]), ], }) - .eq("id", curSub.id) - .select() - .single(); + .where(eq(subscriptions.id, curSub.id)) + .returning(); - if (error) { - throw error; + if (updateResult.length === 0) { + throw new RecaseError({ + code: ErrCode.UpdateSubscriptionFailed, + message: "Failed to update subscription", + statusCode: 500, + }); } - return updatedSub; + return updateResult[0] as Subscription; } static async updateFromStripe({ - sb, + db, stripeSub, }: { - sb: SupabaseClient; + db: DrizzleCli; stripeSub: Stripe.Subscription; }) { - let { data, error } = await sb - .from("subscriptions") - .update({ + let results = await db + .update(subscriptions) + .set({ current_period_start: stripeSub.current_period_start, current_period_end: stripeSub.current_period_end, }) - .eq("stripe_id", stripeSub.id) - .select() - .single(); + .where(eq(subscriptions.stripe_id, stripeSub.id)) + .returning(); - if (error) { - throw error; - } - - return data; - } - - static async updateFromStripeId({ - sb, - stripeId, - updates, - }: { - sb: SupabaseClient; - stripeId: string; - updates: any; - }) { - let { data, error } = await sb - .from("subscriptions") - .update(updates) - .eq("stripe_id", stripeId) - .select() - .single(); - - if (error) { - throw error; - } - - return data; - } - - static async getFromScheduleId({ - sb, - scheduleId, - }: { - sb: SupabaseClient; - scheduleId: string; - }) { - let { data, error } = await sb - .from("subscriptions") - .select("*") - .eq("stripe_schedule_id", scheduleId); - - if (error || !data) { - throw error; - } - - if (data.length == 0) { + if (results.length === 0) { return null; } - return data[0]; + return results[0] as Subscription; } - static async deleteFromStripeId({ - sb, - stripeId, + static async getFromScheduleId({ + db, + scheduleId, }: { - sb: SupabaseClient; - stripeId: string; + db: DrizzleCli; + scheduleId: string; }) { - let { error } = await sb - .from("subscriptions") - .delete() - .eq("stripe_id", stripeId); + let data = await db + .select() + .from(subscriptions) + .where(eq(subscriptions.stripe_schedule_id, scheduleId)); - if (error) { - throw error; + if (data.length === 0) { + return null; } - return; + return data[0] as Subscription; } static async deleteFromScheduleId({ - sb, + db, scheduleId, }: { - sb: SupabaseClient; + db: DrizzleCli; scheduleId: string; }) { - let { error } = await sb - .from("subscriptions") - .delete() - .eq("stripe_schedule_id", scheduleId); - - if (error) { - throw error; - } + await db + .delete(subscriptions) + .where(eq(subscriptions.stripe_schedule_id, scheduleId)); return; } static async updateFromScheduleId({ - sb, + db, scheduleId, updates, }: { - sb: SupabaseClient; + db: DrizzleCli; scheduleId: string; updates: any; }) { - let { data: updatedSub, error } = await sb - .from("subscriptions") - .update(updates) - .eq("stripe_schedule_id", scheduleId) - .select() - .single(); + let results = await db + .update(subscriptions) + .set(updates) + .where(eq(subscriptions.stripe_schedule_id, scheduleId)) + .returning(); - if (error) { - throw error; + if (results.length === 0) { + return null; } - return updatedSub; + return results[0] as Subscription; } - static async getInStripeIds({ - sb, - ids, - }: { - sb: SupabaseClient; - ids: string[]; - }) { - let { data, error } = await sb - .from("subscriptions") - .select("*") - .in("stripe_id", ids); - - if (error) { - throw error; - } - - return data; + static async getInStripeIds({ db, ids }: { db: DrizzleCli; ids: string[] }) { + return (await db + .select() + .from(subscriptions) + .where(inArray(subscriptions.stripe_id, ids))) as Subscription[]; } } diff --git a/server/src/middleware/authMiddleware.ts b/server/src/middleware/authMiddleware.ts index f1d0aed3d..5ddf3bd67 100644 --- a/server/src/middleware/authMiddleware.ts +++ b/server/src/middleware/authMiddleware.ts @@ -43,7 +43,7 @@ export const withOrgAuth = async (req: any, res: any, next: NextFunction) => { let tokenOrg = tokenData!.org as any; let { org, features } = await OrgService.getWithFeatures({ - sb: req.sb, + db: req.db, orgId: tokenOrg.id, env: req.env, }); diff --git a/server/src/middleware/publicAuthMiddleware.ts b/server/src/middleware/publicAuthMiddleware.ts index 9c9365e8b..edb386521 100644 --- a/server/src/middleware/publicAuthMiddleware.ts +++ b/server/src/middleware/publicAuthMiddleware.ts @@ -85,7 +85,7 @@ export const verifyBearerPublishableKey = async ( : AppEnv.Live; const data = await verifyPublicKey({ - sb: req.sb, + db: req.db, pkey, env, }); diff --git a/server/src/queue/queue.ts b/server/src/queue/queue.ts index dc7e22720..1eac83a12 100644 --- a/server/src/queue/queue.ts +++ b/server/src/queue/queue.ts @@ -4,8 +4,7 @@ import { QueueManager } from "./QueueManager.js"; import { createLogtail } from "@/external/logtail/logtailUtils.js"; import { runUpdateUsageTask } from "@/trigger/updateUsageTask.js"; import { JobName } from "./JobName.js"; -import { createSupabaseClient } from "@/external/supabaseUtils.js"; -import { SupabaseClient } from "@supabase/supabase-js"; + import { runMigrationTask } from "@/internal/migrations/runMigrationTask.js"; import { runTriggerCheckoutReward } from "@/internal/rewards/triggerCheckoutReward.js"; import { runSaveFeatureDisplayTask } from "@/internal/features/featureUtils.js"; @@ -68,14 +67,12 @@ const initWorker = ({ queue, useBackup, logtail, - sb, db, }: { id: number; queue: Queue; useBackup: boolean; logtail: any; - sb: SupabaseClient; db: DrizzleCli; }) => { let worker = new Worker( @@ -95,7 +92,6 @@ const initWorker = ({ db, payload: job.data, logger: logtail, - sb, }); return; } @@ -118,12 +114,14 @@ const initWorker = ({ try { await sendProductsUpdatedWebhook({ db, - sb, logger: logtail, data: job.data, }); } catch (error) { - console.error("Error processing job:", error); + console.error( + "Error processing sendProductsUpdatedWebhook job:", + error, + ); } finally { await releaseLock({ customerId: lockKey, useBackup }); } @@ -149,7 +147,6 @@ const initWorker = ({ await runTriggerCheckoutReward({ db, payload: job.data, - sb, logger: logtail, }); } catch (error) { @@ -181,14 +178,12 @@ const initWorker = ({ await runUpdateBalanceTask({ payload: job.data, logger: logtail, - sb, db, }); } else if (job.name === JobName.UpdateUsage) { await runUpdateUsageTask({ payload: job.data, logger: logtail, - sb, db, }); } @@ -241,7 +236,6 @@ export const initWorkers = async () => { const backupQueue = await QueueManager.getQueue({ useBackup: true }); await CacheManager.getInstance(); const logtail = createLogtail(); - const sb = createSupabaseClient(); const { db, client } = initDrizzle(); for (let i = 0; i < NUM_WORKERS; i++) { @@ -251,7 +245,6 @@ export const initWorkers = async () => { queue: mainQueue, useBackup: false, logtail, - sb, db, }), ); @@ -261,7 +254,6 @@ export const initWorkers = async () => { queue: backupQueue, useBackup: true, logtail, - sb, db, }), ); diff --git a/server/src/trigger/adjustAllowance.ts b/server/src/trigger/adjustAllowance.ts index 71589d011..c6de9f98b 100644 --- a/server/src/trigger/adjustAllowance.ts +++ b/server/src/trigger/adjustAllowance.ts @@ -48,7 +48,6 @@ type CusEntWithCusProduct = FullCustomerEntitlement & { export const adjustAllowance = async ({ db, - sb, env, org, affectedFeature, @@ -63,7 +62,6 @@ export const adjustAllowance = async ({ fromEntities = false, }: { db: DrizzleCli; - sb: SupabaseClient; env: AppEnv; affectedFeature: Feature; org: Organization; @@ -115,7 +113,7 @@ export const adjustAllowance = async ({ let stripeCli = createStripeCli({ org, env }); let sub = await getUsageBasedSub({ - sb: sb, + db, stripeCli, subIds: cusProduct.subscription_ids!, feature: affectedFeature, @@ -256,7 +254,7 @@ export const adjustAllowance = async ({ } catch (error) {} await InvoiceService.createInvoiceFromStripe({ - sb, + db, stripeInvoice: latestInvoice, internalCustomerId: customer.internal_id, internalEntityId: cusProduct.internal_entity_id || undefined, diff --git a/server/src/trigger/updateBalanceTask.ts b/server/src/trigger/updateBalanceTask.ts index 04d342d8e..9b6d4214d 100644 --- a/server/src/trigger/updateBalanceTask.ts +++ b/server/src/trigger/updateBalanceTask.ts @@ -37,7 +37,6 @@ import { DrizzleCli } from "@/db/initDrizzle.js"; type DeductParams = { db: DrizzleCli; - sb: SupabaseClient; env: AppEnv; org: Organization; cusPrices: FullCustomerPrice[]; @@ -308,7 +307,7 @@ export const deductAllowanceFromCusEnt = async ({ entityId?: string | null; setZeroAdjustment?: boolean; }) => { - const { db, sb, feature, env, org, cusPrices, customer } = deductParams; + const { db, feature, env, org, cusPrices, customer } = deductParams; if (toDeduct == 0) { return 0; @@ -355,7 +354,6 @@ export const deductAllowanceFromCusEnt = async ({ await adjustAllowance({ db, - sb, env, org, cusPrices: cusPrices as any, @@ -412,7 +410,7 @@ export const deductFromUsageBasedCusEnt = async ({ entityId?: string | null; setZeroAdjustment?: boolean; }) => { - const { db, sb, feature, env, org, cusPrices, customer } = deductParams; + const { db, feature, env, org, cusPrices, customer } = deductParams; // Deduct from usage-based price const usageBasedEnt = cusEnts.find( @@ -464,7 +462,6 @@ export const deductFromUsageBasedCusEnt = async ({ await adjustAllowance({ db, - sb, env, affectedFeature: feature, org, @@ -480,7 +477,6 @@ export const deductFromUsageBasedCusEnt = async ({ // Main function to update customer balance export const updateCustomerBalance = async ({ db, - sb, customerId, entityId, event, @@ -490,7 +486,6 @@ export const updateCustomerBalance = async ({ logger, }: { db: DrizzleCli; - sb: SupabaseClient; customerId: string; entityId: string; event: Event; @@ -501,8 +496,8 @@ export const updateCustomerBalance = async ({ }) => { const startTime = performance.now(); console.log("REVERSE DEDUCTION ORDER", org.config.reverse_deduction_order); - const customer = await CusService.getWithProducts({ - sb, + const customer = await CusService.getFull({ + db, idOrInternalId: customerId, orgId: org.id, env, @@ -511,7 +506,6 @@ export const updateCustomerBalance = async ({ }); const { cusEnts, cusPrices } = await getCusEntsInFeatures({ - sb, customer, internalFeatureIds: features.map((f) => f.internal_id!), logger, @@ -559,7 +553,6 @@ export const updateCustomerBalance = async ({ features, deductParams: { db, - sb, feature, env, org, @@ -582,7 +575,6 @@ export const updateCustomerBalance = async ({ cusEnts, deductParams: { db, - sb, feature, env, org, @@ -602,12 +594,10 @@ export const runUpdateBalanceTask = async ({ payload, logger, db, - sb, }: { payload: any; logger: any; db: DrizzleCli; - sb: SupabaseClient; }) => { try { // 1. Update customer balance @@ -620,7 +610,6 @@ export const runUpdateBalanceTask = async ({ const cusEnts: any = await updateCustomerBalance({ db, - sb, customerId, features, event, diff --git a/server/src/trigger/updateUsageTask.ts b/server/src/trigger/updateUsageTask.ts index 5c0366689..a018661f0 100644 --- a/server/src/trigger/updateUsageTask.ts +++ b/server/src/trigger/updateUsageTask.ts @@ -162,7 +162,6 @@ const logUsageUpdate = ({ // Main function to update customer balance export const updateUsage = async ({ db, - sb, customerId, features, org, @@ -174,7 +173,6 @@ export const updateUsage = async ({ entityId, }: { db: DrizzleCli; - sb: SupabaseClient; customerId: string; features: Feature[]; org: Organization; @@ -185,8 +183,8 @@ export const updateUsage = async ({ logger: any; entityId?: string; }) => { - const customer = await CusService.getWithProducts({ - sb, + const customer = await CusService.getFull({ + db, idOrInternalId: customerId, orgId: org.id, env, @@ -195,7 +193,6 @@ export const updateUsage = async ({ }); const { cusEnts, cusPrices } = await getCusEntsInFeatures({ - sb, customer, internalFeatureIds: features.map((f) => f.internal_id!), logger, @@ -240,7 +237,6 @@ export const updateUsage = async ({ features, deductParams: { db, - sb, feature, env, org, @@ -264,7 +260,6 @@ export const updateUsage = async ({ cusEnts, deductParams: { db, - sb, feature, env, org, @@ -285,12 +280,10 @@ export const runUpdateUsageTask = async ({ payload, logger, db, - sb, }: { payload: any; logger: any; db: DrizzleCli; - sb: SupabaseClient; }) => { try { // 1. Update customer balance @@ -313,7 +306,6 @@ export const runUpdateUsageTask = async ({ const cusEnts: any = await updateUsage({ db, - sb, customerId, features, value, diff --git a/server/src/utils/models/Request.ts b/server/src/utils/models/Request.ts index 0fd707484..d49baca25 100644 --- a/server/src/utils/models/Request.ts +++ b/server/src/utils/models/Request.ts @@ -1,6 +1,5 @@ import { AppEnv, Feature, MinOrg, Organization } from "@autumn/shared"; import { Logtail } from "@logtail/node"; -import { SupabaseClient } from "@supabase/supabase-js"; import type { Request as ExpressRequest, Response as ExpressResponse, @@ -11,7 +10,6 @@ import { DrizzleCli } from "@/db/initDrizzle.js"; import { PostHog } from "posthog-node"; export interface ExtendedRequest extends ExpressRequest { - sb: SupabaseClient; pg: Client; db: DrizzleCli; diff --git a/server/src/websockets/initWs.ts b/server/src/websockets/initWs.ts index dc96ace22..09827dd79 100644 --- a/server/src/websockets/initWs.ts +++ b/server/src/websockets/initWs.ts @@ -37,21 +37,12 @@ const getPkey = async (req: any) => { } const env = pkey.startsWith("am_pk_test_") ? AppEnv.Sandbox : AppEnv.Live; - const sb = createSupabaseClient(); - const org = await OrgService.getFromPkeyWithFeatures({ sb, pkey, env }); - if (!org) { - return { - error: ErrCode.OrgNotFound, - fallback: false, - statusCode: 401, - }; - } - - req.env = env; - req.org = org; - - return { env, org }; + return { + error: ErrCode.OrgNotFound, + fallback: false, + statusCode: 401, + }; }; class WebSocketRouter { @@ -92,8 +83,6 @@ class WebSocketRouter { const path = req.url; try { - const { env, org } = await getPkey(req); - req.sb = createSupabaseClient(); } catch (error) { console.log("Failed to get org from pkey"); ws.close(1000, "Invalid publishable key"); @@ -128,93 +117,14 @@ export const initWs = (server: http.Server) => { wsRouter.on({ route: "/:customer_id/entitlements", callback: async (ws, req, params) => { - await handleRealtimeBalances(ws, req, params); + console.log("entitlements", params); }, }); wsRouter.on({ route: "/:customer_id/entitlements/:feature_id", callback: async (ws, req, params) => { - await handleRealtimeBalance(ws, req, params); + console.log("entitlement", params); }, }); }; - -const handleRealtimeBalances = async (ws: WebSocket, req: any, params: any) => { - // try { - // const { org, env, sb } = req; - // // 1. Get all customer balances - // const balances = await getCusBalances({ - // sb, - // customerId: params.customer_id, - // orgId: org.id, - // env, - // }); - // ws.send(JSON.stringify({ data: balances, error: null })); - // const channel = `${org.id}_${env}_${params.customer_id}`; - // sb.channel(channel) - // .on( - // "broadcast", - // { event: SbChannelEvent.BalanceUpdated }, - // async (payload: any) => { - // const data = payload.payload; - // console.log("Received balance update event from supabase:", data); - // const newBalances = await getCusBalances({ - // customerId: params.customer_id, - // orgId: org.id, - // env, - // }); - // ws.send( - // JSON.stringify({ - // data: newBalances, - // error: null, - // }) - // ); - // } - // ) - // .subscribe(); - // } catch (error) { - // console.log("Error getting customer balances", error); - // ws.send( - // JSON.stringify({ - // data: null, - // error: "Error getting customer balances", - // }) - // ); - // } -}; - -const handleRealtimeBalance = async (ws: WebSocket, req: any, params: any) => { - try { - const { org, env, sb } = req; - - const channel = `${org.id}_${env}_${params.customer_id}`; - - sb.channel(channel) - .on( - "broadcast", - { event: SbChannelEvent.BalanceUpdated }, - async (payload: any) => { - if (payload.payload.feature_id !== params.feature_id) { - return; - } - - // ws.send( - // JSON.stringify({ - // data: newBalance, - // error: null, - // }) - // ); - }, - ) - .subscribe(); - } catch (error) { - console.log("Error getting feature balance", error); - ws.send( - JSON.stringify({ - data: null, - error: "Error getting feature balance", - }), - ); - } -}; diff --git a/server/test.sh b/server/test.sh index cac1f8673..273c85207 100755 --- a/server/test.sh +++ b/server/test.sh @@ -5,9 +5,10 @@ MOCHA_CMD="npx mocha --parallel --timeout 10000000 --ignore tests/00_setup.ts" # TEST PARALLEL if [ "$1" == "basic-parallel" ]; then MOCHA_PARALLEL=true $MOCHA_SETUP && $MOCHA_CMD \ - tests/basic/*.ts \ - tests/basic/multi-feature/*.ts \ - tests/basic/entities/*.ts \ + 'tests/basic/referrals/*.ts' 'tests/attach/**/*.ts' \ + # tests/basic/*.ts \ + # tests/basic/multi-feature/*.ts \ + # tests/basic/entities/*.ts \ # && $MOCHA_CMD \ # 'tests/basic/referrals/*.ts' 'tests/attach/**/*.ts' \ diff --git a/server/tests/00_setup.ts b/server/tests/00_setup.ts index 5f39b6c64..91668cf2d 100644 --- a/server/tests/00_setup.ts +++ b/server/tests/00_setup.ts @@ -1,3 +1,6 @@ +import dotenv from "dotenv"; +dotenv.config(); + import { createSupabaseClient } from "@/external/supabaseUtils.js"; import { AppEnv } from "@autumn/shared"; import { clearOrg, setupOrg } from "tests/utils/setup.js"; diff --git a/server/tests/advanced/arrear_prorated/arrear_prorated2.ts b/server/tests/advanced/arrear_prorated/arrear_prorated2.ts index 848f79847..c6525791a 100644 --- a/server/tests/advanced/arrear_prorated/arrear_prorated2.ts +++ b/server/tests/advanced/arrear_prorated/arrear_prorated2.ts @@ -2,7 +2,7 @@ import { expect } from "chai"; import { AutumnCli } from "tests/cli/AutumnCli.js"; import { advanceProducts, features } from "tests/global.js"; import { compareMainProduct } from "tests/utils/compare.js"; -import { advanceMonths, advanceTestClock } from "tests/utils/stripeUtils.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; import { timeout } from "tests/utils/genUtils.js"; import { createStripeCli } from "@/external/stripe/utils.js"; import { addDays, addMonths, format } from "date-fns"; @@ -189,7 +189,7 @@ describe(`${chalk.yellowBright( customerId, org: this.org, env: this.env, - sb: this.sb, + db: this.db, }, ); @@ -200,7 +200,6 @@ describe(`${chalk.yellowBright( testClockId = createdTestClockId; - // Update org config await this.sb .from("organizations") .update({ @@ -239,7 +238,6 @@ describe(`${chalk.yellowBright( await checkSubscriptionContainsProducts({ db: this.db, - sb: this.sb, org: this.org, env: this.env, subscriptionId: subId, diff --git a/server/tests/advanced/arrear_prorated/arrear_prorated3.ts b/server/tests/advanced/arrear_prorated/arrear_prorated3.ts index 1560fd827..768e0b465 100644 --- a/server/tests/advanced/arrear_prorated/arrear_prorated3.ts +++ b/server/tests/advanced/arrear_prorated/arrear_prorated3.ts @@ -198,7 +198,7 @@ describe(`${chalk.yellowBright( customerId, org: this.org, env: this.env, - sb: this.sb, + db: this.db, }, ); @@ -230,7 +230,6 @@ describe(`${chalk.yellowBright( await checkSubscriptionContainsProducts({ db: this.db, - sb: this.sb, org: this.org, env: this.env, subscriptionId: subId, diff --git a/server/tests/advanced/coupons/coupon1.ts b/server/tests/advanced/coupons/coupon1.ts index 393c8db0b..5c07e79b7 100644 --- a/server/tests/advanced/coupons/coupon1.ts +++ b/server/tests/advanced/coupons/coupon1.ts @@ -37,7 +37,7 @@ describe( customerId, org: this.org, env: this.env, - sb: this.sb, + db: this.db, }); testClockId = testClockId1; customer = customer1; diff --git a/server/tests/advanced/coupons/coupon2.ts b/server/tests/advanced/coupons/coupon2.ts index f5e3b2eda..4e81c8978 100644 --- a/server/tests/advanced/coupons/coupon2.ts +++ b/server/tests/advanced/coupons/coupon2.ts @@ -34,7 +34,7 @@ describe( customerId, org: this.org, env: this.env, - sb: this.sb, + db: this.db, }); testClockId = testClockId1; customer = customer1; diff --git a/server/tests/advanced/usage/group_by.ts b/server/tests/advanced/usage/group_by.ts index fcbd10863..0900a13b1 100644 --- a/server/tests/advanced/usage/group_by.ts +++ b/server/tests/advanced/usage/group_by.ts @@ -15,7 +15,7 @@ import chalk from "chalk"; const PRECISION = 12; describe.skip(`${chalk.yellowBright( - "Testing group by -- regular metered1 feature" + "Testing group by -- regular metered1 feature", )}`, () => { let customerId = "group-by-basic-metered"; before(async function () { @@ -26,7 +26,7 @@ describe.skip(`${chalk.yellowBright( email: "group-by-basic-metered@example.com", }, attachPm: true, - sb: this.sb, + db: this.db, org: this.org, env: this.env, }); @@ -71,7 +71,7 @@ describe.skip(`${chalk.yellowBright( [groupProperty]: user == "null" ? null : user, value: randomVal, }, - }) + }), ); } @@ -88,7 +88,7 @@ describe.skip(`${chalk.yellowBright( customerId, features.metered1.id, true, - user == "null" ? undefined : user + user == "null" ? undefined : user, ); let expectedBalance = new Decimal(metered1Allowance) @@ -109,7 +109,7 @@ describe.skip(`${chalk.yellowBright( }); const entitlements = res.entitlements; const metered1 = entitlements.find( - (e: any) => e.feature_id === features.metered1.id + (e: any) => e.feature_id === features.metered1.id, ); expect(metered1.balance).to.equal(expectedBalance); } @@ -118,7 +118,7 @@ describe.skip(`${chalk.yellowBright( // TO ADD SUPPORT FOR IN THE FUTURE describe.skip(`${chalk.yellowBright( - "Testing group by -- advanced GPU usage" + "Testing group by -- advanced GPU usage", )}`, () => { let customerId = "group-by-advanced-gpu-usage-metered"; before(async function () { @@ -129,7 +129,7 @@ describe.skip(`${chalk.yellowBright( email: "group-by-advanced-gpu-usage@example.com", }, attachPm: true, - sb: this.sb, + db: this.db, org: this.org, env: this.env, }); @@ -175,7 +175,7 @@ describe.skip(`${chalk.yellowBright( customerId, creditSystems.gpuCredits.id, true, - user == "null" ? undefined : user + user == "null" ? undefined : user, ); let expectedCreditAllowance = new Decimal(creditAllowance) diff --git a/server/tests/advanced/usage/multi_interval1.ts b/server/tests/advanced/usage/multi_interval1.ts index 126a20a45..124c07e47 100644 --- a/server/tests/advanced/usage/multi_interval1.ts +++ b/server/tests/advanced/usage/multi_interval1.ts @@ -34,7 +34,7 @@ describe(`${chalk.yellowBright("multi_interval1: GPU starter annual")}`, () => { customerId, org: this.org, env: this.env, - sb: this.sb, + db: this.db, }); testClockId = insertedTestClockId; @@ -99,7 +99,7 @@ describe(`${chalk.yellowBright("multi_interval1: GPU starter annual")}`, () => { const invoices = res!.invoices; let invoiceIndex = invoices.findIndex((invoice: any) => - invoice.product_ids.includes(advanceProducts.gpuStarterAnnual.id) + invoice.product_ids.includes(advanceProducts.gpuStarterAnnual.id), ); await checkUsageInvoiceAmount({ @@ -172,7 +172,7 @@ describe(`${chalk.yellowBright("multi_interval1: GPU starter annual")}`, () => { assert.exists(eventSummary); assert.equal( eventSummary?.aggregated_value, - Math.round(totalCreditsUsed) + Math.round(totalCreditsUsed), ); assert.equal(invoices.length, 13 + 2); } catch (error) { diff --git a/server/tests/advanced/usage/multi_interval2.ts b/server/tests/advanced/usage/multi_interval2.ts index a4537e46f..53bfaaf61 100644 --- a/server/tests/advanced/usage/multi_interval2.ts +++ b/server/tests/advanced/usage/multi_interval2.ts @@ -23,7 +23,7 @@ import { Decimal } from "decimal.js"; // FOURTH, TEST GPU STARTER ANNUAL UPGRADE TO GPU PRO describe(`${chalk.yellowBright( - "multi_interval2: GPU starter annual upgrade to GPU pro annual" + "multi_interval2: GPU starter annual upgrade to GPU pro annual", )}`, () => { const customerId = "multi_interval2"; let testClockId = ""; @@ -37,7 +37,7 @@ describe(`${chalk.yellowBright( customerId, org: this.org, env: this.env, - sb: this.sb, + db: this.db, }); testClockId = insertedTestClockId; @@ -119,7 +119,7 @@ describe(`${chalk.yellowBright( const invoices = res!.invoices; let invoiceIndex = invoices.findIndex((invoice: any) => - invoice.product_ids.includes(advanceProducts.gpuStarterAnnual.id) + invoice.product_ids.includes(advanceProducts.gpuStarterAnnual.id), ); await checkUsageInvoiceAmount({ @@ -178,7 +178,7 @@ describe(`${chalk.yellowBright( let roundedFirst = Math.ceil( new Decimal(totalCreditsUsed) .div(usagePrice?.config?.billing_units!) - .toNumber() + .toNumber(), ); let roundedTotalCreditsUsed = new Decimal(roundedFirst) .mul(usagePrice?.config?.billing_units!) diff --git a/server/tests/advanced/usage/usage1.ts b/server/tests/advanced/usage/usage1.ts index 08917439a..3b6c5aecf 100644 --- a/server/tests/advanced/usage/usage1.ts +++ b/server/tests/advanced/usage/usage1.ts @@ -26,7 +26,7 @@ describe(`${chalk.yellowBright("usage1: Pro with overage")}`, () => { customerId, org: this.org, env: this.env, - sb: this.sb, + db: this.db, }); customer = customer_; @@ -60,7 +60,7 @@ describe(`${chalk.yellowBright("usage1: Pro with overage")}`, () => { AutumnCli.sendEvent({ customerId: customerId, eventName: features.metered1.eventName, - }) + }), ); } @@ -74,7 +74,7 @@ describe(`${chalk.yellowBright("usage1: Pro with overage")}`, () => { expect(res!.allowed).to.be.true; const balance = res!.balances.find( - (balance: any) => balance.feature_id === features.metered1.id + (balance: any) => balance.feature_id === features.metered1.id, ); const proOverageAmt = @@ -120,13 +120,13 @@ describe(`${chalk.yellowBright("usage1: Pro with overage")}`, () => { const invoice2 = invoices[0]; expect(invoice2.total).to.equal( - price + products.proWithOverage.prices[0].config.amount + price + products.proWithOverage.prices[0].config.amount, ); } catch (error) { console.group(); console.log( "Expected invoices[0] to have total of: ", - price + products.proWithOverage.prices[0].config.amount + price + products.proWithOverage.prices[0].config.amount, ); console.log("Invoices", invoices); console.group(); diff --git a/server/tests/advanced/usage/usage2.ts b/server/tests/advanced/usage/usage2.ts index df67e497c..cd2632223 100644 --- a/server/tests/advanced/usage/usage2.ts +++ b/server/tests/advanced/usage/usage2.ts @@ -32,8 +32,8 @@ describe(`${chalk.yellowBright("usage2: GPU starter monthly")}`, () => { customerId, org: this.org, env: this.env, - sb: this.sb, - } + db: this.db, + }, ); testClockId = createdTestClockId; @@ -78,7 +78,7 @@ describe(`${chalk.yellowBright("usage2: GPU starter monthly")}`, () => { let creditsUsed = getCreditsUsed( creditSystems.gpuCredits, gpuId, - randomVal + randomVal, ); totalCreditsUsed = new Decimal(totalCreditsUsed) @@ -90,7 +90,7 @@ describe(`${chalk.yellowBright("usage2: GPU starter monthly")}`, () => { customerId: customerId, eventName: gpuId, properties: { value: randomVal }, - }) + }), ); } @@ -101,7 +101,7 @@ describe(`${chalk.yellowBright("usage2: GPU starter monthly")}`, () => { const { allowed, balanceObj }: any = await AutumnCli.entitled( customerId, creditSystems.gpuCredits.id, - true + true, ); let creditAllowance = @@ -110,7 +110,7 @@ describe(`${chalk.yellowBright("usage2: GPU starter monthly")}`, () => { try { expect(allowed).to.be.true; expect(balanceObj!.balance).to.equal( - new Decimal(creditAllowance).minus(totalCreditsUsed).toNumber() + new Decimal(creditAllowance).minus(totalCreditsUsed).toNumber(), ); console.log(" - Total credits used: ", totalCreditsUsed); console.log(" - Balance: ", balanceObj!.balance); @@ -149,7 +149,7 @@ describe(`${chalk.yellowBright("usage2: GPU starter monthly")}`, () => { const { allowed, balanceObj }: any = await AutumnCli.entitled( customerId, creditSystems.gpuCredits.id, - true + true, ); let allowance = diff --git a/server/tests/advanced/usage/usage3.ts b/server/tests/advanced/usage/usage3.ts index 6b0e2a23b..853a6ca0a 100644 --- a/server/tests/advanced/usage/usage3.ts +++ b/server/tests/advanced/usage/usage3.ts @@ -39,7 +39,7 @@ describe(`${chalk.yellowBright( customerId, org: this.org, env: this.env, - sb: this.sb, + db: this.db, }); testClockId = insertedTestClockId; @@ -101,9 +101,8 @@ describe(`${chalk.yellowBright( const stripeCli = createStripeCli({ org: this.org, env: this.env }); let subscriptionId = res.products[0].subscription_ids![0]!; - checkSubscriptionContainsProducts({ + await checkSubscriptionContainsProducts({ db: this.db, - sb: this.sb, org: this.org, env: this.env, subscriptionId, diff --git a/server/tests/alex/01_free.ts b/server/tests/alex/01_free.ts index 870339b16..0846bef63 100644 --- a/server/tests/alex/01_free.ts +++ b/server/tests/alex/01_free.ts @@ -14,7 +14,7 @@ describe(chalk.yellowBright("Free customer"), () => { // name: null, // email: null, }, - sb: this.sb, + db: this.db, org: this.org, env: this.env, }); diff --git a/server/tests/alex/02_pro.ts b/server/tests/alex/02_pro.ts index 81dfe8fe9..5287875fc 100644 --- a/server/tests/alex/02_pro.ts +++ b/server/tests/alex/02_pro.ts @@ -18,7 +18,7 @@ describe(chalk.yellowBright("Pro entitlements"), () => { // name: "Alex Pro Customer", email: "alex-pro-customer@test.com", }, - sb: this.sb, + db: this.db, org: this.org, env: this.env, }); diff --git a/server/tests/alex/03_premium.ts b/server/tests/alex/03_premium.ts index 274adb0f0..18c48f6a1 100644 --- a/server/tests/alex/03_premium.ts +++ b/server/tests/alex/03_premium.ts @@ -18,7 +18,7 @@ describe(chalk.yellowBright("Premium plan"), () => { name: "Alex Premium Customer", email: "alex-premium-customer@test.com", }, - sb: this.sb, + db: this.db, org: this.org, env: this.env, // attachPm: true, diff --git a/server/tests/alex/04_topups.ts b/server/tests/alex/04_topups.ts index 86483221f..5d4940cd3 100644 --- a/server/tests/alex/04_topups.ts +++ b/server/tests/alex/04_topups.ts @@ -26,7 +26,7 @@ describe(chalk.yellowBright("Top ups"), () => { name: "Alex Top Up Customer", email: "alex-top-up-customer@test.com", }, - sb: this.sb, + db: this.db, org: this.org, env: this.env, attachPm: true, @@ -132,7 +132,7 @@ describe(chalk.yellowBright("Testing o1 message top up"), () => { name: "Alex O1 Top Up Customer", email: "alex-o1-top-up-customer@test.com", }, - sb: this.sb, + db: this.db, org: this.org, env: this.env, attachPm: true, diff --git a/server/tests/alex/05_cancel.ts b/server/tests/alex/05_cancel.ts index ae201a094..cf6b2207b 100644 --- a/server/tests/alex/05_cancel.ts +++ b/server/tests/alex/05_cancel.ts @@ -40,7 +40,7 @@ describe(chalk.yellowBright("05_cancel"), () => { name: "Alex Cancel Customer", email: "alex-cancel-customer@test.com", }, - sb: this.sb, + db: this.db, org: this.org, env: this.env, attachPm: true, @@ -134,7 +134,7 @@ describe(chalk.yellowBright("05_cancel"), () => { customerId, org: this.org, env: this.env, - sb: this.sb, + db: this.db, }); testClockId = testClockId_; customer = customer_; diff --git a/server/tests/alex/06_switch.ts b/server/tests/alex/06_switch.ts index 3db05c4d2..b17d9f247 100644 --- a/server/tests/alex/06_switch.ts +++ b/server/tests/alex/06_switch.ts @@ -14,7 +14,7 @@ import { timeout } from "tests/utils/genUtils.js"; describe( chalk.yellowBright( - "06_switch: Testing upgrades / downgrades from pro <-> premium" + "06_switch: Testing upgrades / downgrades from pro <-> premium", ), () => { let customerId = "alex-upgrade-downgrade-customer"; @@ -29,7 +29,7 @@ describe( }); const { testClockId: newTestClockId } = await initCustomerWithTestClock({ customerId, - sb: this.sb, + db: this.db, org: this.org, env: this.env, fingerprint, @@ -87,7 +87,7 @@ describe( }); let proProduct = cusRes.products.find( - (p: any) => p.id === alexProducts.pro.id + (p: any) => p.id === alexProducts.pro.id, ); expect(proProduct.status).to.equal(CusProductStatus.Scheduled); @@ -151,7 +151,7 @@ describe( expect(lastInvoice.total).to.be.lessThan(proratedAmount * 1.1); }); }); - } + }, ); // Also, downgrade and cancel pro @@ -168,7 +168,7 @@ describe(chalk.yellowBright("06_switch: Testing fingerprint"), () => { fingerprint, }, attachPm: true, - sb: this.sb, + db: this.db, org: this.org, env: this.env, }); diff --git a/server/tests/alex/init.ts b/server/tests/alex/init.ts index 9f0161f73..9a1bd22b2 100644 --- a/server/tests/alex/init.ts +++ b/server/tests/alex/init.ts @@ -17,6 +17,8 @@ import { } from "../utils/init.js"; import { createSupabaseClient } from "@/external/supabaseUtils.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; +import { initDrizzle } from "@/db/initDrizzle.js"; +import { FeatureService } from "@/internal/features/FeatureService.js"; export const alexFeatures = { chatMessage: initFeature({ @@ -421,16 +423,19 @@ before(async function () { try { this.env = AppEnv.Sandbox; this.sb = createSupabaseClient(); + const { db, client } = initDrizzle(); + this.db = db; + this.client = client; this.org = await OrgService.getBySlug({ - sb: this.sb, + db: this.db, slug: orgSlug, }); - let { data: dbFeatures, error } = await this.sb - .from("features") - .select("*") - .eq("org_id", this.org.id) - .eq("env", this.env); + let dbFeatures = await FeatureService.list({ + db: this.db, + orgId: this.org.id, + env: this.env, + }); for (const featureId in alexFeatures) { let feature = alexFeatures[featureId as keyof typeof alexFeatures]; @@ -452,3 +457,7 @@ before(async function () { console.error(error); } }); + +after(async function () { + await this.client.end(); +}); diff --git a/server/tests/attach/01_multi_product1.ts b/server/tests/attach/01_multi_product1.ts index e4376d6d0..513815de9 100644 --- a/server/tests/attach/01_multi_product1.ts +++ b/server/tests/attach/01_multi_product1.ts @@ -15,54 +15,59 @@ FLOW: 2. Upgrade pro group 1 -> premium group 1 3. Upgrade pro group 2 -> premium group 2 */ -describe(chalk.yellowBright("01_multi_product1: Testing multi product attach, and upgrade"), () => { - let customerId = "multi-product-attach-upgrade"; - before(async function () { - this.customer = await initCustomer({ - sb: this.sb, - org: this.org, - customer_data: { - id: customerId, - name: customerId, - email: "multi-product-attach-upgrade@example.com", - }, - env: this.env, - attachPm: true, - }); - }); - - it("should attach pro group 1 and pro group 2", async function () { - await AutumnCli.attach({ - customerId: customerId, - productIds: [attachProducts.proGroup1.id, attachProducts.proGroup2.id], +describe( + chalk.yellowBright( + "01_multi_product1: Testing multi product attach, and upgrade", + ), + () => { + let customerId = "multi-product-attach-upgrade"; + before(async function () { + this.customer = await initCustomer({ + db: this.db, + org: this.org, + customer_data: { + id: customerId, + name: customerId, + email: "multi-product-attach-upgrade@example.com", + }, + env: this.env, + attachPm: true, + }); }); - let cusRes = await AutumnCli.getCustomer(customerId); - compareMainProduct({ sent: attachProducts.proGroup1, cusRes }); - compareMainProduct({ sent: attachProducts.proGroup2, cusRes }); - }); + it("should attach pro group 1 and pro group 2", async function () { + await AutumnCli.attach({ + customerId: customerId, + productIds: [attachProducts.proGroup1.id, attachProducts.proGroup2.id], + }); - it("should upgrade to premium group 1", async function () { - await AutumnCli.attach({ - customerId: customerId, - productId: attachProducts.premiumGroup1.id, + let cusRes = await AutumnCli.getCustomer(customerId); + compareMainProduct({ sent: attachProducts.proGroup1, cusRes }); + compareMainProduct({ sent: attachProducts.proGroup2, cusRes }); }); - // 1. Compare main product - const cusRes = await AutumnCli.getCustomer(customerId); - compareMainProduct({ sent: attachProducts.premiumGroup1, cusRes }); + it("should upgrade to premium group 1", async function () { + await AutumnCli.attach({ + customerId: customerId, + productId: attachProducts.premiumGroup1.id, + }); - // 2. Check latest invoice - }); + // 1. Compare main product + const cusRes = await AutumnCli.getCustomer(customerId); + compareMainProduct({ sent: attachProducts.premiumGroup1, cusRes }); - it("should upgrade to premium group 2", async function () { - await AutumnCli.attach({ - customerId: customerId, - productId: attachProducts.premiumGroup2.id, + // 2. Check latest invoice }); - // 1. Compare main product - const cusRes = await AutumnCli.getCustomer(customerId); - compareMainProduct({ sent: attachProducts.premiumGroup2, cusRes }); - }); -}); + it("should upgrade to premium group 2", async function () { + await AutumnCli.attach({ + customerId: customerId, + productId: attachProducts.premiumGroup2.id, + }); + + // 1. Compare main product + const cusRes = await AutumnCli.getCustomer(customerId); + compareMainProduct({ sent: attachProducts.premiumGroup2, cusRes }); + }); + }, +); diff --git a/server/tests/attach/01_multi_product3.ts b/server/tests/attach/01_multi_product3.ts index 3c73b3ee5..8b1a08f1e 100644 --- a/server/tests/attach/01_multi_product3.ts +++ b/server/tests/attach/01_multi_product3.ts @@ -29,7 +29,7 @@ describe("Multi Product 3: premium1->starter1, premium2->starter2, then premium2 let stripeCli: Stripe; before(async function () { customer = await initCustomer({ - sb: this.sb, + db: this.db, org: this.org, customer_data: { id: customerId, @@ -119,7 +119,6 @@ describe("Multi Product 3: premium1->starter1, premium2->starter2, then premium2 attachProducts.starterGroup1.id, attachProducts.starterGroup2.id, ], - sb: this.sb, org: this.org, env: this.env, }); @@ -159,7 +158,6 @@ describe("Multi Product 3: premium1->starter1, premium2->starter2, then premium2 db: this.db, scheduleId: starterGroup2?.scheduled_ids![0], productIds: [attachProducts.starterGroup2.id], - sb: this.sb, org: this.org, env: this.env, }); diff --git a/server/tests/attach/01_multi_product4.ts b/server/tests/attach/01_multi_product4.ts index bd35ce66a..ff46cbf1b 100644 --- a/server/tests/attach/01_multi_product4.ts +++ b/server/tests/attach/01_multi_product4.ts @@ -27,7 +27,7 @@ describe( before(async function () { customer = await initCustomer({ - sb: this.sb, + db: this.db, org: this.org, env: this.env, customer_data: { diff --git a/server/tests/attach/attach2.ts b/server/tests/attach/attach2.ts index cc6306c21..c0ea56060 100644 --- a/server/tests/attach/attach2.ts +++ b/server/tests/attach/attach2.ts @@ -8,7 +8,7 @@ import { completeCheckoutForm } from "tests/utils/stripeUtils.js"; import { compareMainProduct } from "tests/utils/compare.js"; describe(`${chalk.yellowBright( - "attach2: Testing monthly with one time prepaid, quantity = 0" + "attach2: Testing monthly with one time prepaid, quantity = 0", )}`, () => { let customerId = "attach2"; @@ -25,7 +25,7 @@ describe(`${chalk.yellowBright( before(async function () { await initCustomer({ customerId, - sb: this.sb, + db: this.db, org: this.org, env: this.env, }); diff --git a/server/tests/attach/attach3.ts b/server/tests/attach/attach3.ts index 6470cedcd..d96d67d41 100644 --- a/server/tests/attach/attach3.ts +++ b/server/tests/attach/attach3.ts @@ -31,7 +31,7 @@ describe(`${chalk.yellowBright("attach3: Multi attach, all one off")}`, () => { before(async function () { await initCustomer({ customerId, - sb: this.sb, + db: this.db, org: this.org, env: this.env, }); @@ -76,7 +76,7 @@ describe(`${chalk.yellowBright("attach3: Multi attach, all one off")}`, () => { const metered2Amount = metered2Tiers[0].amount; let numBillingUnits = new Decimal(options[0].quantity).div( - oneTimeProducts.oneTimeMetered2.prices[0].config.billing_units + oneTimeProducts.oneTimeMetered2.prices[0].config.billing_units, ); // console.log("Num billing units: ", numBillingUnits); @@ -85,7 +85,7 @@ describe(`${chalk.yellowBright("attach3: Multi attach, all one off")}`, () => { new Decimal(metered2Amount) .mul(numBillingUnits) .add(metered1Amount) - .toNumber() + .toNumber(), ); }); }); diff --git a/server/tests/basic/01_product.ts b/server/tests/basic/01_product.ts index b77e25d3b..d130d59f7 100644 --- a/server/tests/basic/01_product.ts +++ b/server/tests/basic/01_product.ts @@ -15,13 +15,13 @@ const monthlyQuantity = 2; // UNCOMMENT FROM HERE describe(`${chalk.yellowBright( - "01_product: Testing attach -- free, pro & one-time / monthly add on" + "01_product: Testing attach -- free, pro & one-time / monthly add on", )}`, () => { let customerId = "attach1"; before(async function () { await initCustomer({ customerId, - sb: this.sb, + db: this.db, org: this.org, env: this.env, }); @@ -44,18 +44,18 @@ describe(`${chalk.yellowBright( const entitled: any = await AutumnCli.entitled( customerId, - features.metered1.id + features.metered1.id, ); const metered1Balance = entitled!.balances.find( - (balance: any) => balance.feature_id === features.metered1.id + (balance: any) => balance.feature_id === features.metered1.id, ); try { expect(entitled!.allowed).to.be.true; expect(metered1Balance).to.exist; expect(metered1Balance!.balance).to.equal( - expectedEntitlement.allowance + expectedEntitlement.allowance, ); expect(metered1Balance!.unlimited).to.not.exist; } catch (error) { @@ -72,7 +72,7 @@ describe(`${chalk.yellowBright( it("GET /entitled -- boolean1", async function () { const entitled = await AutumnCli.entitled( customerId, - features.boolean1.id + features.boolean1.id, ); expect(entitled!.allowed).to.be.false; @@ -111,11 +111,11 @@ describe(`${chalk.yellowBright( const res: any = await AutumnCli.entitled( customerId, - entitlement.feature_id! + entitlement.feature_id!, ); const entBalance = res!.balances.find( - (b: any) => b.feature_id === entitlement.feature_id + (b: any) => b.feature_id === entitlement.feature_id, ); try { @@ -182,7 +182,7 @@ describe(`${chalk.yellowBright( (e: any) => e.feature_id === features.metered1.id && e.interval == - products.oneTimeAddOnMetered1.entitlements.metered1.interval + products.oneTimeAddOnMetered1.entitlements.metered1.interval, ); const expectedAmt = @@ -201,7 +201,7 @@ describe(`${chalk.yellowBright( console.log("GET customer, balances failed"); console.log( "Add on entitlement:", - products.oneTimeAddOnMetered1.entitlements.metered1 + products.oneTimeAddOnMetered1.entitlements.metered1, ); console.log("Customer entitlements:", cusRes.entitlements); @@ -214,7 +214,7 @@ describe(`${chalk.yellowBright( it("GET /entitled -- checking entitled for metered1", async function () { const res: any = await AutumnCli.entitled( customerId, - features.metered1.id + features.metered1.id, ); expect(res!.allowed).to.be.true; @@ -223,7 +223,7 @@ describe(`${chalk.yellowBright( const proMetered1Amt = products.pro.entitlements.metered1.allowance; const addOnBalance = res!.balances.find( - (b: any) => b.feature_id === features.metered1.id + (b: any) => b.feature_id === features.metered1.id, ); expect(res!.allowed).to.be.true; @@ -231,7 +231,7 @@ describe(`${chalk.yellowBright( proMetered1Amt! + (oneTimeOverrideQuantity || oneTimeQuantity) * oneTimeBillingUnits * - oneTimePurchaseCount + oneTimePurchaseCount, ); }); }); @@ -266,18 +266,18 @@ describe(`${chalk.yellowBright( (e: any) => e.feature_id === features.metered1.id && e.interval == - products.monthlyAddOnMetered1.entitlements.metered1.interval + products.monthlyAddOnMetered1.entitlements.metered1.interval, ); try { assert.equal( monthlyMetered1Balance!.balance, - proMetered1! + monthlyQuantity * monthlyBillingUnits + proMetered1! + monthlyQuantity * monthlyBillingUnits, ); assert.equal(cusRes.add_ons.length, 2); const monthlyAddOnId = cusRes.add_ons.find( - (a: any) => a.id === products.monthlyAddOnMetered1.id + (a: any) => a.id === products.monthlyAddOnMetered1.id, ); assert.exists(monthlyAddOnId); @@ -288,7 +288,7 @@ describe(`${chalk.yellowBright( console.log("GET customer, balances failed"); console.log( "Add on entitlement:", - products.monthlyAddOnMetered1.entitlements.metered1 + products.monthlyAddOnMetered1.entitlements.metered1, ); console.log("Customer entitlements:", cusRes.entitlements); @@ -301,11 +301,11 @@ describe(`${chalk.yellowBright( it("GET /entitled -- checking entitlements (monthly add on)", async function () { const res: any = await AutumnCli.entitled( customerId, - features.metered1.id + features.metered1.id, ); const metered1Balance = res!.balances.find( - (b: any) => b.feature_id === features.metered1.id + (b: any) => b.feature_id === features.metered1.id, ); const proMetered1Amt = products.pro.entitlements.metered1.allowance; @@ -318,7 +318,7 @@ describe(`${chalk.yellowBright( try { expect(metered1Balance!.balance).to.equal( - proMetered1Amt! + monthlyAddOnMetered1Amt + oneTimeAddOnMetered1Amt + proMetered1Amt! + monthlyAddOnMetered1Amt + oneTimeAddOnMetered1Amt, ); } catch (error) { console.group(); diff --git a/server/tests/basic/03_cancel.ts b/server/tests/basic/03_cancel.ts index 119c12c48..16e37857c 100644 --- a/server/tests/basic/03_cancel.ts +++ b/server/tests/basic/03_cancel.ts @@ -19,7 +19,7 @@ import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; import { addDays, addMonths } from "date-fns"; describe(`${chalk.yellowBright( - "03_cancel: Testing cancel (at period end and now)" + "03_cancel: Testing cancel (at period end and now)", )}`, () => { const customerId = "cancelCustomer"; @@ -32,7 +32,7 @@ describe(`${chalk.yellowBright( name: "Test Customer", email: "test@test.com", }, - sb: this.sb, + db: this.db, org: this.org, env: this.env, }); @@ -61,7 +61,7 @@ describe(`${chalk.yellowBright( const cusRes: any = await AutumnCli.getCustomer(customerId); const proProduct = cusRes.products.find( - (p: any) => p.id === products.pro.id + (p: any) => p.id === products.pro.id, ); for (const subId of proProduct.subscription_ids) { @@ -84,7 +84,7 @@ describe(`${chalk.yellowBright( }); const proProduct = cusRes.products.find( - (p: any) => p.id === products.pro.id + (p: any) => p.id === products.pro.id, ); expect(proProduct.canceled_at).to.not.equal(null); expect(proProduct.status).to.equal(CusProductStatus.Active); @@ -98,7 +98,7 @@ describe(`${chalk.yellowBright( const cusRes: any = await AutumnCli.getCustomer(customerId); const proProduct = cusRes.products.find( - (p: any) => p.id === products.pro.id + (p: any) => p.id === products.pro.id, ); for (const subId of proProduct.subscription_ids) { @@ -133,7 +133,7 @@ describe(`${chalk.yellowBright( }); describe(`${chalk.yellowBright( - "03_cancel: Testing subscription past_due" + "03_cancel: Testing subscription past_due", )}`, () => { const customerId = "03_cancel_past_due"; @@ -154,7 +154,7 @@ describe(`${chalk.yellowBright( name: "Test Customer", email: "test@test.com", }, - sb: this.sb, + db: this.db, org: this.org, env: this.env, attachPm: true, diff --git a/server/tests/basic/04_entitled.ts b/server/tests/basic/04_entitled.ts index 86a8f1718..ce0096a8b 100644 --- a/server/tests/basic/04_entitled.ts +++ b/server/tests/basic/04_entitled.ts @@ -29,7 +29,7 @@ const checkEntitledOnProduct = async ({ AutumnCli.sendEvent({ customerId: customerId, eventName: features.metered1.eventName, - }) + }), ); } @@ -41,7 +41,7 @@ const checkEntitledOnProduct = async ({ const { allowed, balanceObj }: any = await AutumnCli.entitled( customerId, features.metered1.id, - true + true, ); try { @@ -69,7 +69,7 @@ const checkEntitledOnProduct = async ({ AutumnCli.sendEvent({ customerId: customerId, eventName: features.metered1.eventName, - }) + }), ); } await Promise.all(batchUpdates2); @@ -101,7 +101,7 @@ const checkEntitledOnProduct = async ({ // TODO: Add test case for unlimited feature describe(`${chalk.yellowBright( - "04_entitled: Testing /events and /entitled, for pro, one time top up" + "04_entitled: Testing /events and /entitled, for pro, one time top up", )}`, () => { const customerId = "entitledCustomer"; @@ -117,7 +117,7 @@ describe(`${chalk.yellowBright( name: customerId, email: `test@test.com`, }, - sb: this.sb, + db: this.db, org: this.org, env: this.env, attachPm: true, @@ -175,7 +175,7 @@ describe(`${chalk.yellowBright( }); describe(`${chalk.yellowBright( - "04_entitled: Testing /entitled & /events, for pro with overage" + "04_entitled: Testing /entitled & /events, for pro with overage", )}`, () => { const customerId = "entitledCustomerUsageBased"; @@ -186,7 +186,7 @@ describe(`${chalk.yellowBright( name: customerId, email: "test@test.com", }, - sb: this.sb, + db: this.db, org: this.org, env: this.env, attachPm: true, @@ -215,7 +215,7 @@ describe(`${chalk.yellowBright( const { allowed, balanceObj }: any = await AutumnCli.entitled( customerId, features.metered1.id, - true + true, ); expect(allowed).to.be.true; @@ -228,7 +228,7 @@ describe(`${chalk.yellowBright( AutumnCli.sendEvent({ customerId: customerId, eventName: features.metered1.eventName, - }) + }), ); } diff --git a/server/tests/basic/05_trial.ts b/server/tests/basic/05_trial.ts index 773f78be0..fedbea6d3 100644 --- a/server/tests/basic/05_trial.ts +++ b/server/tests/basic/05_trial.ts @@ -46,7 +46,7 @@ describe(`${chalk.yellowBright("05_trial: Testing free trials")}`, () => { email: "test@test.com", fingerprint: "fp1", }, - sb: this.sb, + db: this.db, org: this.org, env: this.env, }); @@ -123,7 +123,7 @@ describe(`${chalk.yellowBright("05_trial: Testing free trials")}`, () => { try { assert.equal( invoices[0].amount, - products.proWithTrial.prices[0].amount + products.proWithTrial.prices[0].amount, ); } catch (error) { console.group(); @@ -131,7 +131,7 @@ describe(`${chalk.yellowBright("05_trial: Testing free trials")}`, () => { console.log("GET customer, balances failed"); console.log( "Expected invoice amount:", - products.proWithTrial.prices[0].amount + products.proWithTrial.prices[0].amount, ); console.log("Customer invoices received:", invoices); console.groupEnd(); @@ -147,7 +147,7 @@ describe(`${chalk.yellowBright("05_trial: Testing free trials")}`, () => { email: "test2@test.com", fingerprint: "fp1", }, - sb: this.sb, + db: this.db, org: this.org, env: this.env, attachPm: true, @@ -174,7 +174,7 @@ describe(`${chalk.yellowBright("05_trial: Testing free trials")}`, () => { try { assert.equal( invoices[0].amount, - products.proWithTrial.prices[0].amount + products.proWithTrial.prices[0].amount, ); } catch (error) { console.group(); @@ -182,7 +182,7 @@ describe(`${chalk.yellowBright("05_trial: Testing free trials")}`, () => { console.log("GET customer, balances failed"); console.log( "Expected invoice amount:", - products.proWithTrial.prices[0].amount + products.proWithTrial.prices[0].amount, ); console.log("Customer invoices received:", invoices); console.groupEnd(); diff --git a/server/tests/basic/06_upgrade.ts b/server/tests/basic/06_upgrade.ts index 272e0d81b..f9bbc2e9b 100644 --- a/server/tests/basic/06_upgrade.ts +++ b/server/tests/basic/06_upgrade.ts @@ -29,7 +29,7 @@ describe(`${chalk.yellowBright("06_upgrade: Testing upgrades")}`, () => { name: "Test Customer", email: "test@test.com", }, - sb: this.sb, + db: this.db, org: this.org, env: this.env, attachPm: true, @@ -69,7 +69,7 @@ describe(`${chalk.yellowBright("06_upgrade: Testing upgrades")}`, () => { } catch (error: any) { assert.equal( error.message, - "Either payment method not found, or force_checkout is true: unable to perform upgrade / downgrade" + "Either payment method not found, or force_checkout is true: unable to perform upgrade / downgrade", ); assert.equal(error.code, "invalid_request"); } @@ -98,13 +98,13 @@ describe(`${chalk.yellowBright("06_upgrade: Testing upgrades")}`, () => { try { assert.equal( error.message, - "Either payment method not found, or force_checkout is true: unable to perform upgrade / downgrade" + "Either payment method not found, or force_checkout is true: unable to perform upgrade / downgrade", ); assert.equal(error.code, "invalid_request"); } catch (error) { console.group(); console.log( - "Expected recase error for force checkout / no payment method" + "Expected recase error for force checkout / no payment method", ); console.log("Got:", error); console.groupEnd(); @@ -117,7 +117,7 @@ describe(`${chalk.yellowBright("06_upgrade: Testing upgrades")}`, () => { it("should attach successful payment method", async function () { this.timeout(30000); await attachPmToCus({ - sb: this.sb, + db: this.db, customer: customer, org: this.org, env: this.env, @@ -145,7 +145,7 @@ describe(`${chalk.yellowBright("06_upgrade: Testing upgrades")}`, () => { }); describe(`${chalk.yellowBright( - "06_upgrade: Testing upgrade (paid to trial)" + "06_upgrade: Testing upgrade (paid to trial)", )}`, () => { const customerId = "paid_to_trial"; let testClockId: string; @@ -167,7 +167,7 @@ describe(`${chalk.yellowBright( name: "Paid to trial customer", email: "paid@trial.com", }, - sb: this.sb, + db: this.db, org: this.org, env: this.env, attachPm: true, @@ -198,7 +198,7 @@ describe(`${chalk.yellowBright( }); describe(`${chalk.yellowBright( - "06_upgrade: Testing upgrade (trial to paid)" + "06_upgrade: Testing upgrade (trial to paid)", )}`, () => { const customerId = "trial_to_paid"; let testClockId: string; @@ -219,7 +219,7 @@ describe(`${chalk.yellowBright( name: "Trial to paid customer", email: "trial@paid.com", }, - sb: this.sb, + db: this.db, org: this.org, env: this.env, attachPm: true, @@ -256,7 +256,7 @@ describe(`${chalk.yellowBright( await timeout(10000); console.log( - ` ${chalk.greenBright("Advanced 3 days and attached premium")}` + ` ${chalk.greenBright("Advanced 3 days and attached premium")}`, ); }); @@ -304,7 +304,7 @@ describe(`${chalk.yellowBright("Testing upgrade (trial to trial)")}`, () => { name: "Trial to trial customer", email: "trial@trial.com", }, - sb: this.sb, + db: this.db, org: this.org, env: this.env, attachPm: true, @@ -342,7 +342,7 @@ describe(`${chalk.yellowBright("Testing upgrade (trial to trial)")}`, () => { }); console.log( - ` ${chalk.greenBright("Advanced 3 days and attached premium")}` + ` ${chalk.greenBright("Advanced 3 days and attached premium")}`, ); }); diff --git a/server/tests/basic/07_downgrade.ts b/server/tests/basic/07_downgrade.ts index e57081209..000f04372 100644 --- a/server/tests/basic/07_downgrade.ts +++ b/server/tests/basic/07_downgrade.ts @@ -16,7 +16,7 @@ import { setupBefore } from "tests/before.js"; export const getCusProduct = async ( sb: SupabaseClient, internalCustomerId: string, - productId: string + productId: string, ) => { const { data, error } = await sb .from("customer_products") @@ -33,7 +33,7 @@ export const getCusProduct = async ( }; describe(`${chalk.yellowBright( - "07_downgrade: testing downgrade (paid to paid)" + "07_downgrade: testing downgrade (paid to paid)", )}`, () => { let customer: Customer; let customerId = "downgrade"; @@ -50,7 +50,7 @@ describe(`${chalk.yellowBright( customer = await initCustomer({ customerId, - sb: this.sb, + db: this.db, org: this.org, env: this.env, attachPm: true, @@ -93,7 +93,7 @@ describe(`${chalk.yellowBright( const resPro = resProducts.find( (p: any) => - p.id === products.pro.id && p.status === CusProductStatus.Scheduled + p.id === products.pro.id && p.status === CusProductStatus.Scheduled, ); assert.isNotNull(resPro); }); @@ -109,7 +109,7 @@ describe(`${chalk.yellowBright( const res = await AutumnCli.getCustomer(customerId); const resPro = res.products.find( (p: any) => - p.id === products.pro.id && p.status === CusProductStatus.Scheduled + p.id === products.pro.id && p.status === CusProductStatus.Scheduled, ); assert.isUndefined(resPro); @@ -167,7 +167,7 @@ describe(`${chalk.yellowBright("07_downgrade: testing expire button")}`, () => { customerId, org: this.org, env: this.env, - sb: this.sb, + db: this.db, }); customer = customer_; @@ -185,7 +185,7 @@ describe(`${chalk.yellowBright("07_downgrade: testing expire button")}`, () => { const customerProduct = await getCusProduct( this.sb, customer.internal_id, - products.premium.id + products.premium.id, ); await AutumnCli.expire(customerProduct.id); @@ -223,7 +223,7 @@ describe(`${chalk.yellowBright("07_downgrade: testing expire button")}`, () => { const customerProduct = await getCusProduct( this.sb, customer.internal_id, - products.pro.id + products.pro.id, ); await AutumnCli.expire(customerProduct.id); await timeout(5000); @@ -247,11 +247,11 @@ describe(`${chalk.yellowBright("07_downgrade: testing expire button")}`, () => { const premiumCusProduct = await getCusProduct( this.sb, customer.internal_id, - products.premium.id + products.premium.id, ); const stripeSub = await stripeCli.subscriptions.retrieve( - premiumCusProduct.processor.subscription_id + premiumCusProduct.processor.subscription_id, ); // Check that canceled is null diff --git a/server/tests/basic/08_pkey.ts b/server/tests/basic/08_pkey.ts index 03cc3f877..7854520f3 100644 --- a/server/tests/basic/08_pkey.ts +++ b/server/tests/basic/08_pkey.ts @@ -25,7 +25,7 @@ describe(`${chalk.yellowBright("Testing pkey")}`, () => { email: "test@test.com", fingerprint: "fp1", }, - sb: this.sb, + db: this.db, org: this.org, env: this.env, attachPm: true, @@ -124,11 +124,11 @@ describe(`${chalk.yellowBright("Testing pkey")}`, () => { assert.equal(data.allowed, true); const metered1Balance = data.balances.find( - (b: any) => b.feature_id === features.metered1.id + (b: any) => b.feature_id === features.metered1.id, ); assert.equal( metered1Balance.balance, - products.pro.entitlements.metered1.allowance + products.pro.entitlements.metered1.allowance, ); }); @@ -141,11 +141,11 @@ describe(`${chalk.yellowBright("Testing pkey")}`, () => { assert.equal(data.allowed, true); const metered1Balance = data.balances.find( - (b: any) => b.feature_id === features.metered1.id + (b: any) => b.feature_id === features.metered1.id, ); assert.equal( metered1Balance.balance, - products.pro.entitlements.metered1.allowance + products.pro.entitlements.metered1.allowance, ); }); diff --git a/server/tests/basic/entities/entities1.ts b/server/tests/basic/entities/entities1.ts index 04fa752a2..9589156d4 100644 --- a/server/tests/basic/entities/entities1.ts +++ b/server/tests/basic/entities/entities1.ts @@ -119,7 +119,7 @@ describe(`${chalk.yellowBright("entities1: Testing entities")}`, () => { const { testClockId: testClockId1 } = await initCustomerWithTestClock({ customerId, - sb: this.sb, + db: this.db, org: this.org, env: this.env, }); diff --git a/server/tests/basic/entities/entities2.ts b/server/tests/basic/entities/entities2.ts index 59074e6c4..91482bd06 100644 --- a/server/tests/basic/entities/entities2.ts +++ b/server/tests/basic/entities/entities2.ts @@ -31,6 +31,7 @@ import { constructFeaturePriceItem, } from "@/internal/products/product-items/productItemUtils.js"; import { createProduct } from "tests/utils/productUtils.js"; +import { OrgService } from "@/internal/orgs/OrgService.js"; // UNCOMMENT FROM HERE let entity2Pro = { @@ -76,7 +77,7 @@ describe(`${chalk.yellowBright( const { testClockId: testClockId1 } = await initCustomerWithTestClock({ customerId, - sb: this.sb, + db: this.db, org: this.org, env: this.env, }); @@ -88,21 +89,19 @@ describe(`${chalk.yellowBright( testClockId = testClockId1; - // await this.sb - // .from("organizations") - // .update({ - // config: { - // ...this.org.config, - // prorate_unused: true, - // }, - // }) - // .eq("id", this.org.id); + await OrgService.update({ + db: this.db, + orgId: this.org.id, + updates: { + config: { ...this.org.config, prorate_unused: true }, + }, + }); - // await CacheManager.invalidate({ - // action: CacheType.SecretKey, - // value: hashApiKey(process.env.UNIT_TEST_AUTUMN_SECRET_KEY!), - // }); - // await CacheManager.disconnect(); + await CacheManager.invalidate({ + action: CacheType.SecretKey, + value: hashApiKey(process.env.UNIT_TEST_AUTUMN_SECRET_KEY!), + }); + await CacheManager.disconnect(); }); it("should create entity, then attach pro product", async function () { @@ -124,18 +123,17 @@ describe(`${chalk.yellowBright( }); after(async function () { - // await this.sb - // .from("organizations") - // .update({ - // config: { - // ...this.org.config, - // prorate_unused: true, - // }, - // }) - // .eq("id", this.org.id); - // void CacheManager.invalidate({ - // action: CacheType.SecretKey, - // value: hashApiKey(process.env.UNIT_TEST_AUTUMN_SECRET_KEY!), - // }); + await OrgService.update({ + db: this.db, + orgId: this.org.id, + updates: { + config: { ...this.org.config, prorate_unused: false }, + }, + }); + + void CacheManager.invalidate({ + action: CacheType.SecretKey, + value: hashApiKey(process.env.UNIT_TEST_AUTUMN_SECRET_KEY!), + }); }); }); diff --git a/server/tests/basic/multi-feature/multi_feature1.ts b/server/tests/basic/multi-feature/multi_feature1.ts index 3b6abe3df..2428f6797 100644 --- a/server/tests/basic/multi-feature/multi_feature1.ts +++ b/server/tests/basic/multi-feature/multi_feature1.ts @@ -17,6 +17,7 @@ import { getPrepaidCusEnt } from "tests/utils/cusProductUtils/cusEntSearchUtils. import { constructFeaturePriceItem } from "@/internal/products/product-items/productItemUtils.js"; import { SupabaseClient } from "@supabase/supabase-js"; import { timeout } from "@/utils/genUtils.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; // Scenario 1: prepaid + pay per use monthly -> prepaid + pay per use monthly let pro = { @@ -70,31 +71,31 @@ let premium = { export const getPrepaidAndUsageCusEnts = async ({ customerId, - sb, + db, orgId, env, featureId, }: { customerId: string; - sb: SupabaseClient; + db: DrizzleCli; orgId: string; env: AppEnv; featureId: string; }) => { let mainCusProduct = await getMainCusProduct({ customerId, - sb, + db, orgId, env, }); let prepaidCusEnt = getPrepaidCusEnt({ - cusProduct: mainCusProduct, + cusProduct: mainCusProduct!, featureId, }); let usageCusEnt = getUsageCusEnt({ - cusProduct: mainCusProduct, + cusProduct: mainCusProduct!, featureId, }); @@ -103,7 +104,7 @@ export const getPrepaidAndUsageCusEnts = async ({ // UNCOMMENT FROM HERE describe(`${chalk.yellowBright( - "multi-feature/multi_feature1: Testing prepaid + pay per use -> prepaid + pay per use" + "multi-feature/multi_feature1: Testing prepaid + pay per use -> prepaid + pay per use", )}`, () => { let autumn: Autumn; let customerId = "multiFeature1Customer"; @@ -127,7 +128,7 @@ describe(`${chalk.yellowBright( await initCustomer({ customerId, - sb: this.sb, + db: this.db, org: this.org, env: this.env, attachPm: true, @@ -155,14 +156,14 @@ describe(`${chalk.yellowBright( let { prepaidCusEnt, usageCusEnt } = await getPrepaidAndUsageCusEnts({ customerId, - sb: this.sb, + db: this.db, orgId: this.org.id, env: this.env, featureId: features.metered1.id, }); expect(prepaidCusEnt?.balance).to.equal( - prepaidQuantity + pro.items.prepaid.included_usage + prepaidQuantity + pro.items.prepaid.included_usage, ); expect(usageCusEnt?.balance).to.equal(pro.items.payPerUse.included_usage); @@ -183,7 +184,7 @@ describe(`${chalk.yellowBright( let { prepaidCusEnt, usageCusEnt } = await getPrepaidAndUsageCusEnts({ customerId, - sb: this.sb, + db: this.db, orgId: this.org.id, env: this.env, featureId: features.metered1.id, @@ -207,7 +208,7 @@ describe(`${chalk.yellowBright( let { usageCusEnt } = await getPrepaidAndUsageCusEnts({ customerId, - sb: this.sb, + db: this.db, orgId: this.org.id, env: this.env, featureId: features.metered1.id, @@ -224,7 +225,7 @@ describe(`${chalk.yellowBright( let { prepaidCusEnt, usageCusEnt: newUsageCusEnt } = await getPrepaidAndUsageCusEnts({ customerId, - sb: this.sb, + db: this.db, orgId: this.org.id, env: this.env, featureId: features.metered1.id, diff --git a/server/tests/basic/multi-feature/multi_feature2.ts b/server/tests/basic/multi-feature/multi_feature2.ts index aa207f6de..bf4bc8c61 100644 --- a/server/tests/basic/multi-feature/multi_feature2.ts +++ b/server/tests/basic/multi-feature/multi_feature2.ts @@ -24,6 +24,7 @@ import { } from "@/internal/products/product-items/productItemUtils.js"; import { SupabaseClient } from "@supabase/supabase-js"; import { timeout } from "@/utils/genUtils.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; // Scenario 1: prepaid + pay per use monthly -> prepaid + pay per use monthly let pro = { @@ -73,31 +74,31 @@ let premium = { export const getLifetimeAndUsageCusEnts = async ({ customerId, - sb, + db, orgId, env, featureId, }: { customerId: string; - sb: SupabaseClient; + db: DrizzleCli; orgId: string; env: AppEnv; featureId: string; }) => { let mainCusProduct = await getMainCusProduct({ customerId, - sb, + db, orgId, env, }); let lifetimeCusEnt = getLifetimeFreeCusEnt({ - cusProduct: mainCusProduct, + cusProduct: mainCusProduct!, featureId, }); let usageCusEnt = getUsageCusEnt({ - cusProduct: mainCusProduct, + cusProduct: mainCusProduct!, featureId, }); @@ -106,7 +107,7 @@ export const getLifetimeAndUsageCusEnts = async ({ // UNCOMMENT FROM HERE describe(`${chalk.yellowBright( - "multi-feature/multi_feature2: Testing lifetime + pay per use -> pay per use" + "multi-feature/multi_feature2: Testing lifetime + pay per use -> pay per use", )}`, () => { let autumn: Autumn; let customerId = "multiFeature2Customer"; @@ -118,7 +119,7 @@ describe(`${chalk.yellowBright( await initCustomer({ customerId, - sb: this.sb, + db: this.db, org: this.org, env: this.env, attachPm: true, @@ -145,7 +146,7 @@ describe(`${chalk.yellowBright( let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({ customerId, - sb: this.sb, + db: this.db, orgId: this.org.id, env: this.env, featureId: features.metered1.id, @@ -171,14 +172,14 @@ describe(`${chalk.yellowBright( let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({ customerId, - sb: this.sb, + db: this.db, orgId: this.org.id, env: this.env, featureId: features.metered1.id, }); expect(lifetimeCusEnt?.balance).to.equal( - (pro.items.lifetime.included_usage as number) - value + (pro.items.lifetime.included_usage as number) - value, ); expect(usageCusEnt?.balance).to.equal(pro.items.payPerUse.included_usage); }); @@ -201,7 +202,7 @@ describe(`${chalk.yellowBright( let { lifetimeCusEnt, usageCusEnt: newUsageCusEnt } = await getLifetimeAndUsageCusEnts({ customerId, - sb: this.sb, + db: this.db, orgId: this.org.id, env: this.env, featureId: features.metered1.id, @@ -218,7 +219,7 @@ describe(`${chalk.yellowBright( expect(lifetimeCusEnt).to.not.exist; expect(newUsageCusEnt?.balance).to.equal( - premium.items.payPerUse.included_usage + premium.items.payPerUse.included_usage, ); }); }); diff --git a/server/tests/basic/multi-feature/multi_feature3.ts b/server/tests/basic/multi-feature/multi_feature3.ts index 81195d101..89b4d3ffe 100644 --- a/server/tests/basic/multi-feature/multi_feature3.ts +++ b/server/tests/basic/multi-feature/multi_feature3.ts @@ -27,6 +27,7 @@ import { timeout } from "@/utils/genUtils.js"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; import { addDays, addMonths } from "date-fns"; +import { DrizzleCli } from "@/db/initDrizzle.js"; // Scenario 1: prepaid + pay per use monthly -> prepaid + pay per use monthly let pro = { @@ -51,31 +52,31 @@ let pro = { export const getLifetimeAndUsageCusEnts = async ({ customerId, - sb, + db, orgId, env, featureId, }: { customerId: string; - sb: SupabaseClient; + db: DrizzleCli; orgId: string; env: AppEnv; featureId: string; }) => { let mainCusProduct = await getMainCusProduct({ customerId, - sb, + db, orgId, env, }); let lifetimeCusEnt = getLifetimeFreeCusEnt({ - cusProduct: mainCusProduct, + cusProduct: mainCusProduct!, featureId, }); let usageCusEnt = getUsageCusEnt({ - cusProduct: mainCusProduct, + cusProduct: mainCusProduct!, featureId, }); @@ -98,7 +99,7 @@ describe(`${chalk.yellowBright( let { customer, testClockId: _testClockId } = await initCustomerWithTestClock({ customerId, - sb: this.sb, + db: this.db, org: this.org, env: this.env, }); @@ -121,7 +122,7 @@ describe(`${chalk.yellowBright( let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({ customerId, - sb: this.sb, + db: this.db, orgId: this.org.id, env: this.env, featureId: features.metered1.id, @@ -149,7 +150,7 @@ describe(`${chalk.yellowBright( let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({ customerId, - sb: this.sb, + db: this.db, orgId: this.org.id, env: this.env, featureId: features.metered1.id, @@ -169,7 +170,7 @@ describe(`${chalk.yellowBright( let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({ customerId, - sb: this.sb, + db: this.db, orgId: this.org.id, env: this.env, featureId: features.metered1.id, diff --git a/server/tests/basic/product/product1.ts b/server/tests/basic/product/product1.ts index 579a31807..b5e05f969 100644 --- a/server/tests/basic/product/product1.ts +++ b/server/tests/basic/product/product1.ts @@ -13,7 +13,7 @@ import { // UNCOMMENT FROM HERE describe(`${chalk.yellowBright( - "product1: Testing create and update product" + "product1: Testing create and update product", )}`, () => { let autumn: Autumn; @@ -104,13 +104,13 @@ describe(`${chalk.yellowBright( }); let metered1Ent = product.entitlements.find( - (ent: any) => ent.id === items[1].entitlement_id + (ent: any) => ent.id === items[1].entitlement_id, ); assert.equal(metered1Ent.allowance, 200); let price = product.prices.find( - (price: any) => price.id === items[3].price_id + (price: any) => price.id === items[3].price_id, ); assert.equal(price.config.amount, 20); @@ -119,7 +119,7 @@ describe(`${chalk.yellowBright( }); describe(`${chalk.yellowBright( - "product1: Testing attach and update product" + "product1: Testing attach and update product", )}`, () => { let autumn: Autumn; let customerId = "product-1-customer"; @@ -129,7 +129,7 @@ describe(`${chalk.yellowBright( await initCustomer({ customerId, - sb: this.sb, + db: this.db, org: this.org, env: this.env, attachPm: true, diff --git a/server/tests/basic/referrals/referrals1.ts b/server/tests/basic/referrals/referrals1.ts index d61a20f68..9a1ffe9d8 100644 --- a/server/tests/basic/referrals/referrals1.ts +++ b/server/tests/basic/referrals/referrals1.ts @@ -18,7 +18,7 @@ import { initCustomer } from "tests/utils/init.js"; // UNCOMMENT FROM HERE describe(`${chalk.yellowBright( - "referrals1: Testing referrals (on checkout)" + "referrals1: Testing referrals (on checkout)", )}`, () => { let mainCustomerId = "main-referral-1"; let alternateCustomerId = "alternate-referral-1"; @@ -39,7 +39,7 @@ describe(`${chalk.yellowBright( const { testClockId: testClockId1, customer } = await initCustomerWithTestClock({ customerId: mainCustomerId, - sb: this.sb, + db: this.db, org: this.org, env: this.env, fingerprint: "main-referral-1", @@ -57,11 +57,11 @@ describe(`${chalk.yellowBright( batchCreate.push( initCustomer({ customerId: redeemer, - sb: this.sb, + db: this.db, org: this.org, env: this.env, attachPm: true, - }) + }), ); } @@ -73,10 +73,10 @@ describe(`${chalk.yellowBright( email: "alternate-referral-1@example.com", fingerprint: "main-referral-1", }, - sb: this.sb, + db: this.db, org: this.org, env: this.env, - }) + }), ); await Promise.all(batchCreate); }); @@ -116,7 +116,7 @@ describe(`${chalk.yellowBright( code: referralCode.code, }); assert.fail( - "Own customer (same fingerprint) should not be able to redeem code" + "Own customer (same fingerprint) should not be able to redeem code", ); } catch (error) { assert.instanceOf(error, AutumnError); @@ -177,7 +177,7 @@ describe(`${chalk.yellowBright( // Check stripe customer let stripeCus = (await stripeCli.customers.retrieve( - mainCustomer.processor?.id + mainCustomer.processor?.id, )) as Stripe.Customer; assert.notEqual(stripeCus.discount, null); @@ -203,7 +203,7 @@ describe(`${chalk.yellowBright( it("customer should have discount for second purchase", async function () { // 2. Check that customer has another discount let stripeCus = (await stripeCli.customers.retrieve( - mainCustomer.processor?.id + mainCustomer.processor?.id, )) as Stripe.Customer; assert.notEqual(stripeCus.discount, null); diff --git a/server/tests/basic/referrals/referrals2.ts b/server/tests/basic/referrals/referrals2.ts index 174f6e5df..201a5f1eb 100644 --- a/server/tests/basic/referrals/referrals2.ts +++ b/server/tests/basic/referrals/referrals2.ts @@ -18,7 +18,7 @@ import { initCustomer } from "tests/utils/init.js"; // UNCOMMENT FROM HERE describe(`${chalk.yellowBright( - "referrals2: Testing referrals (immediate redemption)" + "referrals2: Testing referrals (immediate redemption)", )}`, () => { let mainCustomerId = "main-referral-2"; let redeemers = ["referral2-r1", "referral2-r2", "referral2-r3"]; @@ -38,7 +38,7 @@ describe(`${chalk.yellowBright( const { testClockId: testClockId1, customer } = await initCustomerWithTestClock({ customerId: mainCustomerId, - sb: this.sb, + db: this.db, org: this.org, env: this.env, }); @@ -50,11 +50,11 @@ describe(`${chalk.yellowBright( batchCreate.push( initCustomer({ customerId: redeemer, - sb: this.sb, + db: this.db, org: this.org, env: this.env, attachPm: true, - }) + }), ); } @@ -109,7 +109,7 @@ describe(`${chalk.yellowBright( // Check stripe customer let stripeCus = (await stripeCli.customers.retrieve( - mainCustomer.processor?.id + mainCustomer.processor?.id, )) as Stripe.Customer; assert.notEqual(stripeCus.discount, null); @@ -141,7 +141,7 @@ describe(`${chalk.yellowBright( it("customer should have discount for second purchase", async function () { // 2. Check that customer has another discount let stripeCus = (await stripeCli.customers.retrieve( - mainCustomer.processor?.id + mainCustomer.processor?.id, )) as Stripe.Customer; assert.notEqual(stripeCus.discount, null); diff --git a/server/tests/basic/referrals/referrals3.ts b/server/tests/basic/referrals/referrals3.ts index dfcdab4cf..7903f6a1a 100644 --- a/server/tests/basic/referrals/referrals3.ts +++ b/server/tests/basic/referrals/referrals3.ts @@ -17,7 +17,7 @@ import { compareProductEntitlements } from "tests/utils/compare.js"; // UNCOMMENT FROM HERE describe(`${chalk.yellowBright( - "referrals3: Testing free product referrals" + "referrals3: Testing free product referrals", )}`, () => { let mainCustomerId = "main-referral-3"; let redeemers = ["referral3-r1", "referral3-r2", "referral3-r3"]; @@ -37,7 +37,7 @@ describe(`${chalk.yellowBright( const { testClockId: testClockId1, customer } = await initCustomerWithTestClock({ customerId: mainCustomerId, - sb: this.sb, + db: this.db, org: this.org, env: this.env, fingerprint: "main-referral-3", @@ -55,11 +55,11 @@ describe(`${chalk.yellowBright( batchCreate.push( initCustomer({ customerId: redeemer, - sb: this.sb, + db: this.db, org: this.org, env: this.env, attachPm: true, - }) + }), ); } diff --git a/server/tests/basic/referrals/referrals4.ts b/server/tests/basic/referrals/referrals4.ts index d6a5b165d..c63410134 100644 --- a/server/tests/basic/referrals/referrals4.ts +++ b/server/tests/basic/referrals/referrals4.ts @@ -19,7 +19,7 @@ import { advanceTestClock } from "tests/utils/stripeUtils.js"; // UNCOMMENT FROM HERE describe(`${chalk.yellowBright( - "referrals4: Testing free product referrals with trial" + "referrals4: Testing free product referrals with trial", )}`, () => { let mainCustomerId = "main-referral-4"; // let redeemers = ["referral4-r1", "referral4-r2"]; @@ -41,7 +41,7 @@ describe(`${chalk.yellowBright( await initCustomer({ customerId: mainCustomerId, - sb: this.sb, + db: this.db, org: this.org, env: this.env, attachPm: true, @@ -55,7 +55,7 @@ describe(`${chalk.yellowBright( let { testClockId: testClockId1, customer } = await initCustomerWithTestClock({ customerId: redeemerId, - sb: this.sb, + db: this.db, org: this.org, env: this.env, }); diff --git a/server/tests/before.ts b/server/tests/before.ts index 9637aab8a..7928c0989 100644 --- a/server/tests/before.ts +++ b/server/tests/before.ts @@ -15,13 +15,16 @@ const DEFAULT_ENV = AppEnv.Sandbox; export const setupBefore = async (instance: any) => { const sb = createSupabaseClient(); - const org = await OrgService.getBySlug({ sb, slug: ORG_SLUG }); + const { db, client } = initDrizzle(); + + const org = await OrgService.getBySlug({ db, slug: ORG_SLUG }); + if (!org) { + throw new Error("Org not found"); + } const env = DEFAULT_ENV; const autumnSecretKey = process.env.UNIT_TEST_AUTUMN_SECRET_KEY!; const autumn = new Autumn(autumnSecretKey); - const { db, client } = initDrizzle(); - const autumnJs = new AutumnJS({ secretKey: autumnSecretKey, url: "http://localhost:8080/v1", diff --git a/server/tests/global.ts b/server/tests/global.ts index e4ece64e9..f5d236301 100644 --- a/server/tests/global.ts +++ b/server/tests/global.ts @@ -25,6 +25,7 @@ import { } from "./utils/init.js"; import { createSupabaseClient } from "@/external/supabaseUtils.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; +import { initDrizzle } from "@/db/initDrizzle.js"; export const features: Record = { boolean1: initFeature({ @@ -849,12 +850,17 @@ export const referralPrograms = { const ORG_SLUG = "unit-test-org"; const DEFAULT_ENV = AppEnv.Sandbox; + before(async function () { try { this.env = AppEnv.Sandbox; this.sb = createSupabaseClient(); + let { db, client } = initDrizzle(); + this.db = db; + this.client = client; + this.org = await OrgService.getBySlug({ - sb: this.sb, + db: this.db, slug: ORG_SLUG, }); @@ -890,26 +896,6 @@ before(async function () { } }); -// before(async function () { -// console.log("Running setup"); -// this.timeout(20000); - -// this.org = await clearOrg({ orgSlug: ORG_SLUG, env: DEFAULT_ENV }); -// this.env = DEFAULT_ENV; -// this.sb = createSupabaseClient(); -// this.stripeCli = createStripeCli({ -// org: this.org, -// env: this.env, -// }); - -// await setupOrg({ -// orgId: this.org.id, -// env: DEFAULT_ENV, -// features: { ...features, ...creditSystems } as any, -// products: { ...products, ...advanceProducts } as any, -// }); - -// this.customerId = "123"; - -// console.log("--------------------------------"); -// }); +after(async function () { + await this.client?.end(); +}); diff --git a/server/tests/utils/cusProductUtils/cusProductUtils.ts b/server/tests/utils/cusProductUtils/cusProductUtils.ts index 474cf2074..e0f2c1595 100644 --- a/server/tests/utils/cusProductUtils/cusProductUtils.ts +++ b/server/tests/utils/cusProductUtils/cusProductUtils.ts @@ -1,21 +1,22 @@ +import { DrizzleCli } from "@/db/initDrizzle.js"; import { Autumn } from "@/external/autumn/autumnCli.js"; import { CusService } from "@/internal/customers/CusService.js"; import { AppEnv, CusProductStatus, FullCusProduct } from "@autumn/shared"; import { SupabaseClient } from "@supabase/supabase-js"; export const getMainCusProduct = async ({ - sb, + db, customerId, orgId, env, }: { - sb: SupabaseClient; + db: DrizzleCli; customerId: string; orgId: string; env: AppEnv; }) => { - let customer = await CusService.getWithProducts({ - sb, + let customer = await CusService.getFull({ + db, idOrInternalId: customerId, orgId, env, diff --git a/server/tests/utils/init.ts b/server/tests/utils/init.ts index 99da27fe6..b767c71af 100644 --- a/server/tests/utils/init.ts +++ b/server/tests/utils/init.ts @@ -6,13 +6,11 @@ import { BillWhen, CouponDurationType, CreateFreeTrial, - DiscountType, EntInterval, Entitlement, Feature, FeatureType, FeatureUsageType, - FreeTrial, FreeTrialDuration, Organization, PriceType, @@ -25,7 +23,8 @@ import { import { getAxiosInstance } from "./setup.js"; import { SupabaseClient } from "@supabase/supabase-js"; import { attachPmToCus } from "@/external/stripe/stripeCusUtils.js"; -import { generateId, notNullish } from "@/utils/genUtils.js"; +import { generateId } from "@/utils/genUtils.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; export const keyToTitle = (key: string) => { return key @@ -295,7 +294,7 @@ export const initCustomer = async ({ customer_data, customerId, attachPm = false, - sb, + db, org, env, testClockId, @@ -308,7 +307,7 @@ export const initCustomer = async ({ }; customerId?: string; attachPm?: boolean; - sb: SupabaseClient; + db: DrizzleCli; org: Organization; env: AppEnv; testClockId?: string; @@ -344,7 +343,7 @@ export const initCustomer = async ({ customer: data.customer, org: org, env: env, - sb: sb, + db: db, testClockId: testClockId, }); } diff --git a/server/tests/utils/scheduleCheckUtils.ts b/server/tests/utils/scheduleCheckUtils.ts index ad26d5b3e..e9983479e 100644 --- a/server/tests/utils/scheduleCheckUtils.ts +++ b/server/tests/utils/scheduleCheckUtils.ts @@ -1,13 +1,12 @@ import { ProductService } from "@/internal/products/ProductService.js"; -import { AppEnv, FullProduct, Organization } from "@autumn/shared"; -import { SupabaseClient } from "@supabase/supabase-js"; +import { AppEnv, Organization } from "@autumn/shared"; 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, scheduleId, @@ -15,7 +14,6 @@ export const checkScheduleContainsProducts = async ({ productIds, }: { db: DrizzleCli; - sb: SupabaseClient; org: Organization; env: AppEnv; scheduleId?: string; @@ -55,14 +53,12 @@ export const checkScheduleContainsProducts = async ({ export const checkSubscriptionContainsProducts = async ({ db, - sb, org, env, subscriptionId, productIds, }: { db: DrizzleCli; - sb: SupabaseClient; org: Organization; env: AppEnv; subscriptionId: string; diff --git a/server/tests/utils/setup.ts b/server/tests/utils/setup.ts index 7bf18550a..a3b8991a0 100644 --- a/server/tests/utils/setup.ts +++ b/server/tests/utils/setup.ts @@ -5,7 +5,6 @@ import { Feature, FeatureType, FullProduct, - organizations, Price, PriceType, RewardProgram, @@ -27,6 +26,10 @@ import { CacheType } from "@/external/caching/cacheActions.js"; import { hashApiKey } from "@/internal/dev/api-keys/apiKeyUtils.js"; import { initDrizzle } from "@/db/initDrizzle.js"; import { eq } from "drizzle-orm"; +import { CusService } from "@/internal/customers/CusService.js"; +import { ProductService } from "@/internal/products/ProductService.js"; +import { RewardService } from "@/internal/rewards/RewardService.js"; +import { FeatureService } from "@/internal/features/FeatureService.js"; export const getAxiosInstance = ( apiKey: string = process.env.UNIT_TEST_AUTUMN_SECRET_KEY!, @@ -72,7 +75,8 @@ export const clearOrg = async ({ } const sb = createSupabaseClient(); - const org = await OrgService.getBySlug({ sb, slug: orgSlug }); + const { db, client } = initDrizzle(); + const org = await OrgService.getBySlug({ db, slug: orgSlug }); await Promise.all([ CacheManager.invalidate({ @@ -98,17 +102,7 @@ export const clearOrg = async ({ const orgId = org.id; // 1. Delete all customers - const { data: customers, error: customerError } = await sb - .from("customers") - .delete() - .eq("org_id", orgId) - .eq("env", env) - .select("*"); - - if (customerError) { - console.error("Error deleting customers:", customerError); - } - + await CusService.deleteByOrgId({ db, orgId, env }); console.log(" ✅ Deleted customers"); const stripeCli = createStripeCli({ org, env: env! }); @@ -141,15 +135,7 @@ export const clearOrg = async ({ console.log(" ✅ Deleted Stripe customers"); // 2. Delete all products - const { data: products, error: productError } = await sb - .from("products") - .delete() - .eq("org_id", orgId) - .eq("env", env) - .select("*, prices(*)"); - if (productError) { - console.error("Error deleting products:", productError); - } + await ProductService.deleteByOrgId({ db, orgId, env }); console.log(" ✅ Deleted products"); @@ -196,12 +182,7 @@ export const clearOrg = async ({ // Batch delete coupons const batchDeleteCoupons = []; - const { data: coupons, error: couponError } = await sb - .from("rewards") - .delete() - .eq("org_id", orgId) - .eq("env", env) - .select(); + await RewardService.deleteByOrgId({ db, orgId, env }); const stripeCoupons = await stripeCli.coupons.list({ limit: 100, @@ -213,17 +194,11 @@ export const clearOrg = async ({ await Promise.all(batchDeleteCoupons); console.log(" ✅ Deleted Stripe coupons"); - const { error: featureError } = await sb - .from("features") - .delete() - .eq("org_id", orgId) - .eq("env", env); - - if (featureError) { - console.error("Error deleting features:", featureError); - } + await FeatureService.deleteByOrgId({ db, orgId, env }); console.log(`✅ Cleared org ${orgSlug} (${env})`); + + await client.end(); return org; }; diff --git a/server/tests/utils/stripeUtils.ts b/server/tests/utils/stripeUtils.ts index 0346b5e7f..00f9f70b1 100644 --- a/server/tests/utils/stripeUtils.ts +++ b/server/tests/utils/stripeUtils.ts @@ -370,8 +370,6 @@ export const checkBillingMeterEventSummary = async ({ stripeMeterId: string; stripeCustomerId: string; }) => { - const sb = createSupabaseClient(); - let endTime = addMonths(startTime, 1); const event = await stripeCli.billing.meters.listEventSummaries( stripeMeterId, diff --git a/server/tests/utils/testInitUtils.ts b/server/tests/utils/testInitUtils.ts index 874a55cc9..c8bc5b8c3 100644 --- a/server/tests/utils/testInitUtils.ts +++ b/server/tests/utils/testInitUtils.ts @@ -2,18 +2,19 @@ import { createStripeCli } from "@/external/stripe/utils.js"; import { AppEnv, Organization } from "@autumn/shared"; import { SupabaseClient } from "@supabase/supabase-js"; import { initCustomer } from "./init.js"; +import { DrizzleCli } from "@/db/initDrizzle.js"; export const initCustomerWithTestClock = async ({ customerId, org, env, - sb, + db, fingerprint, }: { customerId: string; org: Organization; env: AppEnv; - sb: SupabaseClient; + db: DrizzleCli; fingerprint?: string; }) => { const stripeCli = createStripeCli({ org: org, env: env }); @@ -28,7 +29,7 @@ export const initCustomerWithTestClock = async ({ email: "test@test.com", fingerprint, }, - sb: sb, + db: db, org: org, env: env, testClockId: testClock.id, diff --git a/shared/db/schema.ts b/shared/db/schema.ts index e73da20c9..b99a0ce93 100644 --- a/shared/db/schema.ts +++ b/shared/db/schema.ts @@ -22,6 +22,17 @@ import { customerEntitlements } from "../models/cusProductModels/cusEntModels/cu import { apiKeys } from "../models/devModels/apiKeyTable.js"; import { metadata } from "../models/otherModels/metadataTable.js"; import { subscriptions } from "../models/subModels/subTable.js"; +import { invoices } from "../models/cusModels/invoiceModels/invoiceTable.js"; + +// Reward Tables +import { rewards } from "../models/rewardModels/rewardModels/rewardTable.js"; +import { rewardPrograms } from "../models/rewardModels/rewardProgramModels/rewardProgramTable.js"; +import { referralCodes } from "../models/rewardModels/referralModels/referralCodeTable.js"; +import { rewardRedemptions } from "../models/rewardModels/referralModels/rewardRedemptionTable.js"; + +// Migration Tables +import { migrationJobs } from "../models/migrationModels/migrationJobTable.js"; +import { migrationErrors } from "../models/migrationModels/migrationErrorTable.js"; /* RELATIONS */ import { organizationsRelations } from "../models/orgModels/orgRelations.js"; @@ -43,6 +54,14 @@ import { customerPricesRelations } from "../models/cusProductModels/cusPriceMode import { customerEntitlementsRelations } from "../models/cusProductModels/cusEntModels/cusEntRelations.js"; import { apiKeyRelations } from "../models/devModels/apiKeyRelations.js"; +// Reward Relations +import { rewardProgramRelations } from "../models/rewardModels/rewardProgramModels/rewardProgramRelations.js"; +import { referralCodeRelations } from "../models/rewardModels/referralModels/referralCodeRelations.js"; +import { rewardRedemptionRelations } from "../models/rewardModels/referralModels/rewardRedemptionRelations.js"; + +// Migration Relations +import { migrationErrorRelations } from "../models/migrationModels/migrationErrorRelations.js"; + const relations = { organizationsRelations, entitlementsRelations, @@ -58,6 +77,14 @@ const relations = { customersRelations, entitiesRelations, apiKeyRelations, + + // Reward Relations + rewardProgramRelations, + referralCodeRelations, + rewardRedemptionRelations, + + // Migration Relations + migrationErrorRelations, }; export const schemas = { @@ -71,6 +98,7 @@ export const schemas = { customerProducts, customerPrices, customerEntitlements, + invoices, // Customer customers, @@ -81,5 +109,15 @@ export const schemas = { metadata, subscriptions, + // Reward Tables + rewards, + rewardPrograms, + referralCodes, + rewardRedemptions, + + // Migration Tables + migrationJobs, + migrationErrors, + ...relations, }; diff --git a/shared/enums/ErrCode.ts b/shared/enums/ErrCode.ts index 9106f6171..577bed14e 100644 --- a/shared/enums/ErrCode.ts +++ b/shared/enums/ErrCode.ts @@ -133,4 +133,23 @@ export const ErrCode = { // Entity EntityIdRequired: "entity_id_required", + + // Subscription + InsertSubscriptionFailed: "insert_subscription_failed", + UpdateSubscriptionFailed: "update_subscription_failed", + + // Rewards + RewardNotFound: "reward_not_found", + RewardProgramNotFound: "reward_program_not_found", + InsertRewardProgramFailed: "insert_reward_program_failed", + InsertReferralCodeFailed: "insert_referral_code_failed", + ReferralCodeNotFound: "referral_code_not_found", + UpdateRewardRedemptionFailed: "update_reward_redemption_failed", + RewardRedemptionNotFound: "reward_redemption_not_found", + InsertRewardRedemptionFailed: "insert_reward_redemption_failed", + + // Migration + InsertMigrationJobFailed: "insert_migration_job_failed", + InsertMigrationErrorFailed: "insert_migration_error_failed", + MigrationJobNotFound: "migration_job_not_found", }; diff --git a/shared/index.ts b/shared/index.ts index e500c5bd0..9c7575f4c 100644 --- a/shared/index.ts +++ b/shared/index.ts @@ -47,9 +47,11 @@ export * from "./models/productV2Models/productItemModels/prodItemResponseModels // 7. Cus Product Models export * from "./models/cusProductModels/cusProductModels.js"; +export * from "./models/cusProductModels/cusProductTable.js"; export * from "./models/cusProductModels/cusProductEnums.js"; export * from "./models/cusProductModels/cusEntModels/cusEntModels.js"; export * from "./models/cusProductModels/cusEntModels/cusEntWithProduct.js"; +export * from "./models/cusProductModels/cusEntModels/cusEntTable.js"; export * from "./models/cusProductModels/cusPriceModels/cusPriceModels.js"; export * from "./models/cusProductModels/cusPriceModels/cusPriceTable.js"; @@ -59,6 +61,8 @@ export * from "./models/cusModels/cusTable.js"; export * from "./models/cusModels/fullCusModel.js"; export * from "./models/cusModels/cusExpand.js"; export * from "./models/cusModels/invoiceModels/invoiceResponseModels.js"; +export * from "./models/cusModels/invoiceModels/invoiceTable.js"; +export * from "./models/cusModels/cusResponseModels.js"; export * from "./models/cusModels/entityModels/entityModels.js"; export * from "./models/cusModels/entityModels/entityTable.js"; @@ -69,6 +73,19 @@ export * from "./models/cusModels/entityModels/entityResModels.js"; export * from "./models/chatResultModels/chatResultTable.js"; export * from "./models/chatResultModels/chatResultFeature.js"; +// Reward Models +export * from "./models/rewardModels/rewardModels/rewardModels.js"; +export * from "./models/rewardModels/rewardModels/rewardEnums.js"; +export * from "./models/rewardModels/rewardModels/rewardTable.js"; +export * from "./models/rewardModels/rewardModels/rewardResponseModels.js"; + +export * from "./models/rewardModels/rewardProgramModels/rewardProgramModels.js"; +export * from "./models/rewardModels/rewardProgramModels/rewardProgramEnums.js"; +export * from "./models/rewardModels/rewardProgramModels/rewardProgramTable.js"; +export * from "./models/rewardModels/referralModels/referralModels.js"; +export * from "./models/rewardModels/referralModels/rewardRedemptionTable.js"; +export * from "./models/rewardModels/referralModels/referralCodeTable.js"; + // 5. Others: events, apiKeys export * from "./models/eventModels/eventModels.js"; export * from "./models/eventModels/eventTable.js"; @@ -83,22 +100,14 @@ export * from "./models/subModels/subModels.js"; export * from "./models/subModels/subTable.js"; export * from "./models/cusModels/invoiceModels/invoiceModels.js"; -export * from "./models/cusModels/cusResponseModels.js"; export * from "./models/migrationModels/migrationModels.js"; - -// Product Models -export * from "./models/productModels/freeTrialModels/freeTrialModels.js"; -export * from "./models/rewardModels/rewardModels.js"; -export * from "./models/rewardModels/rewardProgramModels.js"; -export * from "./models/rewardModels/referralModels/referralModels.js"; +export * from "./models/migrationModels/migrationJobTable.js"; +export * from "./models/migrationModels/migrationErrorTable.js"; // Utils export * from "./utils/displayUtils.js"; export * from "./models/checkModels/checkPreviewModels.js"; - -// Reward Models -export * from "./models/rewardModels/rewardResponseModels.js"; export * from "./models/chatResultModels/chatResultFeature.js"; // ENUMS diff --git a/shared/models/cusModels/cusResponseModels.ts b/shared/models/cusModels/cusResponseModels.ts index a923a828a..89bd09c88 100644 --- a/shared/models/cusModels/cusResponseModels.ts +++ b/shared/models/cusModels/cusResponseModels.ts @@ -2,8 +2,8 @@ import { z } from "zod"; import { AppEnv } from "../genModels/genEnums.js"; import { EntInterval } from "../productModels/entModels/entEnums.js"; import { InvoiceResponseSchema } from "./invoiceModels/invoiceResponseModels.js"; -import { RewardResponseSchema } from "../rewardModels/rewardResponseModels.js"; import { CusProductStatus } from "../cusProductModels/cusProductEnums.js"; +import { RewardResponseSchema } from "../rewardModels/rewardModels/rewardResponseModels.js"; export const CusProductResponseSchema = z.object({ id: z.string(), diff --git a/shared/models/cusModels/cusTable.ts b/shared/models/cusModels/cusTable.ts index b9b3fa26b..1333d0467 100644 --- a/shared/models/cusModels/cusTable.ts +++ b/shared/models/cusModels/cusTable.ts @@ -41,3 +41,6 @@ export const customers = pgTable( ).enableRLS(); collatePgColumn(customers.internal_id, "C"); + +// CREATE INDEX idx_customers_org_env_internal_id +// ON customers (org_id, env, internal_id DESC); diff --git a/shared/models/cusModels/fullCusModel.ts b/shared/models/cusModels/fullCusModel.ts index a96affa75..0b1a4348b 100644 --- a/shared/models/cusModels/fullCusModel.ts +++ b/shared/models/cusModels/fullCusModel.ts @@ -1,4 +1,5 @@ import { FullCusProduct } from "../cusProductModels/cusProductModels.js"; +import { Subscription } from "../subModels/subModels.js"; import { Customer } from "./cusModels.js"; import { Entity } from "./entityModels/entityModels.js"; import { Invoice } from "./invoiceModels/invoiceModels.js"; @@ -7,10 +8,11 @@ export type FullCustomer = Customer & { customer_products: FullCusProduct[]; entities: Entity[]; entity: Entity; - trials_used: { + trials_used?: { product_id: string; customer_id: string; fingerprint: string; }[]; - invoices: Invoice[]; + invoices?: Invoice[]; + subscriptions?: Subscription[]; }; diff --git a/shared/models/cusModels/invoiceModels/invoiceTable.ts b/shared/models/cusModels/invoiceModels/invoiceTable.ts new file mode 100644 index 000000000..14535cafd --- /dev/null +++ b/shared/models/cusModels/invoiceModels/invoiceTable.ts @@ -0,0 +1,40 @@ +import { foreignKey, jsonb, numeric, pgTable, text } from "drizzle-orm/pg-core"; +import { collatePgColumn, sqlNow } from "../../../db/utils.js"; +import { InvoiceDiscount, InvoiceItem } from "./invoiceModels.js"; +import { customers } from "../cusTable.js"; +import { entities } from "../entityModels/entityTable.js"; + +export const invoices = pgTable( + "invoices", + { + id: text("id").primaryKey(), + created_at: numeric({ mode: "number" }).notNull().default(sqlNow), + product_ids: text("product_ids").array().default([]), + internal_product_ids: text("internal_product_ids").array().default([]), + + internal_customer_id: text("internal_customer_id").notNull(), + internal_entity_id: text("internal_entity_id"), + + stripe_id: text("stripe_id").notNull(), + status: text("status").notNull().default("draft"), + hosted_invoice_url: text("hosted_invoice_url"), + total: numeric({ mode: "number" }).notNull().default(0), + currency: text("currency").notNull().default("usd"), + discounts: jsonb("discounts").$type().array().default([]), + items: jsonb("items").$type().array().default([]), + }, + (table) => [ + foreignKey({ + columns: [table.internal_customer_id], + foreignColumns: [customers.id], + name: "invoices_internal_customer_id_fkey", + }), + foreignKey({ + columns: [table.internal_entity_id], + foreignColumns: [entities.id], + name: "invoices_internal_entity_id_fkey", + }), + ], +); + +collatePgColumn(invoices.id, "C"); diff --git a/shared/models/cusProductModels/cusEntModels/cusEntModels.ts b/shared/models/cusProductModels/cusEntModels/cusEntModels.ts index 879576eee..e3be0dff4 100644 --- a/shared/models/cusProductModels/cusEntModels/cusEntModels.ts +++ b/shared/models/cusProductModels/cusEntModels/cusEntModels.ts @@ -22,7 +22,7 @@ export const CustomerEntitlementSchema = z.object({ // Balance fields unlimited: z.boolean().nullish(), - balance: z.number().nullable(), + balance: z.number().default(0), usage_allowed: z.boolean().nullable(), next_reset_at: z.number().nullable(), diff --git a/shared/models/migrationModels/migrationErrorRelations.ts b/shared/models/migrationModels/migrationErrorRelations.ts new file mode 100644 index 000000000..8820e9683 --- /dev/null +++ b/shared/models/migrationModels/migrationErrorRelations.ts @@ -0,0 +1,13 @@ +import { relations } from "drizzle-orm"; +import { migrationErrors } from "./migrationErrorTable.js"; +import { customers } from "../cusModels/cusTable.js"; + +export const migrationErrorRelations = relations( + migrationErrors, + ({ one }) => ({ + customer: one(customers, { + fields: [migrationErrors.internal_customer_id], + references: [customers.internal_id], + }), + }), +); diff --git a/shared/models/migrationModels/migrationErrorTable.ts b/shared/models/migrationModels/migrationErrorTable.ts new file mode 100644 index 000000000..699a10b61 --- /dev/null +++ b/shared/models/migrationModels/migrationErrorTable.ts @@ -0,0 +1,41 @@ +import { + pgTable, + text, + numeric, + jsonb, + foreignKey, + primaryKey, +} from "drizzle-orm/pg-core"; + +import { migrationJobs } from "./migrationJobTable.js"; +import { customers } from "../cusModels/cusTable.js"; + +export const migrationErrors = pgTable( + "migration_errors", + { + internal_customer_id: text().notNull(), + migration_job_id: text().notNull(), + created_at: numeric({ mode: "number" }), + updated_at: numeric({ mode: "number" }), + data: jsonb(), + message: text(), + code: text(), + }, + (table) => [ + foreignKey({ + columns: [table.internal_customer_id], + foreignColumns: [customers.internal_id], + name: "migration_customers_internal_customer_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.migration_job_id], + foreignColumns: [migrationJobs.id], + name: "migration_customers_migration_job_id_fkey", + }).onDelete("cascade"), + + primaryKey({ + columns: [table.internal_customer_id, table.migration_job_id], + name: "migration_errors_pkey", + }), + ], +); diff --git a/shared/models/migrationModels/migrationJobTable.ts b/shared/models/migrationModels/migrationJobTable.ts new file mode 100644 index 000000000..ac2a8b936 --- /dev/null +++ b/shared/models/migrationModels/migrationJobTable.ts @@ -0,0 +1,36 @@ +import { pgTable, text, numeric, jsonb, foreignKey } from "drizzle-orm/pg-core"; +import { products } from "../productModels/productTable.js"; +import { organizations } from "../orgModels/orgTable.js"; + +export const migrationJobs = pgTable( + "migration_jobs", + { + id: text().primaryKey().notNull(), + org_id: text().notNull(), + env: text().notNull(), + + created_at: numeric({ mode: "number" }).notNull(), + updated_at: numeric({ mode: "number" }), + current_step: text(), + from_internal_product_id: text(), + to_internal_product_id: text(), + step_details: jsonb(), + }, + (table) => [ + foreignKey({ + columns: [table.from_internal_product_id], + foreignColumns: [products.internal_id], + name: "migration_jobs_from_internal_product_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.org_id], + foreignColumns: [organizations.id], + name: "migration_jobs_org_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.to_internal_product_id], + foreignColumns: [products.internal_id], + name: "migration_jobs_to_internal_product_id_fkey", + }).onDelete("cascade"), + ], +); diff --git a/shared/models/rewardModels/discountModels.ts b/shared/models/rewardModels/discountModels.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/shared/models/rewardModels/referralModels/referralCodeRelations.ts b/shared/models/rewardModels/referralModels/referralCodeRelations.ts new file mode 100644 index 000000000..34a90af37 --- /dev/null +++ b/shared/models/rewardModels/referralModels/referralCodeRelations.ts @@ -0,0 +1,21 @@ +import { relations } from "drizzle-orm"; +import { referralCodes } from "./referralCodeTable.js"; +import { customers } from "../../cusModels/cusTable.js"; +import { rewardPrograms } from "../rewardProgramModels/rewardProgramTable.js"; +import { rewardRedemptions } from "./rewardRedemptionTable.js"; + +export const referralCodeRelations = relations( + referralCodes, + ({ many, one }) => ({ + reward_program: one(rewardPrograms, { + fields: [referralCodes.internal_reward_program_id], + references: [rewardPrograms.internal_id], + }), + customer: one(customers, { + fields: [referralCodes.internal_customer_id], + references: [customers.internal_id], + }), + + reward_redemptions: many(rewardRedemptions), + }), +); diff --git a/shared/models/rewardModels/referralModels/referralCodeTable.ts b/shared/models/rewardModels/referralModels/referralCodeTable.ts new file mode 100644 index 000000000..71ab0b030 --- /dev/null +++ b/shared/models/rewardModels/referralModels/referralCodeTable.ts @@ -0,0 +1,47 @@ +import { + text, + foreignKey, + pgTable, + numeric, + primaryKey, + unique, +} from "drizzle-orm/pg-core"; +import { customers } from "../../cusModels/cusTable.js"; +import { rewardPrograms } from "../rewardProgramModels/rewardProgramTable.js"; +import { organizations } from "../../orgModels/orgTable.js"; + +export const referralCodes = pgTable( + "referral_codes", + { + code: text().notNull(), + org_id: text("org_id").notNull(), + env: text().notNull(), + internal_customer_id: text("internal_customer_id"), + internal_reward_program_id: text("internal_reward_program_id"), + id: text().notNull(), + created_at: numeric({ mode: "number" }), + }, + + (table) => [ + foreignKey({ + columns: [table.internal_customer_id], + foreignColumns: [customers.internal_id], + name: "referral_codes_internal_customer_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.internal_reward_program_id], + foreignColumns: [rewardPrograms.internal_id], + name: "referral_codes_internal_reward_program_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.org_id], + foreignColumns: [organizations.id], + name: "referral_codes_org_id_fkey", + }).onDelete("cascade"), + primaryKey({ + columns: [table.code, table.org_id, table.env], + name: "referral_codes_pkey", + }), + unique("referral_codes_id_key").on(table.id), + ], +); diff --git a/shared/models/rewardModels/referralModels/rewardRedemptionRelations.ts b/shared/models/rewardModels/referralModels/rewardRedemptionRelations.ts new file mode 100644 index 000000000..f3b9dcfc9 --- /dev/null +++ b/shared/models/rewardModels/referralModels/rewardRedemptionRelations.ts @@ -0,0 +1,25 @@ +import { relations } from "drizzle-orm"; +import { rewardRedemptions } from "./rewardRedemptionTable.js"; +import { customers } from "../../cusModels/cusTable.js"; +import { referralCodes } from "./referralCodeTable.js"; +import { rewardPrograms } from "../rewardProgramModels/rewardProgramTable.js"; + +export const rewardRedemptionRelations = relations( + rewardRedemptions, + ({ one }) => ({ + customer: one(customers, { + fields: [rewardRedemptions.internal_customer_id], + references: [customers.internal_id], + }), + + referral_code: one(referralCodes, { + fields: [rewardRedemptions.referral_code_id], + references: [referralCodes.id], + }), + + reward_program: one(rewardPrograms, { + fields: [rewardRedemptions.internal_reward_program_id], + references: [rewardPrograms.internal_id], + }), + }), +); diff --git a/shared/models/rewardModels/referralModels/rewardRedemptionTable.ts b/shared/models/rewardModels/referralModels/rewardRedemptionTable.ts new file mode 100644 index 000000000..54eb0ab75 --- /dev/null +++ b/shared/models/rewardModels/referralModels/rewardRedemptionTable.ts @@ -0,0 +1,41 @@ +import { + text, + foreignKey, + boolean, + pgTable, + numeric, +} from "drizzle-orm/pg-core"; +import { customers } from "../../cusModels/cusTable.js"; +import { rewardPrograms } from "../rewardProgramModels/rewardProgramTable.js"; +import { referralCodes } from "./referralCodeTable.js"; + +export const rewardRedemptions = pgTable( + "reward_redemptions", + { + id: text().primaryKey().notNull(), + created_at: numeric({ mode: "number" }), + updated_at: numeric({ mode: "number" }), + internal_customer_id: text("internal_customer_id"), + triggered: boolean(), + internal_reward_program_id: text("internal_reward_program_id"), + applied: boolean().default(false), + referral_code_id: text("referral_code_id"), + }, + (table) => [ + foreignKey({ + columns: [table.internal_customer_id], + foreignColumns: [customers.internal_id], + name: "reward_redemptions_internal_customer_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.internal_reward_program_id], + foreignColumns: [rewardPrograms.internal_id], + name: "reward_redemptions_internal_reward_program_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.referral_code_id], + foreignColumns: [referralCodes.id], + name: "reward_redemptions_referral_code_id_fkey", + }).onDelete("cascade"), + ], +); diff --git a/shared/models/rewardModels/rewardModels/rewardEnums.ts b/shared/models/rewardModels/rewardModels/rewardEnums.ts new file mode 100644 index 000000000..810bccdc0 --- /dev/null +++ b/shared/models/rewardModels/rewardModels/rewardEnums.ts @@ -0,0 +1,21 @@ +export enum RewardCategory { + FreeProduct = "free_product", + Discount = "discount", +} + +export enum CouponDurationType { + Months = "months", + OneOff = "one_off", + Forever = "forever", +} + +export enum DiscountType { + Percentage = "percentage", + Fixed = "fixed", +} + +export enum RewardType { + PercentageDiscount = "percentage_discount", + FixedDiscount = "fixed_discount", + FreeProduct = "free_product", +} diff --git a/shared/models/rewardModels/rewardModels.ts b/shared/models/rewardModels/rewardModels/rewardModels.ts similarity index 64% rename from shared/models/rewardModels/rewardModels.ts rename to shared/models/rewardModels/rewardModels/rewardModels.ts index cb5beb125..2feb6ba39 100644 --- a/shared/models/rewardModels/rewardModels.ts +++ b/shared/models/rewardModels/rewardModels/rewardModels.ts @@ -1,28 +1,5 @@ import { z } from "zod"; - -export enum RewardCategory { - FreeProduct = "free_product", - Discount = "discount", -} - -export enum CouponDurationType { - Months = "months", - OneOff = "one_off", - Forever = "forever", -} - -export enum DiscountType { - Percentage = "percentage", - Fixed = "fixed", -} - -export enum RewardType { - // Coupon = "coupon", - // Reward = "reward", - PercentageDiscount = "percentage_discount", - FixedDiscount = "fixed_discount", - FreeProduct = "free_product", -} +import { CouponDurationType, RewardType } from "./rewardEnums.js"; const PromoCodeSchema = z.object({ code: z.string(), @@ -42,17 +19,11 @@ const RewardSchema = z.object({ promo_codes: z.array(PromoCodeSchema), id: z.string(), - - // discount_type: z.nativeEnum(DiscountType), type: z.nativeEnum(RewardType), - // For free product coupons free_product_id: z.string().nullish(), - - // For discount type coupons discount_config: DiscountConfigSchema.nullish(), - // EXTRA internal_id: z.string(), org_id: z.string(), env: z.string(), @@ -63,16 +34,12 @@ export const CreateRewardSchema = z.object({ name: z.string(), promo_codes: z.array(PromoCodeSchema), id: z.string().nullish(), - type: z.nativeEnum(RewardType).nullish(), - - // For discount type coupons discount_config: DiscountConfigSchema.nullish(), - - // For free product coupons free_product_id: z.string().nullish(), }); +export type PromoCode = z.infer; export type CreateReward = z.infer; export type Reward = z.infer; export type DiscountConfig = z.infer; diff --git a/shared/models/rewardModels/rewardResponseModels.ts b/shared/models/rewardModels/rewardModels/rewardResponseModels.ts similarity index 87% rename from shared/models/rewardModels/rewardResponseModels.ts rename to shared/models/rewardModels/rewardModels/rewardResponseModels.ts index efe050057..4233be3db 100644 --- a/shared/models/rewardModels/rewardResponseModels.ts +++ b/shared/models/rewardModels/rewardModels/rewardResponseModels.ts @@ -1,5 +1,6 @@ import { z } from "zod"; -import { CouponDurationType, RewardType } from "./rewardModels.js"; +import { RewardType } from "./rewardEnums.js"; +import { CouponDurationType } from "./rewardEnums.js"; export const DiscountResponseSchema = z.object({ id: z.string(), // either from Autumn or Stripe diff --git a/shared/models/rewardModels/rewardModels/rewardTable.ts b/shared/models/rewardModels/rewardModels/rewardTable.ts new file mode 100644 index 000000000..3f2527207 --- /dev/null +++ b/shared/models/rewardModels/rewardModels/rewardTable.ts @@ -0,0 +1,28 @@ +import { foreignKey, jsonb, text } from "drizzle-orm/pg-core"; +import { numeric } from "drizzle-orm/pg-core"; +import { pgTable } from "drizzle-orm/pg-core"; +import { organizations } from "../../orgModels/orgTable.js"; +import { DiscountConfig, PromoCode } from "./rewardModels.js"; + +export const rewards = pgTable( + "rewards", + { + internal_id: text("internal_id").primaryKey().notNull(), + id: text(), + org_id: text("org_id"), + env: text(), + created_at: numeric({ mode: "number" }), + name: text(), + discount_config: jsonb("discount_config").$type(), + free_product_id: text("free_product_id"), + promo_codes: jsonb("promo_codes").$type().array(), + type: text(), + }, + (table) => [ + foreignKey({ + columns: [table.org_id], + foreignColumns: [organizations.id], + name: "coupons_org_id_fkey", + }).onDelete("cascade"), + ], +); diff --git a/shared/models/rewardModels/rewardProgramModels/rewardProgramEnums.ts b/shared/models/rewardModels/rewardProgramModels/rewardProgramEnums.ts new file mode 100644 index 000000000..709d0aa6e --- /dev/null +++ b/shared/models/rewardModels/rewardProgramModels/rewardProgramEnums.ts @@ -0,0 +1,9 @@ +export enum RewardTriggerEvent { + CustomerCreation = "customer_creation", + Checkout = "checkout", +} + +export enum RewardReceivedBy { + Referrer = "referrer", + All = "all", +} diff --git a/shared/models/rewardModels/rewardProgramModels.ts b/shared/models/rewardModels/rewardProgramModels/rewardProgramModels.ts similarity index 76% rename from shared/models/rewardModels/rewardProgramModels.ts rename to shared/models/rewardModels/rewardProgramModels/rewardProgramModels.ts index 49559f415..8a10fd9c3 100644 --- a/shared/models/rewardModels/rewardProgramModels.ts +++ b/shared/models/rewardModels/rewardProgramModels/rewardProgramModels.ts @@ -1,18 +1,7 @@ import { z } from "zod"; -import { Reward } from "./rewardModels.js"; - -export enum RewardTriggerEvent { - // SignUp = "sign_up", - // Immediately = "immediately", - CustomerCreation = "customer_creation", - Checkout = "checkout", -} - -export enum RewardReceivedBy { - Referrer = "referrer", - All = "all", - // Redeemer = "redeemer", -} +import { Reward } from "../rewardModels/rewardModels.js"; +import { RewardReceivedBy } from "./rewardProgramEnums.js"; +import { RewardTriggerEvent } from "./rewardProgramEnums.js"; export const RewardProgram = z.object({ internal_id: z.string(), @@ -41,7 +30,6 @@ export const CreateRewardProgram = z.object({ exclude_trial: z.boolean().optional(), internal_reward_id: z.string(), max_redemptions: z.number().optional(), - received_by: z.nativeEnum(RewardReceivedBy), }); diff --git a/shared/models/rewardModels/rewardProgramModels/rewardProgramRelations.ts b/shared/models/rewardModels/rewardProgramModels/rewardProgramRelations.ts new file mode 100644 index 000000000..d3da2afae --- /dev/null +++ b/shared/models/rewardModels/rewardProgramModels/rewardProgramRelations.ts @@ -0,0 +1,10 @@ +import { relations } from "drizzle-orm"; +import { rewardPrograms } from "./rewardProgramTable.js"; +import { rewards } from "../rewardModels/rewardTable.js"; + +export const rewardProgramRelations = relations(rewardPrograms, ({ one }) => ({ + reward: one(rewards, { + fields: [rewardPrograms.internal_reward_id], + references: [rewards.internal_id], + }), +})); diff --git a/shared/models/rewardModels/rewardProgramModels/rewardProgramTable.ts b/shared/models/rewardModels/rewardProgramModels/rewardProgramTable.ts new file mode 100644 index 000000000..e9174f852 --- /dev/null +++ b/shared/models/rewardModels/rewardProgramModels/rewardProgramTable.ts @@ -0,0 +1,34 @@ +import { pgTable, text, foreignKey, boolean } from "drizzle-orm/pg-core"; +import { numeric } from "drizzle-orm/pg-core"; +import { organizations } from "../../orgModels/orgTable.js"; +import { rewards } from "../rewardModels/rewardTable.js"; + +export const rewardPrograms = pgTable( + "reward_programs", + { + internal_id: text("internal_id").primaryKey().notNull(), + id: text(), + created_at: numeric({ mode: "number" }), + internal_reward_id: text("internal_reward_id"), + max_redemptions: numeric({ mode: "number" }), + unlimited_redemptions: boolean("unlimited_redemptions").default(false), + org_id: text("org_id"), + env: text(), + when: text().default("immediately"), + product_ids: text("product_ids").array().default([""]), + exclude_trial: boolean("exclude_trial").default(false), + received_by: text("received_by"), + }, + (table) => [ + foreignKey({ + columns: [table.internal_reward_id], + foreignColumns: [rewards.internal_id], + name: "reward_triggers_internal_reward_id_fkey", + }).onDelete("cascade"), + foreignKey({ + columns: [table.org_id], + foreignColumns: [organizations.id], + name: "reward_triggers_org_id_fkey", + }).onDelete("cascade"), + ], +); diff --git a/vite/src/views/customers/CustomersView.tsx b/vite/src/views/customers/CustomersView.tsx index 7d8d7040f..a2131192c 100644 --- a/vite/src/views/customers/CustomersView.tsx +++ b/vite/src/views/customers/CustomersView.tsx @@ -25,7 +25,7 @@ function CustomersView({ env }: { env: AppEnv }) { const [searchParams] = useSearchParams(); const [searchQuery, setSearchQuery] = React.useState( - searchParams.get("q") || "" + searchParams.get("q") || "", ); const [filters, setFilters] = React.useState({ @@ -214,8 +214,7 @@ function CustomersView({ env }: { env: AppEnv }) {

- {data?.totalCount || 0} - {/* {data?.totalCount === 1 || "Customer" : "Customers"} */} + {data?.totalCount}