import { sql, type InferInsertModel, type InferSelectModel } from "drizzle-orm"; import { boolean, foreignKey, index, jsonb, numeric, pgTable, text, } from "drizzle-orm/pg-core"; import { collatePgColumn } from "../../db/utils.js"; import { customers } from "../cusModels/cusTable.js"; import { entities } from "../cusModels/entityModels/entityTable.js"; import { freeTrials } from "../productModels/freeTrialModels/freeTrialTable.js"; import { products } from "../productModels/productTable.js"; export type CustomerProductProcessor = { type: "stripe" | "revenuecat"; id: string; }; export const customerProducts = pgTable( "customer_products", { id: text().primaryKey().notNull(), internal_customer_id: text("internal_customer_id").notNull(), internal_product_id: text("internal_product_id").notNull(), internal_entity_id: text("internal_entity_id"), created_at: numeric({ mode: "number" }), updated_at: numeric({ mode: "number" }), status: text(), processor: jsonb().$type(), // processors: jsonb().$type(), canceled: boolean("canceled").default(false), canceled_at: numeric({ mode: "number" }), ended_at: numeric({ mode: "number" }), starts_at: numeric({ mode: "number" }), access_starts_at: numeric({ mode: "number" }), options: jsonb().array(), product_id: text("product_id"), free_trial_id: text("free_trial_id"), trial_ends_at: numeric({ mode: "number" }), billing_cycle_anchor_resets_at: numeric({ mode: "number" }), collection_method: text("collection_method").default( "charge_automatically", ), subscription_ids: text("subscription_ids").array(), scheduled_ids: text("scheduled_ids").array(), quantity: numeric({ mode: "number" }).default(1), is_custom: boolean("is_custom").default(false).notNull(), // Optional... customer_id: text("customer_id"), entity_id: text("entity_id"), billing_version: text("billing_version"), api_version: numeric({ mode: "number" }), api_semver: text("api_semver"), external_id: text("external_id"), // When the cusProduct was created via a Stripe checkout flow with // enable_plan_immediately, this links the row to the pending checkout session // so the webhook can patch in subscription_ids on completion (or expire on abandonment). stripe_checkout_session_id: text("stripe_checkout_session_id"), previous_customer_product_id: text("previous_customer_product_id"), on_trial_end: text("on_trial_end"), }, (table) => [ foreignKey({ columns: [table.free_trial_id], foreignColumns: [freeTrials.id], name: "customer_products_free_trial_id_fkey", }), foreignKey({ columns: [table.internal_customer_id], foreignColumns: [customers.internal_id], name: "customer_products_internal_customer_id_fkey", }) .onUpdate("cascade") .onDelete("cascade"), foreignKey({ columns: [table.internal_product_id], foreignColumns: [products.internal_id], name: "customer_products_internal_product_id_fkey", }), foreignKey({ columns: [table.internal_entity_id], foreignColumns: [entities.internal_id], name: "customer_products_internal_entity_id_fkey", }).onDelete("set null"), index("idx_customer_products_customer_status").on( table.internal_customer_id, table.status, ), index("idx_customer_products_on_internal_entity_id").on( table.internal_entity_id, ), index("idx_customer_products_on_internal_product_id").on( table.internal_product_id, ), index("idx_customer_products_subscription_ids").using( "gin", table.subscription_ids, ), index("idx_customer_products_scheduled_ids").using( "gin", table.scheduled_ids, ), index("idx_customer_products_stripe_checkout_session_id").on( table.stripe_checkout_session_id, ), index("idx_customer_products_revenuecat_processor") .on(table.internal_customer_id) .where(sql`(${table.processor} ->> 'type') = 'revenuecat'`), ], ); collatePgColumn(customerProducts.id, "C"); export type CustomerProduct = InferSelectModel; export type InsertCustomerProduct = InferInsertModel; export type DbCustomerProduct = InferSelectModel; export type InsertDbCustomerProduct = InferInsertModel;