Add business nickname support across app and API

This commit is contained in:
2025-08-11 01:50:20 -04:00
parent 93ffdf3c86
commit a680f89a46
19 changed files with 308 additions and 1193 deletions

View File

@@ -6,7 +6,17 @@ import { invoices } from "~/server/db/schema";
import { sql } from "drizzle-orm";
const businessSchema = z.object({
name: z.string().min(1, "Business name is required"),
name: z
.string()
.trim()
.min(1, "Business name is required")
.max(255, "Business name must be 255 characters or less"),
nickname: z
.string()
.trim()
.max(255, "Nickname must be 255 characters or less")
.optional()
.or(z.literal("")),
email: z.string().email().optional().or(z.literal("")),
phone: z.string().optional().or(z.literal("")),
addressLine1: z.string().optional().or(z.literal("")),
@@ -96,7 +106,54 @@ export const businessesRouter = createTRPCRouter({
const [newBusiness] = await ctx.db
.insert(businesses)
.values({
...input,
name: input.name.trim(),
nickname:
input.nickname && input.nickname.trim() !== ""
? input.nickname.trim()
: null,
email:
input.email && input.email.trim() !== ""
? input.email.trim()
: null,
phone:
input.phone && input.phone.trim() !== ""
? input.phone.trim()
: null,
addressLine1:
input.addressLine1 && input.addressLine1.trim() !== ""
? input.addressLine1.trim()
: null,
addressLine2:
input.addressLine2 && input.addressLine2.trim() !== ""
? input.addressLine2.trim()
: null,
city:
input.city && input.city.trim() !== "" ? input.city.trim() : null,
state:
input.state && input.state.trim() !== ""
? input.state.trim()
: null,
postalCode:
input.postalCode && input.postalCode.trim() !== ""
? input.postalCode.trim()
: null,
country:
input.country && input.country.trim() !== ""
? input.country.trim()
: null,
website:
input.website && input.website.trim() !== ""
? input.website.trim()
: null,
taxId:
input.taxId && input.taxId.trim() !== ""
? input.taxId.trim()
: null,
logoUrl:
input.logoUrl && input.logoUrl.trim() !== ""
? input.logoUrl.trim()
: null,
isDefault: input.isDefault ?? false,
createdById: ctx.session.user.id,
})
.returning();
@@ -126,7 +183,56 @@ export const businessesRouter = createTRPCRouter({
const [updatedBusiness] = await ctx.db
.update(businesses)
.set({
...updateData,
name: (updateData.name ?? "").trim(),
nickname:
updateData.nickname && updateData.nickname.trim() !== ""
? updateData.nickname.trim()
: null,
email:
updateData.email && updateData.email.trim() !== ""
? updateData.email.trim()
: null,
phone:
updateData.phone && updateData.phone.trim() !== ""
? updateData.phone.trim()
: null,
addressLine1:
updateData.addressLine1 && updateData.addressLine1.trim() !== ""
? updateData.addressLine1.trim()
: null,
addressLine2:
updateData.addressLine2 && updateData.addressLine2.trim() !== ""
? updateData.addressLine2.trim()
: null,
city:
updateData.city && updateData.city.trim() !== ""
? updateData.city.trim()
: null,
state:
updateData.state && updateData.state.trim() !== ""
? updateData.state.trim()
: null,
postalCode:
updateData.postalCode && updateData.postalCode.trim() !== ""
? updateData.postalCode.trim()
: null,
country:
updateData.country && updateData.country.trim() !== ""
? updateData.country.trim()
: null,
website:
updateData.website && updateData.website.trim() !== ""
? updateData.website.trim()
: null,
taxId:
updateData.taxId && updateData.taxId.trim() !== ""
? updateData.taxId.trim()
: null,
logoUrl:
updateData.logoUrl && updateData.logoUrl.trim() !== ""
? updateData.logoUrl.trim()
: null,
isDefault: updateData.isDefault ?? false,
updatedAt: new Date(),
})
.where(

View File

@@ -77,7 +77,7 @@ export const emailRouter = createTRPCRouter({
// Create email content
const subject =
input.customSubject ??
`Invoice ${invoice.invoiceNumber} from ${invoice.business?.name ?? "Your Business"}`;
`Invoice ${invoice.invoiceNumber} from ${invoice.business ? `${invoice.business.name}${invoice.business.nickname ? ` (${invoice.business.nickname})` : ""}` : "Your Business"}`;
const userName =
invoice.business?.emailFromName ??
@@ -124,7 +124,11 @@ export const emailRouter = createTRPCRouter({
// Use business's custom Resend setup
resendInstance = new Resend(invoice.business.resendApiKey);
const fromName =
invoice.business.emailFromName ?? invoice.business.name ?? userName;
invoice.business.emailFromName ??
(invoice.business.nickname
? `${invoice.business.name} (${invoice.business.nickname})`
: invoice.business.name) ??
userName;
fromEmail = `${fromName} <noreply@${invoice.business.resendDomain}>`;
} else if (env.RESEND_DOMAIN) {
// Use system Resend configuration

View File

@@ -25,6 +25,7 @@ const ClientBackupSchema = z.object({
const BusinessBackupSchema = z.object({
name: z.string(),
nickname: z.string().optional(),
email: z.string().optional(),
phone: z.string().optional(),
addressLine1: z.string().optional(),
@@ -51,6 +52,7 @@ const InvoiceItemBackupSchema = z.object({
const InvoiceBackupSchema = z.object({
invoiceNumber: z.string(),
businessName: z.string().optional(),
businessNickname: z.string().optional(),
clientName: z.string(),
issueDate: z.string().transform((str) => new Date(str)),
dueDate: z.string().transform((str) => new Date(str)),
@@ -205,6 +207,7 @@ export const settingsRouter = createTRPCRouter({
columns: {
id: true,
name: true,
nickname: true,
email: true,
phone: true,
addressLine1: true,
@@ -232,6 +235,7 @@ export const settingsRouter = createTRPCRouter({
business: {
columns: {
name: true,
nickname: true,
},
},
items: {
@@ -269,6 +273,7 @@ export const settingsRouter = createTRPCRouter({
})),
businesses: userBusinesses.map((business) => ({
name: business.name,
nickname: business.nickname ?? undefined,
email: business.email ?? undefined,
phone: business.phone ?? undefined,
addressLine1: business.addressLine1 ?? undefined,
@@ -285,6 +290,7 @@ export const settingsRouter = createTRPCRouter({
invoices: userInvoices.map((invoice) => ({
invoiceNumber: invoice.invoiceNumber,
businessName: invoice.business?.name,
businessNickname: invoice.business?.nickname,
clientName: invoice.client.name,
issueDate: invoice.issueDate,
dueDate: invoice.dueDate,
@@ -337,6 +343,9 @@ export const settingsRouter = createTRPCRouter({
if (newBusiness) {
businessIdMap.set(businessData.name, newBusiness.id);
if (businessData.nickname) {
businessIdMap.set(businessData.nickname, newBusiness.id);
}
}
}
@@ -347,9 +356,14 @@ export const settingsRouter = createTRPCRouter({
throw new Error(`Client ${invoiceData.clientName} not found`);
}
const businessId = invoiceData.businessName
? businessIdMap.get(invoiceData.businessName)
: null;
const businessId = invoiceData.businessNickname
? (businessIdMap.get(invoiceData.businessNickname) ??
(invoiceData.businessName
? (businessIdMap.get(invoiceData.businessName) ?? null)
: null))
: invoiceData.businessName
? (businessIdMap.get(invoiceData.businessName) ?? null)
: null;
const [newInvoice] = await tx
.insert(invoices)

View File

@@ -143,6 +143,7 @@ export const businesses = createTable(
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
name: d.varchar({ length: 255 }).notNull(),
nickname: d.varchar({ length: 255 }),
email: d.varchar({ length: 255 }),
phone: d.varchar({ length: 50 }),
addressLine1: d.varchar({ length: 255 }),
@@ -172,6 +173,7 @@ export const businesses = createTable(
(t) => [
index("business_created_by_idx").on(t.createdById),
index("business_name_idx").on(t.name),
index("business_nickname_idx").on(t.nickname),
index("business_email_idx").on(t.email),
index("business_is_default_idx").on(t.isDefault),
],