Fix Docker signup, refresh docs, and improve blank invoice flow.

Parse DISABLE_SIGNUPS and related env booleans correctly for Compose string values, and derive auth trustedOrigins from BETTER_AUTH_URL. Rewrite README and architecture docs with the git.soconnor.dev remote and accurate deployment guidance. Allow zero-line-item draft invoices with validation when sending email.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-25 23:45:57 -04:00
co-authored by Cursor
parent 480c50981d
commit 463b1f503e
12 changed files with 323 additions and 394 deletions
+4
View File
@@ -70,6 +70,10 @@ export const emailRouter = createTRPCRouter({
throw new Error("Client has no email address");
}
if (!invoice.items.length) {
throw new Error("Add at least one line item before sending this invoice");
}
// Validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(invoice.client.email)) {
+40 -18
View File
@@ -44,7 +44,7 @@ const createInvoiceSchema = z.object({
taxRate: z.number().min(0).max(100).default(0),
currency: z.string().length(3).default("USD"),
sendReminderAt: z.date().nullable().optional(),
items: z.array(invoiceItemSchema).min(1, "At least one item is required"),
items: z.array(invoiceItemSchema).min(0, "Items must be an array"),
});
const updateInvoiceSchema = createInvoiceSchema.partial().extend({
@@ -83,6 +83,24 @@ async function verifyBusinessAccess(
return business;
}
async function resolveBusinessForInvoice(
ctx: InvoiceRouterContext,
businessId?: string | null,
) {
if (businessId && businessId.trim() !== "") {
return verifyBusinessAccess(ctx, businessId);
}
const [defaultBusiness] = await ctx.db
.select()
.from(businesses)
.where(eq(businesses.createdById, ctx.session.user.id))
.orderBy(desc(businesses.isDefault), desc(businesses.createdAt))
.limit(1);
return defaultBusiness ?? null;
}
async function verifyClientAccess(ctx: InvoiceRouterContext, clientId: string) {
const client = await ctx.db.query.clients.findFirst({
where: eq(clients.id, clientId),
@@ -349,14 +367,16 @@ export const invoicesRouter = createTRPCRouter({
});
}
await tx.insert(invoiceItems).values(
items.map((item, idx) => ({
...item,
invoiceId: invoice.id,
amount: item.hours * item.rate,
position: idx,
})),
);
if (items.length > 0) {
await tx.insert(invoiceItems).values(
items.map((item, idx) => ({
...item,
invoiceId: invoice.id,
amount: item.hours * item.rate,
position: idx,
})),
);
}
return invoice;
});
@@ -475,14 +495,16 @@ export const invoicesRouter = createTRPCRouter({
await tx.delete(invoiceItems).where(eq(invoiceItems.invoiceId, id));
await tx.insert(invoiceItems).values(
items.map((item, idx) => ({
...item,
invoiceId: id,
amount: item.hours * item.rate,
position: idx,
})),
);
if (items.length > 0) {
await tx.insert(invoiceItems).values(
items.map((item, idx) => ({
...item,
invoiceId: id,
amount: item.hours * item.rate,
position: idx,
})),
);
}
} else {
const [updatedInvoice] = await tx
.update(invoices)
@@ -658,7 +680,7 @@ export const invoicesRouter = createTRPCRouter({
: null;
const [client, business, settings] = await Promise.all([
verifyClientAccess(ctx, input.clientId),
verifyBusinessAccess(ctx, businessId),
resolveBusinessForInvoice(ctx, businessId),
ctx.db.query.platformSettings.findFirst({
where: eq(platformSettings.id, "global"),
}),