616 lines
20 KiB
TypeScript
616 lines
20 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import bcrypt from "bcryptjs";
|
|
import { TenantUserJoined } from "~/application/enums/tenants/TenantUserJoined";
|
|
import { TenantUserType } from "~/application/enums/tenants/TenantUserType";
|
|
import { TenantUserStatus } from "~/application/enums/tenants/TenantUserStatus";
|
|
import { getAvailableTenantInboundAddress } from "~/utils/services/emailService";
|
|
import { getEntitlementPlanStripeId, seedStandardEntitlements, type EntitlementsSeedDiagnosticEvent } from "@toolctrl/db/entitlements-seed.server";
|
|
import { runDbSeed, type DbSeedDiagnosticEvent } from "@toolctrl/db/seed.server";
|
|
import { entitlementPlanIds, featureKeys, productBlueprintIds, type EntitlementPlanId } from "@toolctrl/entitlements";
|
|
import { seedRolesAndPermissions, type RolesAndPermissionsSeedDiagnosticEvent } from "~/utils/services/rolesAndPermissionsService";
|
|
import { db } from "~/utils/db.server";
|
|
import { ensureProductCoreCrmEntities } from "~/modules/crm/services/ProductCoreCrmEntities.server";
|
|
import { createScopedLogger } from "~/utils/logger.server";
|
|
|
|
const seedLogger = createScopedLogger("seed");
|
|
|
|
/**
|
|
* Hashes an email (lower-cased + trimmed) so seed logs cannot leak the
|
|
* actual address but operators can still correlate diagnostics.
|
|
*/
|
|
function emailFingerprint(email: string): string {
|
|
return createHash("sha256").update(email.trim().toLowerCase(), "utf8").digest("hex").slice(0, 12);
|
|
}
|
|
|
|
const DEFAULT_ADMIN_EMAIL = "admin@email.com";
|
|
const DEFAULT_ADMIN_PASSWORD = "password";
|
|
const localDemoSeedUsers = [
|
|
{
|
|
userType: "app" as const,
|
|
firstName: "John",
|
|
lastName: "Doe",
|
|
email: "john.doe@company.com",
|
|
password: "password",
|
|
},
|
|
{
|
|
userType: "app" as const,
|
|
firstName: "Luna",
|
|
lastName: "Davis",
|
|
email: "luna.davis@company.com",
|
|
password: "password",
|
|
},
|
|
];
|
|
const demoCrmTenants = [
|
|
{ slug: "demo-starter-crm", name: "Demo Starter CRM", planId: entitlementPlanIds.starter },
|
|
{ slug: "demo-pro-crm", name: "Demo Pro CRM", planId: entitlementPlanIds.pro },
|
|
{ slug: "demo-business-crm", name: "Demo Business CRM", planId: entitlementPlanIds.business },
|
|
] as const;
|
|
const demoTenantRoleSeeds = [
|
|
{
|
|
name: "CRM Manager",
|
|
description: "Owns CRM records, imports, and tenant team coordination in the demo workspace.",
|
|
permissions: ["app.dashboard.view", "app.settings.members.view", "app.settings.roles.view"],
|
|
},
|
|
{
|
|
name: "Support Agent",
|
|
description: "Handles tenant support tickets and reads the tenant knowledge base in the demo workspace.",
|
|
permissions: ["app.dashboard.view"],
|
|
},
|
|
] as const;
|
|
const demoKnowledgeBaseCategoryTitle = "Product Core";
|
|
const demoKnowledgeBaseCategoryDescription = "Plan gates, tenant roles, support, and CRM SaaS setup.";
|
|
const demoKnowledgeBaseArticleTitle = "Plan gates overview";
|
|
const demoSupportTicketSubjects = {
|
|
welcome: "Welcome to the CRM SaaS demo workspace",
|
|
planGates: "Verify plan gates and tenant roles",
|
|
} as const;
|
|
const demoSupportTicketPlanGatesMessage =
|
|
"Use this ticket to confirm blueprint, plan, tenant override, and tenant-owned role access in the seeded workspace.";
|
|
|
|
function getAdminSeedUser() {
|
|
return {
|
|
email: process.env.SEED_ADMIN_EMAIL ?? DEFAULT_ADMIN_EMAIL,
|
|
password: process.env.SEED_ADMIN_PASSWORD ?? DEFAULT_ADMIN_PASSWORD,
|
|
firstName: "Admin",
|
|
lastName: "User",
|
|
};
|
|
}
|
|
|
|
async function seed() {
|
|
const adminUser = getAdminSeedUser();
|
|
const shouldSeedDemoData = shouldSeedCrmDemoData();
|
|
const seedCredentials = shouldSeedDemoData
|
|
? [
|
|
{ label: "admin", email: adminUser.email, password: adminUser.password },
|
|
...localDemoSeedUsers.map((user) => ({ label: user.email, email: user.email, password: user.password })),
|
|
]
|
|
: [{ label: "admin", email: adminUser.email, password: adminUser.password }];
|
|
|
|
return runDbSeed({
|
|
nodeEnv: getSeedSafetyEnvironment(),
|
|
adminEmail: adminUser.email,
|
|
adminPassword: adminUser.password,
|
|
seedCredentials,
|
|
diagnostics: logDbSeedDiagnostic,
|
|
seed: async () => {
|
|
const admin = await createUser({ userType: "admin", ...adminUser });
|
|
let demoUsers: Awaited<ReturnType<typeof createUser>>[] = [];
|
|
|
|
if (shouldSeedDemoData) {
|
|
seedLogger.info({ op: "seed.createDemoUsers", count: localDemoSeedUsers.length }, "Creating CRM SaaS demo users with tenants");
|
|
demoUsers = await Promise.all(localDemoSeedUsers.map((user) => createUser(user)));
|
|
const [user1, user2] = demoUsers;
|
|
|
|
await Promise.all(
|
|
demoCrmTenants.map((tenant, index) =>
|
|
createTenant(tenant.slug, tenant.name, [
|
|
{ ...admin, type: TenantUserType.ADMIN },
|
|
{ ...user1, type: index === 0 ? TenantUserType.ADMIN : TenantUserType.OWNER },
|
|
{ ...user2, type: TenantUserType.MEMBER },
|
|
])
|
|
)
|
|
);
|
|
}
|
|
|
|
if (shouldSeedDemoData) {
|
|
await ensureProductCoreCrmEntities({ ensurePermissions: false });
|
|
}
|
|
|
|
// Permissions
|
|
await seedRolesAndPermissions({
|
|
adminEmail: adminUser.email,
|
|
diagnostics: logRolesAndPermissionsSeedDiagnostic,
|
|
});
|
|
|
|
await seedStandardEntitlements({
|
|
client: db,
|
|
diagnostics: logEntitlementsSeedDiagnostic,
|
|
});
|
|
|
|
if (shouldSeedDemoData) {
|
|
await Promise.all(demoCrmTenants.map((tenant) => assignTenantPlan(tenant.slug, tenant.planId)));
|
|
await Promise.all(demoCrmTenants.map((tenant) => seedDemoProductCoreTenant(tenant.slug, tenant.planId, demoUsers)));
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
function shouldSeedCrmDemoData() {
|
|
if (process.env.SEED_DEMO_DATA !== "true") {
|
|
return false;
|
|
}
|
|
if (process.env.NODE_ENV !== "production") {
|
|
return true;
|
|
}
|
|
if (process.env.ALLOW_STAGING_DEMO_SEED === "true") {
|
|
return true;
|
|
}
|
|
throw new Error("Refusing to seed demo data in production without ALLOW_STAGING_DEMO_SEED=true.");
|
|
}
|
|
|
|
function getSeedSafetyEnvironment() {
|
|
if (process.env.NODE_ENV === "production" && process.env.SEED_DEMO_DATA === "true" && process.env.ALLOW_STAGING_DEMO_SEED === "true") {
|
|
return "staging-demo";
|
|
}
|
|
return process.env.NODE_ENV ?? "development";
|
|
}
|
|
|
|
function logDbSeedDiagnostic(event: DbSeedDiagnosticEvent) {
|
|
const { phase, nodeEnv, adminEmail } = event;
|
|
// Never log raw adminEmail (PII). Use fingerprint hash for correlation.
|
|
seedLogger.info(
|
|
{ op: "db-seed", phase, nodeEnv, adminEmailFingerprint: adminEmail ? emailFingerprint(adminEmail) : null },
|
|
"DB seed event",
|
|
);
|
|
}
|
|
|
|
function logRolesAndPermissionsSeedDiagnostic(event: RolesAndPermissionsSeedDiagnosticEvent) {
|
|
const {
|
|
phase,
|
|
adminRoleCount,
|
|
appRoleCount,
|
|
defaultPermissionCount,
|
|
dynamicEntityPermissionCount,
|
|
adminUserAssignmentCount,
|
|
tenantUserAssignmentCount,
|
|
error,
|
|
} = event;
|
|
seedLogger.info(
|
|
{
|
|
op: "seed.rolesAndPermissions",
|
|
phase,
|
|
adminRoleCount,
|
|
appRoleCount,
|
|
defaultPermissionCount,
|
|
dynamicEntityPermissionCount,
|
|
adminUserAssignmentCount,
|
|
tenantUserAssignmentCount,
|
|
...(error ? { err: error } : {}),
|
|
},
|
|
"Roles and permissions seed event",
|
|
);
|
|
}
|
|
|
|
function logEntitlementsSeedDiagnostic(event: EntitlementsSeedDiagnosticEvent) {
|
|
const { phase, planCount, featureCount, createdProducts, updatedProducts, deletedFeatures, createdFeatures, planId, error } = event;
|
|
seedLogger.info(
|
|
{
|
|
op: "seed.entitlements",
|
|
phase,
|
|
planCount,
|
|
featureCount,
|
|
createdProducts,
|
|
updatedProducts,
|
|
deletedFeatures,
|
|
createdFeatures,
|
|
...(planId ? { planId } : {}),
|
|
...(error ? { err: error } : {}),
|
|
},
|
|
"Entitlements seed event",
|
|
);
|
|
}
|
|
|
|
async function createUser({
|
|
firstName,
|
|
lastName,
|
|
email,
|
|
password,
|
|
userType,
|
|
}: {
|
|
firstName: string;
|
|
lastName: string;
|
|
email: string;
|
|
password: string;
|
|
userType: "admin" | "app";
|
|
}) {
|
|
// Never log raw `email` (PII) and never log `password` (secret).
|
|
// Use email fingerprint hash so operators can still correlate seed events.
|
|
seedLogger.info(
|
|
{ op: "seed.createUser", userType, emailFingerprint: emailFingerprint(email) },
|
|
userType === "admin" ? "Seeding admin user" : "Seeding app user",
|
|
);
|
|
const salt = await bcrypt.genSalt(10);
|
|
const passwordHash = await bcrypt.hash(password, salt);
|
|
let user = await db.user.findUnique({
|
|
where: { email },
|
|
});
|
|
if (user) {
|
|
const isAdmin = await db.adminUser.findUnique({ where: { userId: user.id } });
|
|
if (!isAdmin && userType === "admin") {
|
|
if (process.env.NODE_ENV === "production") {
|
|
throw new Error("Production seed refused to promote an existing non-admin user. Choose a new SEED_ADMIN_EMAIL or bootstrap the admin user explicitly.");
|
|
}
|
|
seedLogger.info(
|
|
{ op: "seed.promoteUserToAdmin", emailFingerprint: emailFingerprint(email) },
|
|
"User already exists; promoting to admin",
|
|
);
|
|
await db.adminUser.create({ data: { userId: user.id } });
|
|
} else {
|
|
seedLogger.info(
|
|
{ op: "seed.userExists", userType, emailFingerprint: emailFingerprint(email) },
|
|
"User already exists",
|
|
);
|
|
}
|
|
return user;
|
|
}
|
|
user = await db.user.create({
|
|
data: {
|
|
email,
|
|
passwordHash,
|
|
avatar: "",
|
|
firstName,
|
|
lastName,
|
|
phone: "",
|
|
},
|
|
});
|
|
if (userType === "admin") {
|
|
await db.adminUser.create({
|
|
data: { userId: user.id },
|
|
});
|
|
}
|
|
return user;
|
|
}
|
|
|
|
async function createTenant(slug: string, name: string, users: { id: string; type: TenantUserType }[]) {
|
|
seedLogger.info({ op: "seed.createTenant", tenantSlug: slug, tenantName: name }, "Creating tenant");
|
|
let tenant = await db.tenant.findUnique({
|
|
where: { slug },
|
|
});
|
|
if (tenant) {
|
|
seedLogger.info({ op: "seed.tenantExists", tenantSlug: slug }, "Tenant already exists");
|
|
await db.tenant.update({
|
|
where: { id: tenant.id },
|
|
data: { active: true },
|
|
});
|
|
await ensureTenantSubscription(tenant.id);
|
|
await ensureTenantInboundAddress(tenant.id, name);
|
|
await ensureTenantUsers(tenant.id, users);
|
|
return tenant;
|
|
}
|
|
const address = await getAvailableTenantInboundAddress(name);
|
|
tenant = await db.tenant.create({
|
|
data: {
|
|
name,
|
|
slug,
|
|
icon: "",
|
|
active: true,
|
|
inboundAddresses: {
|
|
create: {
|
|
address,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
let tenantId = tenant.id;
|
|
|
|
await ensureTenantSubscription(tenantId);
|
|
await ensureTenantUsers(tenantId, users);
|
|
|
|
return tenant;
|
|
}
|
|
|
|
async function ensureTenantSubscription(tenantId: string) {
|
|
const existing = await db.tenantSubscription.findUnique({ where: { tenantId } });
|
|
if (existing) {
|
|
return existing;
|
|
}
|
|
return db.tenantSubscription.create({
|
|
data: {
|
|
tenantId,
|
|
stripeCustomerId: "",
|
|
},
|
|
});
|
|
}
|
|
|
|
async function ensureTenantInboundAddress(tenantId: string, tenantName: string) {
|
|
const existing = await db.tenantInboundAddress.findFirst({ where: { tenantId } });
|
|
if (existing) {
|
|
return existing;
|
|
}
|
|
const address = await getAvailableTenantInboundAddress(tenantName);
|
|
return db.tenantInboundAddress.create({
|
|
data: {
|
|
tenantId,
|
|
address,
|
|
},
|
|
});
|
|
}
|
|
|
|
async function ensureTenantUsers(tenantId: string, users: { id: string; type: TenantUserType }[]) {
|
|
for (const user of users) {
|
|
const tenantUser = await db.tenantUser.findFirst({
|
|
where: { tenantId, userId: user.id },
|
|
});
|
|
if (tenantUser) {
|
|
seedLogger.info({ op: "seed.tenantUserExists", userId: user.id, tenantId }, "User already in tenant");
|
|
continue;
|
|
}
|
|
await db.tenantUser.create({
|
|
data: {
|
|
tenantId,
|
|
userId: user.id,
|
|
type: user.type,
|
|
joined: TenantUserJoined.CREATOR,
|
|
status: TenantUserStatus.ACTIVE,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
async function assignTenantPlan(tenantSlug: string, planId: EntitlementPlanId) {
|
|
const tenant = await db.tenant.findUnique({
|
|
where: { slug: tenantSlug },
|
|
include: { subscription: { include: { products: true } } },
|
|
});
|
|
if (!tenant?.subscription) {
|
|
return;
|
|
}
|
|
|
|
const product = await db.subscriptionProduct.findFirst({
|
|
where: { stripeId: getEntitlementPlanStripeId({ id: planId }) },
|
|
select: { id: true },
|
|
});
|
|
if (!product) {
|
|
return;
|
|
}
|
|
|
|
const existingProduct = tenant.subscription.products.find((item) => item.subscriptionProductId === product.id && !item.endsAt);
|
|
if (existingProduct) {
|
|
return;
|
|
}
|
|
|
|
await db.tenantSubscriptionProduct.create({
|
|
data: {
|
|
tenantSubscriptionId: tenant.subscription.id,
|
|
subscriptionProductId: product.id,
|
|
stripeSubscriptionId: null,
|
|
quantity: 1,
|
|
fromCheckoutSessionId: null,
|
|
customEntitlementPlanId: planId,
|
|
},
|
|
});
|
|
}
|
|
|
|
async function seedDemoProductCoreTenant(tenantSlug: string, planId: EntitlementPlanId, demoUsers: { id: string }[]) {
|
|
const tenant = await db.tenant.findUnique({
|
|
where: { slug: tenantSlug },
|
|
select: { id: true, name: true, slug: true },
|
|
});
|
|
if (!tenant) {
|
|
return;
|
|
}
|
|
|
|
await Promise.all([
|
|
seedDemoTenantProductConfiguration(tenant.id, planId),
|
|
seedDemoTenantRoles(tenant.id, demoUsers),
|
|
seedDemoTenantKnowledgeBase(tenant.id, tenant.slug, tenant.name, planId),
|
|
seedDemoSupportTickets(tenant.id, tenant.slug, planId, demoUsers[0]?.id),
|
|
]);
|
|
}
|
|
|
|
async function seedDemoTenantProductConfiguration(tenantId: string, planId: EntitlementPlanId) {
|
|
await db.tenantProductConfiguration.upsert({
|
|
where: { tenantId },
|
|
create: {
|
|
tenantId,
|
|
blueprintId: productBlueprintIds.crmSaas,
|
|
notes: `Seeded CRM SaaS product-core demo configuration for the ${planId} plan.`,
|
|
},
|
|
update: {
|
|
blueprintId: productBlueprintIds.crmSaas,
|
|
notes: `Seeded CRM SaaS product-core demo configuration for the ${planId} plan.`,
|
|
},
|
|
});
|
|
|
|
const overrides =
|
|
planId === entitlementPlanIds.business
|
|
? [{ featureKey: featureKeys.mcp, override: "enabled", reason: "Business demo tenant shows blueprint-enabled MCP access." }]
|
|
: [{ featureKey: featureKeys.workflowEngine, override: "disabled", reason: `${planId} demo tenant keeps workflow automation plan-gated.` }];
|
|
|
|
for (const override of overrides) {
|
|
await db.tenantFeatureOverride.upsert({
|
|
where: { tenantId_featureKey: { tenantId, featureKey: override.featureKey } },
|
|
create: { tenantId, ...override },
|
|
update: { override: override.override, reason: override.reason },
|
|
});
|
|
}
|
|
}
|
|
|
|
async function seedDemoTenantRoles(tenantId: string, demoUsers: { id: string }[]) {
|
|
const permissions = await db.permission.findMany({
|
|
where: {
|
|
name: {
|
|
in: [...new Set(demoTenantRoleSeeds.flatMap((role) => role.permissions))],
|
|
},
|
|
},
|
|
select: { id: true, name: true },
|
|
});
|
|
const permissionIdByName = new Map(permissions.map((permission) => [permission.name, permission.id]));
|
|
|
|
for (const [index, roleSeed] of demoTenantRoleSeeds.entries()) {
|
|
let role = await db.role.findFirst({
|
|
where: { tenantId, name: roleSeed.name, type: "app" },
|
|
select: { id: true },
|
|
});
|
|
if (!role) {
|
|
role = await db.role.create({
|
|
data: {
|
|
tenantId,
|
|
name: roleSeed.name,
|
|
description: roleSeed.description,
|
|
type: "app",
|
|
assignToNewUsers: index === 1,
|
|
isDefault: false,
|
|
order: 100 + index,
|
|
},
|
|
select: { id: true },
|
|
});
|
|
}
|
|
|
|
for (const permissionName of roleSeed.permissions) {
|
|
const permissionId = permissionIdByName.get(permissionName);
|
|
if (!permissionId) {
|
|
continue;
|
|
}
|
|
const existing = await db.rolePermission.findFirst({ where: { roleId: role.id, permissionId } });
|
|
if (!existing) {
|
|
await db.rolePermission.create({ data: { roleId: role.id, permissionId } });
|
|
}
|
|
}
|
|
|
|
const user = demoUsers[index];
|
|
if (user) {
|
|
const existing = await db.userRole.findFirst({ where: { tenantId, userId: user.id, roleId: role.id } });
|
|
if (!existing) {
|
|
await db.userRole.create({ data: { tenantId, userId: user.id, roleId: role.id } });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function seedDemoTenantKnowledgeBase(tenantId: string, tenantSlug: string, tenantName: string, planId: EntitlementPlanId) {
|
|
const ownerKey = `tenant:${tenantId}`;
|
|
const slug = `${tenantSlug}-kb`;
|
|
const knowledgeBase = await db.knowledgeBase.upsert({
|
|
where: { ownerKey_slug: { ownerKey, slug } },
|
|
create: {
|
|
tenantId,
|
|
ownerKey,
|
|
basePath: "help",
|
|
slug,
|
|
title: `${tenantName} Knowledge Base`,
|
|
description: `Tenant-owned product-core demo knowledge base for the ${planId} plan.`,
|
|
defaultLanguage: "en",
|
|
layout: "articles",
|
|
color: 500,
|
|
enabled: true,
|
|
languages: "en,de",
|
|
links: "[]",
|
|
logo: "",
|
|
seoImage: "",
|
|
},
|
|
update: {
|
|
tenantId,
|
|
title: `${tenantName} Knowledge Base`,
|
|
description: `Tenant-owned product-core demo knowledge base for the ${planId} plan.`,
|
|
enabled: true,
|
|
},
|
|
});
|
|
|
|
const category = await db.knowledgeBaseCategory.upsert({
|
|
where: { knowledgeBaseId_slug: { knowledgeBaseId: knowledgeBase.id, slug: "product-core" } },
|
|
create: {
|
|
knowledgeBaseId: knowledgeBase.id,
|
|
slug: "product-core",
|
|
order: 1,
|
|
title: demoKnowledgeBaseCategoryTitle,
|
|
description: demoKnowledgeBaseCategoryDescription,
|
|
icon: "book-open",
|
|
language: "en",
|
|
seoImage: "",
|
|
},
|
|
update: {
|
|
title: demoKnowledgeBaseCategoryTitle,
|
|
description: demoKnowledgeBaseCategoryDescription,
|
|
},
|
|
});
|
|
|
|
const content = `<p>${tenantName} is seeded on the ${planId} plan with CRM SaaS blueprint gates, tenant roles, support tickets, and tenant-owned knowledge base content.</p>`;
|
|
await db.knowledgeBaseArticle.upsert({
|
|
where: { knowledgeBaseId_slug: { knowledgeBaseId: knowledgeBase.id, slug: "plan-gates-overview" } },
|
|
create: {
|
|
knowledgeBaseId: knowledgeBase.id,
|
|
categoryId: category.id,
|
|
slug: "plan-gates-overview",
|
|
title: demoKnowledgeBaseArticleTitle,
|
|
description: `What the ${planId} plan enables in this seeded tenant.`,
|
|
order: 1,
|
|
contentDraft: content,
|
|
contentPublished: content,
|
|
contentPublishedAsText: `${tenantName} is seeded on the ${planId} plan with CRM SaaS blueprint gates, tenant roles, support tickets, and tenant-owned knowledge base content.`,
|
|
contentType: "html",
|
|
language: "en",
|
|
seoImage: "",
|
|
publishedAt: new Date(),
|
|
},
|
|
update: {
|
|
categoryId: category.id,
|
|
title: demoKnowledgeBaseArticleTitle,
|
|
description: `What the ${planId} plan enables in this seeded tenant.`,
|
|
contentDraft: content,
|
|
contentPublished: content,
|
|
contentPublishedAsText: `${tenantName} is seeded on the ${planId} plan with CRM SaaS blueprint gates, tenant roles, support tickets, and tenant-owned knowledge base content.`,
|
|
publishedAt: new Date(),
|
|
},
|
|
});
|
|
}
|
|
|
|
async function seedDemoSupportTickets(tenantId: string, tenantSlug: string, planId: EntitlementPlanId, userId?: string) {
|
|
const tickets = [
|
|
{
|
|
inboundMessageId: `seed:${tenantSlug}:welcome`,
|
|
subject: demoSupportTicketSubjects.welcome,
|
|
message: `This ${planId} tenant includes seeded support-ticket data so HelpDesk can be reviewed immediately.`,
|
|
priority: "normal",
|
|
status: "open",
|
|
},
|
|
{
|
|
inboundMessageId: `seed:${tenantSlug}:plan-gates`,
|
|
subject: demoSupportTicketSubjects.planGates,
|
|
message: demoSupportTicketPlanGatesMessage,
|
|
priority: planId === entitlementPlanIds.business ? "high" : "normal",
|
|
status: "open",
|
|
},
|
|
];
|
|
|
|
for (const ticket of tickets) {
|
|
await db.supportTicket.upsert({
|
|
where: { inboundMessageId: ticket.inboundMessageId },
|
|
create: {
|
|
tenantId,
|
|
userId,
|
|
source: "seed",
|
|
fromEmail: "support-demo@example.test",
|
|
fromUrl: `/${tenantSlug}/dashboard`,
|
|
...ticket,
|
|
},
|
|
update: {
|
|
tenantId,
|
|
userId,
|
|
source: "seed",
|
|
fromEmail: "support-demo@example.test",
|
|
fromUrl: `/${tenantSlug}/dashboard`,
|
|
subject: ticket.subject,
|
|
message: ticket.message,
|
|
priority: ticket.priority,
|
|
status: ticket.status,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
export default {
|
|
seed,
|
|
};
|