feat(storage): MinIO object storage, Neon Auth, Supabase removal
Deploy to route.crispygoat.com / deploy (push) Failing after 3m1s

- Add MinIO/S3-compatible storage client (src/lib/storage.ts) with uploadObject,
  deleteObject, presigned URL helpers, and BUCKETS constant
- Wire product images, brand logos, and water log photos to MinIO via the
  new storage client
- Migrate forgot-password to Neon Auth (remove Supabase /auth/v1/recover call)
- Migrate send-scheduled cron to direct Postgres + Resend (remove Supabase Edge
  Function proxy)
- Add logoUrl to email types (OrderReceipt, Welcome, PasswordReset) and pass
  brand_settings.logo_url from all call sites
- Update email templates to use dynamic logoUrl instead of hardcoded Supabase
  bucket URLs
- Remove hardcoded Supabase URLs from TuxedoVideoHero, TuxedoAboutPage,
  TimeTrackingFieldClient; use brand_settings props + local public/ fallback
- Download brand logos (3) and tuxedo-hero.mp4 (36MB) from Supabase bucket to
  public/ for local development
- Add MinIO env vars to .env.example (endpoint, access key, secret, buckets)
- Fix TimeTrackingFieldClient to destructure logoUrl and brandAccent props
- Fix admin/users.ts logoUrl type (null → undefined for optional string)
- Remove stale sb- cookie from wholesale-auth
- Migrate tuxedo/about page to remove supabase import and use pool query for
  wholesale_settings lookup
This commit is contained in:
openclaw
2026-06-09 11:32:17 -06:00
parent bb464bda65
commit 916ad39176
349 changed files with 6706 additions and 3343 deletions
+13 -13
View File
@@ -3,7 +3,7 @@
import { and, desc, eq, SQL } from "drizzle-orm";
import { getAdminUser } from "@/lib/admin-permissions";
import { getActiveBrandId } from "@/lib/brand-scope";
import { withTenant, withPlatformAdmin } from "@/db/client";
import { withBrand, withPlatformAdmin } from "@/db/client";
import { campaigns, emailTemplates } from "@/db/schema";
export type CampaignType = "marketing" | "operational" | "transactional";
@@ -83,7 +83,7 @@ function stripHtml(html: string | null): string {
function rowToCampaign(c: CampaignRow, t?: TemplateRow | null): Campaign {
return {
id: c.id,
brand_id: c.tenantId,
brand_id: c.brandId,
name: c.name,
subject: t?.subject ?? null,
body_text: stripHtml(t?.bodyHtml ?? null),
@@ -111,7 +111,7 @@ export async function getCommunicationCampaigns(brandId?: string): Promise<ListC
try {
const rows = activeBrandId
? await withTenant(activeBrandId, (db) =>
? await withBrand(activeBrandId, (db) =>
db
.select({
campaign: campaigns,
@@ -178,7 +178,7 @@ export async function upsertCampaign(params: {
const subject = params.subject ?? "";
const bodyHtml = params.body_html ?? (params.body_text ? `<p style="white-space:pre-wrap">${escapeHtml(params.body_text)}</p>` : "<p></p>");
const { row, template } = await withTenant(params.brand_id, async (db) => {
const { row, template } = await withBrand(params.brand_id, async (db) => {
// Resolve / create the linked email_templates row.
let templateId: string | null = params.template_id ?? null;
let templateRow: TemplateRow | null = null;
@@ -187,7 +187,7 @@ export async function upsertCampaign(params: {
const existing = await db
.select()
.from(emailTemplates)
.where(and(eq(emailTemplates.id, templateId), eq(emailTemplates.tenantId, params.brand_id)))
.where(and(eq(emailTemplates.id, templateId), eq(emailTemplates.brandId, params.brand_id)))
.limit(1);
if (existing[0]) {
const updated = await db
@@ -207,7 +207,7 @@ export async function upsertCampaign(params: {
const inserted = await db
.insert(emailTemplates)
.values({
tenantId: params.brand_id,
brandId: params.brand_id,
name: params.name,
subject,
bodyHtml,
@@ -229,7 +229,7 @@ export async function upsertCampaign(params: {
scheduledFor: scheduled,
updatedAt: new Date(),
})
.where(and(eq(campaigns.id, params.id), eq(campaigns.tenantId, params.brand_id)))
.where(and(eq(campaigns.id, params.id), eq(campaigns.brandId, params.brand_id)))
.returning();
if (!updated[0]) {
return { row: null, template: null };
@@ -239,7 +239,7 @@ export async function upsertCampaign(params: {
const inserted = await db
.insert(campaigns)
.values({
tenantId: params.brand_id,
brandId: params.brand_id,
name: params.name,
templateId,
status,
@@ -274,8 +274,8 @@ export async function deleteCampaign(campaignId: string, brandId?: string): Prom
try {
if (activeBrandId) {
await withTenant(activeBrandId, (db) =>
db.delete(campaigns).where(and(eq(campaigns.id, campaignId), eq(campaigns.tenantId, activeBrandId))),
await withBrand(activeBrandId, (db) =>
db.delete(campaigns).where(and(eq(campaigns.id, campaignId), eq(campaigns.brandId, activeBrandId))),
);
} else {
await withPlatformAdmin((db) => db.delete(campaigns).where(eq(campaigns.id, campaignId)));
@@ -297,12 +297,12 @@ export async function getCampaignById(campaignId: string, brandId?: string): Pro
try {
const result = activeBrandId
? await withTenant(activeBrandId, (db) =>
? await withBrand(activeBrandId, (db) =>
db
.select({ campaign: campaigns, template: emailTemplates })
.from(campaigns)
.leftJoin(emailTemplates, eq(emailTemplates.id, campaigns.templateId))
.where(and(eq(campaigns.id, campaignId), eq(campaigns.tenantId, activeBrandId)))
.where(and(eq(campaigns.id, campaignId), eq(campaigns.brandId, activeBrandId)))
.limit(1)
.then((r) => r[0] ?? null),
)
@@ -318,7 +318,7 @@ export async function getCampaignById(campaignId: string, brandId?: string): Pro
if (!result) return null;
if (adminUser.role === "brand_admin" && result.campaign.tenantId !== adminUser.brand_id) {
if (adminUser.role === "brand_admin" && result.campaign.brandId !== adminUser.brand_id) {
return null;
}
+37 -45
View File
@@ -4,7 +4,7 @@ import { and, eq, ilike, or, sql, SQL } from "drizzle-orm";
import { getAdminUser } from "@/lib/admin-permissions";
import { parseCSVWithLimits } from "@/lib/csv-parser";
import { buildImportPreview } from "@/lib/column-detector";
import { withTenant, withPlatformAdmin } from "@/db/client";
import { withBrand, withPlatformAdmin } from "@/db/client";
import { customers } from "@/db/schema";
export type ContactSource = "order" | "import" | "manual" | "admin";
@@ -19,7 +19,7 @@ export type ContactSource = "order" | "import" | "manual" | "admin";
*/
export type Contact = {
id: string;
tenant_id: string;
brand_id: string;
email: string | null;
phone: string | null;
full_name: string;
@@ -101,23 +101,9 @@ export type GetContactsResult = {
};
function rowToContact(row: typeof customers.$inferSelect): Contact {
// Derive first/last name from the stored single `name` column.
// Multi-word names split on the first whitespace; single-word names
// populate first_name and leave last_name null.
const fullName = row.name ?? "";
const trimmed = fullName.trim();
let firstName: string | null = null;
let lastName: string | null = null;
if (trimmed) {
const idx = trimmed.indexOf(" ");
if (idx === -1) {
firstName = trimmed;
} else {
firstName = trimmed.slice(0, idx);
const rest = trimmed.slice(idx + 1).trim();
lastName = rest.length > 0 ? rest : null;
}
}
const firstName = row.firstName;
const lastName = row.lastName;
const fullName = [firstName, lastName].filter(Boolean).join(" ") || "";
// Derive unsubscribed_at: the new schema only has opt-in flags, not
// a timestamp. Mark the unsubscribe moment as updatedAt if opted out
@@ -126,10 +112,10 @@ function rowToContact(row: typeof customers.$inferSelect): Contact {
return {
id: row.id,
tenant_id: row.tenantId,
brand_id: row.brandId,
email: row.email,
phone: row.phone,
full_name: row.name,
full_name: row.fullName ?? "",
first_name: firstName,
last_name: lastName,
source: "manual",
@@ -162,13 +148,13 @@ export async function getContacts(params: {
const search = params.search?.trim() ?? "";
try {
const conds: SQL[] = [eq(customers.tenantId, params.brandId)];
const conds: SQL[] = [eq(customers.brandId, params.brandId)];
if (search) {
const like = `%${search}%`;
conds.push(or(ilike(customers.name, like), ilike(customers.email, like))!);
conds.push(or(ilike(customers.firstName, like), ilike(customers.email, like))!);
}
const rows = await withTenant(params.brandId, async (db) => {
const rows = await withBrand(params.brandId, async (db) => {
const [items, countRows] = await Promise.all([
db
.select()
@@ -233,7 +219,7 @@ export async function upsertContact(contact: {
return { success: false, error: "Not authorized for this brand" };
}
const name =
const fullName =
contact.full_name?.trim() ||
[contact.first_name, contact.last_name].filter(Boolean).join(" ").trim() ||
contact.email ||
@@ -243,18 +229,20 @@ export async function upsertContact(contact: {
try {
if (contact.id) {
const contactId = contact.id;
const updated = await withTenant(contact.brand_id, (db) =>
const updated = await withBrand(contact.brand_id, (db) =>
db
.update(customers)
.set({
name,
fullName,
email: contact.email ?? null,
phone: contact.phone ?? null,
firstName: contact.first_name ?? null,
lastName: contact.last_name ?? null,
smsOptIn: contact.sms_opt_in ?? false,
emailOptIn: contact.email_opt_in ?? true,
updatedAt: new Date(),
})
.where(and(eq(customers.id, contactId), eq(customers.tenantId, contact.brand_id)))
.where(and(eq(customers.id, contactId), eq(customers.brandId, contact.brand_id)))
.returning(),
);
const row = updated[0];
@@ -262,23 +250,25 @@ export async function upsertContact(contact: {
return { success: true, contact: rowToContact(row) };
}
// INSERT — de-dupe on (tenant_id, email) when email is provided
// INSERT — de-dupe on (brand_id, email) when email is provided
const contactEmail = contact.email;
if (contactEmail) {
const existing = await withTenant(contact.brand_id, (db) =>
const existing = await withBrand(contact.brand_id, (db) =>
db
.select()
.from(customers)
.where(and(eq(customers.email, contactEmail), eq(customers.tenantId, contact.brand_id)))
.where(and(eq(customers.email, contactEmail), eq(customers.brandId, contact.brand_id)))
.limit(1),
);
if (existing[0]) {
const updated = await withTenant(contact.brand_id, (db) =>
const updated = await withBrand(contact.brand_id, (db) =>
db
.update(customers)
.set({
name,
fullName,
phone: contact.phone ?? null,
firstName: contact.first_name ?? null,
lastName: contact.last_name ?? null,
smsOptIn: contact.sms_opt_in ?? existing[0].smsOptIn,
emailOptIn: contact.email_opt_in ?? existing[0].emailOptIn,
updatedAt: new Date(),
@@ -292,14 +282,16 @@ export async function upsertContact(contact: {
}
}
const inserted = await withTenant(contact.brand_id, (db) =>
const inserted = await withBrand(contact.brand_id, (db) =>
db
.insert(customers)
.values({
tenantId: contact.brand_id,
name,
brandId: contact.brand_id,
fullName,
email: contact.email ?? null,
phone: contact.phone ?? null,
firstName: contact.first_name ?? null,
lastName: contact.last_name ?? null,
smsOptIn: contact.sms_opt_in ?? false,
emailOptIn: contact.email_opt_in ?? true,
})
@@ -328,7 +320,7 @@ export type ImportContactsResult = {
* The legacy `import_communication_contacts_batch` RPC is replaced with
* an in-process batch: parse → dedupe → upsert per row, returning the
* same ImportResult shape. This avoids a round-trip to the DB for each
* row and keeps the call inside the `withTenant` transaction.
* row and keeps the call inside the `withBrand` transaction.
*/
export async function importContactsBatch(params: {
brandId: string;
@@ -403,14 +395,14 @@ export async function optOutContact(params: {
}
try {
await withTenant(params.brandId, (db) =>
await withBrand(params.brandId, (db) =>
db
.update(customers)
.set({
...(params.method === "email" ? { emailOptIn: false } : { smsOptIn: false }),
updatedAt: new Date(),
})
.where(and(eq(customers.email, params.email), eq(customers.tenantId, params.brandId))),
.where(and(eq(customers.email, params.email), eq(customers.brandId, params.brandId))),
);
return { success: true };
} catch (err) {
@@ -434,10 +426,10 @@ export async function deleteContact(id: string, brandId?: string): Promise<{ suc
try {
if (brandId) {
await withTenant(brandId, (db) =>
await withBrand(brandId, (db) =>
db
.delete(customers)
.where(and(eq(customers.id, id), eq(customers.tenantId, brandId))),
.where(and(eq(customers.id, id), eq(customers.brandId, brandId))),
);
} else {
// platform_admin fallback — by id only
@@ -492,20 +484,20 @@ export async function exportContacts(params: {
try {
while (true) {
const rows = await withTenant(effectiveBrandId, (db) =>
const rows = await withBrand(effectiveBrandId, (db) =>
db
.select()
.from(customers)
.where(
params.search
? and(
eq(customers.tenantId, effectiveBrandId),
eq(customers.brandId, effectiveBrandId),
or(
ilike(customers.name, `%${params.search}%`),
ilike(customers.firstName, `%${params.search}%`),
ilike(customers.email, `%${params.search}%`),
),
)
: eq(customers.tenantId, effectiveBrandId),
: eq(customers.brandId, effectiveBrandId),
)
.limit(batchSize)
.offset(offset)
@@ -2,7 +2,7 @@
import { and, desc, eq } from "drizzle-orm";
import { getAdminUser } from "@/lib/admin-permissions";
import { withTenant, withPlatformAdmin } from "@/db/client";
import { withBrand, withPlatformAdmin } from "@/db/client";
import { files } from "@/db/schema";
import {
importContactsBatch,
@@ -45,11 +45,11 @@ export async function uploadContactsToBucket(
const storageKey = `imports/${brandId}/${timestamp}-${safeName}`;
try {
const [row] = await withTenant(brandId, (db) =>
const [row] = await withBrand(brandId, (db) =>
db
.insert(files)
.values({
tenantId: brandId,
brandId: brandId,
storageKey,
mimeType: "text/csv",
sizeBytes: file.size,
@@ -130,7 +130,7 @@ export async function listImportHistory(
if (!adminUser) return { success: false, error: "Not authenticated" };
try {
const rows = await withTenant(brandId, (db) =>
const rows = await withBrand(brandId, (db) =>
db
.select({
storageKey: files.storageKey,
@@ -138,7 +138,7 @@ export async function listImportHistory(
createdAt: files.createdAt,
})
.from(files)
.where(and(eq(files.tenantId, brandId), eq(files.purpose, "contact_import")))
.where(and(eq(files.brandId, brandId), eq(files.purpose, "contact_import")))
.orderBy(desc(files.createdAt))
.limit(limit),
);
+6 -6
View File
@@ -2,7 +2,7 @@
import { eq } from "drizzle-orm";
import { getAdminUser } from "@/lib/admin-permissions";
import { withTenant } from "@/db/client";
import { withBrand } from "@/db/client";
import { brandSettings } from "@/db/schema";
import type { AudienceRules } from "./campaigns";
@@ -56,22 +56,22 @@ function isSegment(v: unknown): v is Segment {
}
async function loadSegments(brandId: string): Promise<Segment[]> {
const rows = await withTenant(brandId, (db) =>
const rows = await withBrand(brandId, (db) =>
db
.select({ flags: brandSettings.featureFlags })
.from(brandSettings)
.where(eq(brandSettings.tenantId, brandId))
.where(eq(brandSettings.brandId, brandId))
.limit(1),
);
return readSegments((rows[0]?.flags ?? null) as Record<string, unknown> | null);
}
async function saveSegments(brandId: string, segments: Segment[]): Promise<void> {
await withTenant(brandId, async (db) => {
await withBrand(brandId, async (db) => {
const existing = await db
.select({ flags: brandSettings.featureFlags })
.from(brandSettings)
.where(eq(brandSettings.tenantId, brandId))
.where(eq(brandSettings.brandId, brandId))
.limit(1);
const baseFlags = (existing[0]?.flags ?? {}) as Record<string, unknown>;
const nextFlags: Record<string, unknown> = {
@@ -81,7 +81,7 @@ async function saveSegments(brandId: string, segments: Segment[]): Promise<void>
await db
.update(brandSettings)
.set({ featureFlags: nextFlags, updatedAt: new Date() })
.where(eq(brandSettings.tenantId, brandId));
.where(eq(brandSettings.brandId, brandId));
});
}
+10 -10
View File
@@ -3,13 +3,13 @@
import { and, eq, sql, SQL } from "drizzle-orm";
import { getAdminUser } from "@/lib/admin-permissions";
import { getActiveBrandId } from "@/lib/brand-scope";
import { withTenant, withPlatformAdmin } from "@/db/client";
import { withBrand, withPlatformAdmin } from "@/db/client";
import { campaigns, customers } from "@/db/schema";
import type { AudienceRules } from "./campaigns";
export type AudiencePreviewResult = {
count: number;
sample_customers: { id: string; email: string; name: string }[];
sample_customers: { id: string; email: string; fullName: string | null }[];
};
/**
@@ -36,23 +36,23 @@ export async function previewCampaignAudience(
}
try {
const rows = await withTenant(brandId, (db) =>
const rows = await withBrand(brandId, (db) =>
db
.select({
id: customers.id,
email: customers.email,
name: customers.name,
fullName: customers.fullName,
})
.from(customers)
.where(and(eq(customers.tenantId, brandId), eq(customers.emailOptIn, true)))
.where(and(eq(customers.brandId, brandId), eq(customers.emailOptIn, true)))
.limit(5),
);
const countRows = await withTenant(brandId, (db) =>
const countRows = await withBrand(brandId, (db) =>
db
.select({ value: sql<number>`count(*)::int` })
.from(customers)
.where(and(eq(customers.tenantId, brandId), eq(customers.emailOptIn, true))),
.where(and(eq(customers.brandId, brandId), eq(customers.emailOptIn, true))),
);
return {
@@ -60,7 +60,7 @@ export async function previewCampaignAudience(
sample_customers: rows.map((r) => ({
id: r.id,
email: r.email ?? "",
name: r.name,
fullName: r.fullName,
})),
};
} catch {
@@ -153,11 +153,11 @@ export async function sendCampaign(campaignId: string, brandId?: string): Promis
}
const conds: SQL[] = [eq(campaigns.id, campaignId)];
if (activeBrandId) conds.push(eq(campaigns.tenantId, activeBrandId));
if (activeBrandId) conds.push(eq(campaigns.brandId, activeBrandId));
try {
const updated = activeBrandId
? await withTenant(activeBrandId, (db) =>
? await withBrand(activeBrandId, (db) =>
db
.update(campaigns)
.set({ status: "sent", sentAt: new Date() })
+12 -12
View File
@@ -1,7 +1,7 @@
"use server";
import { getAdminUser } from "@/lib/admin-permissions";
import { withTenant } from "@/db/client";
import { withBrand } from "@/db/client";
import { brandSettings } from "@/db/schema";
import { eq } from "drizzle-orm";
@@ -47,15 +47,15 @@ function readFlag(
export async function getCommunicationSettings(brandId: string): Promise<CommunicationSettings | null> {
try {
const rows = await withTenant(brandId, (db) =>
const rows = await withBrand(brandId, (db) =>
db
.select({
tenantId: brandSettings.tenantId,
brandId: brandSettings.brandId,
featureFlags: brandSettings.featureFlags,
updatedAt: brandSettings.updatedAt,
})
.from(brandSettings)
.where(eq(brandSettings.tenantId, brandId))
.where(eq(brandSettings.brandId, brandId))
.limit(1),
);
@@ -65,8 +65,8 @@ export async function getCommunicationSettings(brandId: string): Promise<Communi
const flags = (row.featureFlags ?? {}) as Record<string, unknown>;
return {
id: row.tenantId,
brand_id: row.tenantId,
id: row.brandId,
brand_id: row.brandId,
default_sender_email: readFlag(flags, "comm_default_sender_email"),
default_sender_name: readFlag(flags, "comm_default_sender_name"),
reply_to_email: readFlag(flags, "comm_reply_to_email"),
@@ -96,11 +96,11 @@ export async function upsertCommunicationSettings(params: {
}
try {
const updated = await withTenant(params.brand_id, async (db) => {
const updated = await withBrand(params.brand_id, async (db) => {
const existing = await db
.select({ flags: brandSettings.featureFlags })
.from(brandSettings)
.where(eq(brandSettings.tenantId, params.brand_id))
.where(eq(brandSettings.brandId, params.brand_id))
.limit(1);
const baseFlags = (existing[0]?.flags ?? {}) as Record<string, unknown>;
const nextFlags: Record<string, unknown> = {
@@ -115,9 +115,9 @@ export async function upsertCommunicationSettings(params: {
const result = await db
.update(brandSettings)
.set({ featureFlags: nextFlags, updatedAt: new Date() })
.where(eq(brandSettings.tenantId, params.brand_id))
.where(eq(brandSettings.brandId, params.brand_id))
.returning({
tenantId: brandSettings.tenantId,
brandId: brandSettings.brandId,
updatedAt: brandSettings.updatedAt,
});
return result[0] ?? null;
@@ -128,8 +128,8 @@ export async function upsertCommunicationSettings(params: {
}
const settings: CommunicationSettings = {
id: updated.tenantId,
brand_id: updated.tenantId,
id: updated.brandId,
brand_id: updated.brandId,
default_sender_email: params.sender_email ?? null,
default_sender_name: params.sender_name ?? null,
reply_to_email: params.reply_to_email ?? null,
+7 -7
View File
@@ -2,7 +2,7 @@
import { and, eq, sql, SQL } from "drizzle-orm";
import { getAdminUser } from "@/lib/admin-permissions";
import { withTenant } from "@/db/client";
import { withBrand } from "@/db/client";
import { customers, orders, campaigns, emailTemplates } from "@/db/schema";
export type StopBlastResult =
@@ -37,8 +37,8 @@ export async function sendStopBlast(params: {
}
try {
const conds: SQL[] = [eq(customers.tenantId, params.brandId)];
const recipientRows = await withTenant(params.brandId, async (db) => {
const conds: SQL[] = [eq(customers.brandId, params.brandId)];
const recipientRows = await withBrand(params.brandId, async (db) => {
// Distinct customers from the tenant's recent orders.
const orderCustomers = await db
.selectDistinct({ id: customers.id })
@@ -46,7 +46,7 @@ export async function sendStopBlast(params: {
.innerJoin(orders, eq(orders.customerId, customers.id))
.where(
and(
eq(orders.tenantId, params.brandId),
eq(orders.brandId, params.brandId),
params.channel === "sms"
? eq(customers.smsOptIn, true)
: params.channel === "email"
@@ -81,11 +81,11 @@ export async function sendStopBlast(params: {
// Persist a draft campaign for traceability. No template link — the
// stop-blast content is supplied inline and not yet modeled.
const bodyHtml = `<p style="white-space:pre-wrap">${escapeHtml(params.body)}</p>`;
const inserted = await withTenant(params.brandId, async (db) => {
const inserted = await withBrand(params.brandId, async (db) => {
const template = await db
.insert(emailTemplates)
.values({
tenantId: params.brandId,
brandId: params.brandId,
name: `Stop blast ${new Date().toISOString()}`,
subject: params.subject ?? "Pickup update",
bodyHtml,
@@ -96,7 +96,7 @@ export async function sendStopBlast(params: {
const campaign = await db
.insert(campaigns)
.values({
tenantId: params.brandId,
brandId: params.brandId,
name: `Stop blast ${new Date().toISOString()}`,
templateId: tplId,
status: "sent",
+6 -6
View File
@@ -2,7 +2,7 @@
import { and, desc, eq, isNotNull } from "drizzle-orm";
import { getAdminUser } from "@/lib/admin-permissions";
import { withTenant } from "@/db/client";
import { withBrand } from "@/db/client";
import { customers, orders, campaigns } from "@/db/schema";
/**
@@ -57,13 +57,13 @@ export async function getStopMessagingData(params: {
}
try {
const [orderRows, campaignRows] = await withTenant(brandId, async (db) => {
const [orderRows, campaignRows] = await withBrand(brandId, async (db) => {
const o = await db
.select({
id: orders.id,
status: orders.status,
placedAt: orders.placedAt,
customerName: customers.name,
customerName: customers.fullName,
customerEmail: customers.email,
customerPhone: customers.phone,
})
@@ -71,7 +71,7 @@ export async function getStopMessagingData(params: {
.innerJoin(customers, eq(customers.id, orders.customerId))
.where(
and(
eq(orders.tenantId, brandId),
eq(orders.brandId, brandId),
isNotNull(customers.email),
),
)
@@ -86,7 +86,7 @@ export async function getStopMessagingData(params: {
updatedAt: campaigns.updatedAt,
})
.from(campaigns)
.where(eq(campaigns.tenantId, brandId))
.where(eq(campaigns.brandId, brandId))
.orderBy(desc(campaigns.sentAt), desc(campaigns.updatedAt))
.limit(10);
@@ -97,7 +97,7 @@ export async function getStopMessagingData(params: {
// schema; treat "fulfilled" status as picked up).
const mappedOrders: StopOrder[] = orderRows.map((o) => ({
id: o.id,
customer_name: o.customerName,
customer_name: o.customerName ?? "",
customer_email: o.customerEmail,
customer_phone: o.customerPhone,
pickup_complete: o.status === "fulfilled",
+11 -11
View File
@@ -3,7 +3,7 @@
import { and, eq } from "drizzle-orm";
import { getAdminUser } from "@/lib/admin-permissions";
import { getActiveBrandId } from "@/lib/brand-scope";
import { withTenant, withPlatformAdmin } from "@/db/client";
import { withBrand, withPlatformAdmin } from "@/db/client";
import { emailTemplates } from "@/db/schema";
export type TemplateType = "email_template" | "sms_template" | "internal_note";
@@ -20,7 +20,7 @@ export type CampaignType = "marketing" | "operational" | "transactional";
*/
export type Template = {
id: string;
tenant_id: string;
brand_id: string;
name: string;
subject: string;
body_text: string;
@@ -42,7 +42,7 @@ export type UpsertTemplateResult = {
function rowToTemplate(row: typeof emailTemplates.$inferSelect): Template {
return {
id: row.id,
tenant_id: row.tenantId,
brand_id: row.brandId,
name: row.name,
subject: row.subject,
body_text: stripHtml(row.bodyHtml),
@@ -79,7 +79,7 @@ export async function getCommunicationTemplates(brandId?: string): Promise<{ suc
try {
const rows = activeBrandId
? await withTenant(activeBrandId, (db) =>
? await withBrand(activeBrandId, (db) =>
db
.select()
.from(emailTemplates)
@@ -121,7 +121,7 @@ export async function upsertTemplate(params: {
try {
const row = params.id
? await withTenant(params.brand_id, async (db) => {
? await withBrand(params.brand_id, async (db) => {
const updated = await db
.update(emailTemplates)
.set({
@@ -133,17 +133,17 @@ export async function upsertTemplate(params: {
.where(
and(
eq(emailTemplates.id, params.id!),
eq(emailTemplates.tenantId, params.brand_id),
eq(emailTemplates.brandId, params.brand_id),
),
)
.returning();
return updated[0] ?? null;
})
: await withTenant(params.brand_id, async (db) => {
: await withBrand(params.brand_id, async (db) => {
const inserted = await db
.insert(emailTemplates)
.values({
tenantId: params.brand_id,
brandId: params.brand_id,
name: params.name,
subject: params.subject,
bodyHtml,
@@ -170,14 +170,14 @@ export async function getTemplateById(templateId: string): Promise<Template | nu
try {
const row = activeBrandId
? await withTenant(activeBrandId, (db) =>
? await withBrand(activeBrandId, (db) =>
db
.select()
.from(emailTemplates)
.where(
and(
eq(emailTemplates.id, templateId),
eq(emailTemplates.tenantId, activeBrandId),
eq(emailTemplates.brandId, activeBrandId),
),
)
.limit(1)
@@ -194,7 +194,7 @@ export async function getTemplateById(templateId: string): Promise<Template | nu
if (!row) return null;
if (adminUser.role === "brand_admin" && row.tenantId !== adminUser.brand_id) {
if (adminUser.role === "brand_admin" && row.brandId !== adminUser.brand_id) {
return null;
}