chore: fix customer email config
This commit is contained in:
13
server/src/external/autumn/autumnCli.ts
vendored
13
server/src/external/autumn/autumnCli.ts
vendored
@@ -468,6 +468,19 @@ export class AutumnInt {
|
||||
return data;
|
||||
},
|
||||
|
||||
update: async (
|
||||
customerId: string,
|
||||
updates: {
|
||||
name?: string;
|
||||
email?: string;
|
||||
send_email_receipts?: boolean;
|
||||
metadata?: Record<string, unknown>;
|
||||
},
|
||||
) => {
|
||||
const data = await this.patch(`/customers/${customerId}`, updates);
|
||||
return data;
|
||||
},
|
||||
|
||||
setBalance: async ({
|
||||
customerId,
|
||||
balances,
|
||||
|
||||
@@ -22,7 +22,7 @@ export const sendEmailReceipt = async ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fullCustomer.should_send_email_receipts) {
|
||||
if (!fullCustomer.send_email_receipts) {
|
||||
logger.debug(
|
||||
"[invoice.paid] Customer has email receipts disabled, skipping",
|
||||
);
|
||||
|
||||
@@ -59,6 +59,7 @@ export const getApiCustomerBase = async ({
|
||||
),
|
||||
|
||||
balances: apiBalances,
|
||||
send_email_receipts: fullCus.send_email_receipts ?? false,
|
||||
|
||||
invoices:
|
||||
fullCus.invoices && ctx.expand.includes(CusExpand.Invoices)
|
||||
|
||||
@@ -48,6 +48,13 @@ export const updateCustomerDetails = async ({
|
||||
updates.email = customerData.email;
|
||||
}
|
||||
}
|
||||
// Update send_email_receipts if explicitly provided
|
||||
if (customerData?.send_email_receipts !== undefined) {
|
||||
const fullCus = customer as FullCustomer;
|
||||
if (fullCus.send_email_receipts !== customerData.send_email_receipts) {
|
||||
updates.send_email_receipts = customerData.send_email_receipts;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length > 0) {
|
||||
logger.info(`Updating customer details:`, {
|
||||
|
||||
@@ -119,6 +119,10 @@ export const getCachedFullCustomer = async ({
|
||||
fullCustomer.extra_customer_entitlements = [];
|
||||
}
|
||||
|
||||
if (!fullCustomer.send_email_receipts) {
|
||||
fullCustomer.send_email_receipts = false;
|
||||
}
|
||||
|
||||
// Round balance fields to handle floating-point precision from JSON.NUMINCRBY
|
||||
return roundFullCustomerBalances(fullCustomer);
|
||||
};
|
||||
|
||||
@@ -33,6 +33,7 @@ export const initCustomer = ({
|
||||
type: "stripe",
|
||||
}
|
||||
: null,
|
||||
send_email_receipts: customerData?.send_email_receipts ?? false,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { createRoute } from "@/honoMiddlewares/routeHandler.js";
|
||||
import { notNullish } from "@/utils/genUtils.js";
|
||||
import { CusService } from "../CusService.js";
|
||||
import { getApiCustomer } from "../cusUtils/apiCusUtils/getApiCustomer.js";
|
||||
import { deleteCachedFullCustomer } from "../cusUtils/fullCustomerCacheUtils/deleteCachedFullCustomer.js";
|
||||
import { getOrSetCachedFullCustomer } from "../cusUtils/fullCustomerCacheUtils/getOrSetCachedFullCustomer.js";
|
||||
|
||||
export const handleUpdateCustomerV2 = createRoute({
|
||||
@@ -128,6 +129,13 @@ export const handleUpdateCustomerV2 = createRoute({
|
||||
update: updateData,
|
||||
});
|
||||
|
||||
// Invalidate cache after DB update
|
||||
await deleteCachedFullCustomer({
|
||||
customerId: customer_id,
|
||||
ctx,
|
||||
source: "handleUpdateCustomerV2",
|
||||
});
|
||||
|
||||
// Skip cache to get fresh data after update
|
||||
ctx.skipCache = true;
|
||||
const fullCustomer = await getOrSetCachedFullCustomer({
|
||||
|
||||
@@ -15,6 +15,7 @@ export const initCustomerV3 = async ({
|
||||
withDefault = false,
|
||||
defaultGroup = customerId,
|
||||
skipWebhooks,
|
||||
sendEmailReceipts,
|
||||
}: {
|
||||
ctx: TestContext;
|
||||
customerId: string;
|
||||
@@ -24,10 +25,10 @@ export const initCustomerV3 = async ({
|
||||
withDefault?: boolean;
|
||||
defaultGroup?: string;
|
||||
skipWebhooks?: boolean;
|
||||
sendEmailReceipts?: boolean;
|
||||
}) => {
|
||||
const name = customerId;
|
||||
const email = `${customerId}@example.com`;
|
||||
const fingerprint_ = "";
|
||||
const { stripeCli } = ctx;
|
||||
const autumn = new AutumnInt({
|
||||
version: ApiVersion.V1_2,
|
||||
@@ -62,6 +63,7 @@ export const initCustomerV3 = async ({
|
||||
email,
|
||||
fingerprint: customerData?.fingerprint,
|
||||
stripe_id: stripeCus.id,
|
||||
send_email_receipts: sendEmailReceipts,
|
||||
internalOptions: {
|
||||
disable_defaults: !withDefault,
|
||||
// Only pass default_group when defaults are enabled
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* Tests for the sendEmailReceipt task in the invoice.paid webhook handler.
|
||||
* Verifies that email receipts are sent (via PaymentIntent.receipt_email)
|
||||
* based on the customer's should_send_email_receipts flag.
|
||||
* based on the customer's send_email_receipts flag.
|
||||
*/
|
||||
|
||||
import { expect, test } from "bun:test";
|
||||
@@ -16,7 +16,7 @@ import { CusService } from "@/internal/customers/CusService";
|
||||
import { timeout } from "@/utils/genUtils";
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// TEST 1: Email receipt sent when should_send_email_receipts is true
|
||||
// TEST 1: Email receipt sent when send_email_receipts is true
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
@@ -28,7 +28,7 @@ import { timeout } from "@/utils/genUtils";
|
||||
* Expected Result:
|
||||
* - PaymentIntent from the renewal invoice should have receipt_email set
|
||||
*/
|
||||
test(`${chalk.yellowBright("invoice.paid: sends email receipt when should_send_email_receipts is true")}`, async () => {
|
||||
test(`${chalk.yellowBright("invoice.paid: sends email receipt when send_email_receipts is true")}`, async () => {
|
||||
const customerId = "inv-paid-email-receipt-enabled";
|
||||
const testEmail = "test-receipt@example.com";
|
||||
|
||||
@@ -59,7 +59,7 @@ test(`${chalk.yellowBright("invoice.paid: sends email receipt when should_send_e
|
||||
orgId: ctx.org.id,
|
||||
env: ctx.env,
|
||||
update: {
|
||||
should_send_email_receipts: true,
|
||||
send_email_receipts: true,
|
||||
email: testEmail,
|
||||
},
|
||||
});
|
||||
@@ -74,7 +74,7 @@ test(`${chalk.yellowBright("invoice.paid: sends email receipt when should_send_e
|
||||
orgId: ctx.org.id,
|
||||
env: ctx.env,
|
||||
});
|
||||
expect(updatedCustomer.should_send_email_receipts).toBe(true);
|
||||
expect(updatedCustomer.send_email_receipts).toBe(true);
|
||||
expect(updatedCustomer.email).toBe(testEmail);
|
||||
|
||||
// Step 3: Advance to next billing cycle - this triggers invoice.paid webhook
|
||||
@@ -121,19 +121,19 @@ test(`${chalk.yellowBright("invoice.paid: sends email receipt when should_send_e
|
||||
}, 120000); // 2 minute timeout for test clock operations
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// TEST 2: Email receipt NOT sent when should_send_email_receipts is false
|
||||
// TEST 2: Email receipt NOT sent when send_email_receipts is false
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Scenario:
|
||||
* - Create customer and attach paid product
|
||||
* - Customer has should_send_email_receipts: false (default)
|
||||
* - Customer has send_email_receipts: false (default)
|
||||
* - Advance to next billing cycle (triggers invoice.paid)
|
||||
*
|
||||
* Expected Result:
|
||||
* - PaymentIntent from the renewal invoice should NOT have receipt_email set
|
||||
*/
|
||||
test(`${chalk.yellowBright("invoice.paid: does NOT send email receipt when should_send_email_receipts is false")}`, async () => {
|
||||
test(`${chalk.yellowBright("invoice.paid: does NOT send email receipt when send_email_receipts is false")}`, async () => {
|
||||
const customerId = "inv-paid-email-receipt-disabled";
|
||||
|
||||
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
|
||||
@@ -143,7 +143,7 @@ test(`${chalk.yellowBright("invoice.paid: does NOT send email receipt when shoul
|
||||
});
|
||||
|
||||
// Step 1: Create customer and attach product
|
||||
// Customer has should_send_email_receipts: false by default
|
||||
// Customer has send_email_receipts: false by default
|
||||
const { ctx, customer, testClockId } = await initScenario({
|
||||
customerId,
|
||||
setup: [
|
||||
@@ -158,7 +158,7 @@ test(`${chalk.yellowBright("invoice.paid: does NOT send email receipt when shoul
|
||||
expect(testClockId).toBeDefined();
|
||||
|
||||
// Step 2: Advance to next billing cycle - this triggers invoice.paid webhook
|
||||
// Since should_send_email_receipts is false, receipt_email should NOT be set
|
||||
// Since send_email_receipts is false, receipt_email should NOT be set
|
||||
await advanceToNextInvoice({
|
||||
stripeCli: ctx.stripeCli,
|
||||
testClockId: testClockId!,
|
||||
@@ -243,7 +243,7 @@ test(`${chalk.yellowBright("invoice.paid: does NOT send email receipt when custo
|
||||
orgId: ctx.org.id,
|
||||
env: ctx.env,
|
||||
update: {
|
||||
should_send_email_receipts: true,
|
||||
send_email_receipts: true,
|
||||
email: "", // Empty email
|
||||
},
|
||||
});
|
||||
@@ -258,7 +258,7 @@ test(`${chalk.yellowBright("invoice.paid: does NOT send email receipt when custo
|
||||
orgId: ctx.org.id,
|
||||
env: ctx.env,
|
||||
});
|
||||
expect(updatedCustomer.should_send_email_receipts).toBe(true);
|
||||
expect(updatedCustomer.send_email_receipts).toBe(true);
|
||||
expect(updatedCustomer.email).toBe("");
|
||||
|
||||
// Step 3: Advance to next billing cycle - this triggers invoice.paid webhook
|
||||
|
||||
@@ -106,3 +106,44 @@ test.concurrent(`${chalk.yellowBright("create: null ID no email (error)")}`, asy
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// SEND EMAIL RECEIPTS TESTS
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
test.concurrent(`${chalk.yellowBright("create: with send_email_receipts true")}`, async () => {
|
||||
const customerId = "create-send-email-receipts-true";
|
||||
const { autumnV1 } = await initScenario({
|
||||
customerId,
|
||||
setup: [s.deleteCustomer({ customerId })],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
const data = await autumnV1.customers.create({
|
||||
id: customerId,
|
||||
name: "Email Receipts Customer",
|
||||
email: `${customerId}@example.com`,
|
||||
send_email_receipts: true,
|
||||
});
|
||||
|
||||
expect(data.id).toBe(customerId);
|
||||
expect(data.send_email_receipts).toBe(true);
|
||||
});
|
||||
|
||||
test.concurrent(`${chalk.yellowBright("create: send_email_receipts defaults to false")}`, async () => {
|
||||
const customerId = "create-send-email-receipts-default";
|
||||
const { autumnV1 } = await initScenario({
|
||||
customerId,
|
||||
setup: [s.deleteCustomer({ customerId })],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
const data = await autumnV1.customers.create({
|
||||
id: customerId,
|
||||
name: "No Email Receipts Customer",
|
||||
email: `${customerId}@example.com`,
|
||||
});
|
||||
|
||||
expect(data.id).toBe(customerId);
|
||||
expect(data.send_email_receipts).toBe(false);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js";
|
||||
import chalk from "chalk";
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
// UPDATE CUSTOMER TESTS
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
test.concurrent(`${chalk.yellowBright("update: send_email_receipts can be updated")}`, async () => {
|
||||
const customerId = "update-send-email-receipts";
|
||||
const { autumnV1 } = await initScenario({
|
||||
customerId,
|
||||
setup: [s.deleteCustomer({ customerId })],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
// Create customer without send_email_receipts
|
||||
const createData = await autumnV1.customers.create({
|
||||
id: customerId,
|
||||
name: "Update Email Receipts Customer",
|
||||
email: `${customerId}@example.com`,
|
||||
});
|
||||
|
||||
expect(createData.send_email_receipts).toBe(false);
|
||||
|
||||
// Update to enable send_email_receipts
|
||||
const updateData = await autumnV1.customers.update(customerId, {
|
||||
send_email_receipts: true,
|
||||
});
|
||||
|
||||
expect(updateData.send_email_receipts).toBe(true);
|
||||
|
||||
// Verify by getting the customer
|
||||
const getData = (await autumnV1.customers.get(customerId)) as {
|
||||
send_email_receipts: boolean;
|
||||
};
|
||||
expect(getData.send_email_receipts).toBe(true);
|
||||
});
|
||||
|
||||
test.concurrent(`${chalk.yellowBright("update: send_email_receipts can be disabled")}`, async () => {
|
||||
const customerId = "update-send-email-receipts-disable";
|
||||
const { autumnV1 } = await initScenario({
|
||||
customerId,
|
||||
setup: [s.deleteCustomer({ customerId })],
|
||||
actions: [],
|
||||
});
|
||||
|
||||
// Create customer with send_email_receipts enabled
|
||||
const createData = await autumnV1.customers.create({
|
||||
id: customerId,
|
||||
name: "Disable Email Receipts Customer",
|
||||
email: `${customerId}@example.com`,
|
||||
send_email_receipts: true,
|
||||
});
|
||||
|
||||
expect(createData.send_email_receipts).toBe(true);
|
||||
|
||||
// Update to disable send_email_receipts
|
||||
const updateData = await autumnV1.customers.update(customerId, {
|
||||
send_email_receipts: false,
|
||||
});
|
||||
|
||||
expect(updateData.send_email_receipts).toBe(false);
|
||||
|
||||
// Verify by getting the customer
|
||||
const getData = (await autumnV1.customers.get(customerId)) as {
|
||||
send_email_receipts: boolean;
|
||||
};
|
||||
expect(getData.send_email_receipts).toBe(false);
|
||||
});
|
||||
@@ -2,7 +2,7 @@
|
||||
* Unit tests for sendEmailReceipt function.
|
||||
*
|
||||
* Tests the logic that sets receipt_email on PaymentIntent
|
||||
* based on customer's should_send_email_receipts flag.
|
||||
* based on customer's send_email_receipts flag.
|
||||
*/
|
||||
|
||||
import { describe, expect, test } from "bun:test";
|
||||
@@ -38,7 +38,7 @@ const createMockFullCustomer = (
|
||||
customer_products: [],
|
||||
entities: [],
|
||||
extra_customer_entitlements: [],
|
||||
should_send_email_receipts: true,
|
||||
send_email_receipts: true,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
@@ -81,13 +81,13 @@ describe(chalk.yellowBright("sendEmailReceipt"), () => {
|
||||
expect(mockCli._calls.paymentIntents.update).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("returns when should_send_email_receipts is false", async () => {
|
||||
test("returns when send_email_receipts is false", async () => {
|
||||
const mockCli = stripeClients.createMockStripeClient();
|
||||
const ctx = {
|
||||
stripeCli: mockCli,
|
||||
logger: createMockLogger(),
|
||||
fullCustomer: createMockFullCustomer({
|
||||
should_send_email_receipts: false,
|
||||
send_email_receipts: false,
|
||||
}),
|
||||
} as unknown as StripeWebhookContext;
|
||||
|
||||
@@ -100,13 +100,13 @@ describe(chalk.yellowBright("sendEmailReceipt"), () => {
|
||||
expect(mockCli._calls.paymentIntents.update).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("returns when should_send_email_receipts is undefined", async () => {
|
||||
test("returns when send_email_receipts is undefined", async () => {
|
||||
const mockCli = stripeClients.createMockStripeClient();
|
||||
const ctx = {
|
||||
stripeCli: mockCli,
|
||||
logger: createMockLogger(),
|
||||
fullCustomer: createMockFullCustomer({
|
||||
should_send_email_receipts: undefined as unknown as boolean,
|
||||
send_email_receipts: undefined as unknown as boolean,
|
||||
}),
|
||||
} as unknown as StripeWebhookContext;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ const create = ({
|
||||
customer_products: customerProducts,
|
||||
entities: [],
|
||||
extra_customer_entitlements: [],
|
||||
should_send_email_receipts: false,
|
||||
send_email_receipts: false,
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -108,6 +108,7 @@ type ScenarioConfig = {
|
||||
withDefault: boolean;
|
||||
defaultGroup?: string;
|
||||
skipWebhooks?: boolean;
|
||||
sendEmailReceipts?: boolean;
|
||||
products: ProductV2[];
|
||||
productPrefix?: string;
|
||||
entityConfig?: EntityConfig;
|
||||
@@ -146,10 +147,12 @@ const generateEntities = (config: EntityConfig): GeneratedEntity[] => {
|
||||
* @param withDefault - Attach the default product on creation (default: false)
|
||||
* @param defaultGroup - The product group to use for default product selection
|
||||
* @param skipWebhooks - Skip sending webhooks for this customer creation (default: undefined, uses server default)
|
||||
* @param send_email_receipts - Whether to send email receipts to the customer
|
||||
* @example s.customer({ paymentMethod: "success" })
|
||||
* @example s.customer({ paymentMethod: "success", data: { name: "Test" } })
|
||||
* @example s.customer({ withDefault: true, defaultGroup: "enterprise" })
|
||||
* @example s.customer({ withDefault: true, skipWebhooks: false }) // Enable webhooks for testing
|
||||
* @example s.customer({ paymentMethod: "success", send_email_receipts: true })
|
||||
*/
|
||||
const customer = ({
|
||||
testClock = true,
|
||||
@@ -158,6 +161,7 @@ const customer = ({
|
||||
withDefault,
|
||||
defaultGroup,
|
||||
skipWebhooks,
|
||||
send_email_receipts,
|
||||
}: {
|
||||
testClock?: boolean;
|
||||
paymentMethod?: "success" | "fail" | "authenticate";
|
||||
@@ -165,6 +169,7 @@ const customer = ({
|
||||
withDefault?: boolean;
|
||||
defaultGroup?: string;
|
||||
skipWebhooks?: boolean;
|
||||
send_email_receipts?: boolean;
|
||||
}): ConfigFn => {
|
||||
return (config) => ({
|
||||
...config,
|
||||
@@ -174,6 +179,7 @@ const customer = ({
|
||||
withDefault: withDefault ?? config.withDefault,
|
||||
defaultGroup: defaultGroup ?? config.defaultGroup,
|
||||
skipWebhooks: skipWebhooks ?? config.skipWebhooks,
|
||||
sendEmailReceipts: send_email_receipts ?? config.sendEmailReceipts,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -717,6 +723,7 @@ export async function initScenario({
|
||||
// Default group matches the product prefix (customerId) used in initProductsV0
|
||||
defaultGroup: config.defaultGroup ?? customerId,
|
||||
skipWebhooks: config.skipWebhooks,
|
||||
sendEmailReceipts: config.sendEmailReceipts,
|
||||
});
|
||||
testClockId = result.testClockId;
|
||||
customer = result.customer;
|
||||
|
||||
@@ -45,7 +45,7 @@ export const ExtCustomerDataSchema = z
|
||||
internal: true,
|
||||
description: "External processors for the customer",
|
||||
}),
|
||||
should_send_email_receipts: z.boolean().optional().meta({
|
||||
send_email_receipts: z.boolean().optional().meta({
|
||||
description: "Whether to send email receipts to this customer",
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -34,6 +34,7 @@ export const BaseApiCustomerSchema = z
|
||||
subscriptions: z.array(ApiSubscriptionSchema),
|
||||
scheduled_subscriptions: z.array(ApiSubscriptionSchema),
|
||||
balances: z.record(z.string(), ApiBalanceSchema),
|
||||
send_email_receipts: z.boolean(),
|
||||
})
|
||||
.meta({
|
||||
id: "BaseCustomer",
|
||||
|
||||
@@ -100,6 +100,7 @@ export const V1_2_CustomerChange = defineVersionChange({
|
||||
stripe_id: input.stripe_id,
|
||||
env: input.env,
|
||||
metadata: input.metadata,
|
||||
send_email_receipts: input.send_email_receipts,
|
||||
products: v3CusProducts,
|
||||
features: v3_features,
|
||||
|
||||
|
||||
@@ -173,6 +173,9 @@ export const ApiCustomerV3Schema = z.object({
|
||||
metadata: z.record(z.any(), z.any()).default({}).meta({
|
||||
description: cusDescriptions.metadata,
|
||||
}),
|
||||
send_email_receipts: z.boolean().default(false).meta({
|
||||
description: "Whether to send email receipts to the customer.",
|
||||
}),
|
||||
products: z.array(ApiCusProductV3Schema).meta({
|
||||
description: cusDescriptions.products,
|
||||
}),
|
||||
|
||||
@@ -16,7 +16,7 @@ export const CustomerSchema = z.object({
|
||||
processor: z.any(),
|
||||
processors: ExternalProcessorsSchema.nullish(),
|
||||
metadata: z.record(z.any(), z.any()).nullish().default({}),
|
||||
should_send_email_receipts: z.boolean().default(false),
|
||||
send_email_receipts: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export type Customer = z.infer<typeof CustomerSchema>;
|
||||
@@ -69,7 +69,7 @@ export const CreateCustomerSchema = z.object({
|
||||
metadata: z.record(z.any(), z.any()).default({}).nullish(),
|
||||
stripe_id: z.string().nullish(),
|
||||
processors: ExternalProcessorsSchema.nullish(),
|
||||
should_send_email_receipts: z.boolean().default(false),
|
||||
send_email_receipts: z.boolean().default(false),
|
||||
});
|
||||
|
||||
export type CreateCustomer = z.infer<typeof CreateCustomerSchema>;
|
||||
|
||||
@@ -34,9 +34,7 @@ export const customers = pgTable(
|
||||
processors: jsonb()
|
||||
.$type<ExternalProcessors>()
|
||||
.default({} as ExternalProcessors),
|
||||
should_send_email_receipts: boolean("should_send_email_receipts").default(
|
||||
false,
|
||||
),
|
||||
send_email_receipts: boolean("send_email_receipts").default(false),
|
||||
},
|
||||
(table) => [
|
||||
unique("cus_id_constraint").on(table.org_id, table.id, table.env),
|
||||
|
||||
Reference in New Issue
Block a user