Refactored a bunch of shit
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m57s

This commit is contained in:
2026-02-09 01:49:19 -07:00
parent c39865031a
commit 12d59bb42f
40 changed files with 844 additions and 678 deletions

View File

@@ -45,6 +45,11 @@ export const POST: APIRoute = async ({ redirect, locals, params }) => {
return new Response("Unauthorized", { status: 401 });
}
const isAdminOrOwner = membership.role === "owner" || membership.role === "admin";
if (!isAdminOrOwner) {
return new Response("Only owners and admins can convert quotes", { status: 403 });
}
try {
const lastInvoice = await db
.select()

View File

@@ -107,7 +107,7 @@ export const GET: APIRoute = async ({ params, locals }) => {
return new Response(buffer, {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename="${invoice.number}.pdf"`,
"Content-Disposition": `attachment; filename="${invoice.number.replace(/[^a-zA-Z0-9_\-\.]/g, "_")}.pdf"`,
},
});
} catch (error) {

View File

@@ -222,51 +222,52 @@ export const POST: APIRoute = async ({ request, params, locals, redirect }) => {
return redirect(`/dashboard/invoices/${id}?error=no-entries`);
}
// Transaction-like operations
try {
await db.insert(invoiceItems).values(newItems);
await db.transaction(async (tx) => {
await tx.insert(invoiceItems).values(newItems);
if (entryIdsToUpdate.length > 0) {
await db
.update(timeEntries)
.set({ invoiceId: invoice.id })
.where(inArray(timeEntries.id, entryIdsToUpdate));
}
const allItems = await db
.select()
.from(invoiceItems)
.where(eq(invoiceItems.invoiceId, invoice.id));
const subtotal = allItems.reduce((sum, item) => sum + item.amount, 0);
let discountAmount = 0;
if (invoice.discountType === "percentage") {
discountAmount = Math.round(
subtotal * ((invoice.discountValue || 0) / 100),
);
} else {
discountAmount = Math.round((invoice.discountValue || 0) * 100);
if (invoice.discountValue && invoice.discountValue > 0) {
discountAmount = Math.round((invoice.discountValue || 0) * 100);
if (entryIdsToUpdate.length > 0) {
await tx
.update(timeEntries)
.set({ invoiceId: invoice.id })
.where(inArray(timeEntries.id, entryIdsToUpdate));
}
}
const taxableAmount = Math.max(0, subtotal - discountAmount);
const taxAmount = Math.round(
taxableAmount * ((invoice.taxRate || 0) / 100),
);
const total = subtotal - discountAmount + taxAmount;
const allItems = await tx
.select()
.from(invoiceItems)
.where(eq(invoiceItems.invoiceId, invoice.id));
await db
.update(invoices)
.set({
subtotal,
discountAmount,
taxAmount,
total,
})
.where(eq(invoices.id, invoice.id));
const subtotal = allItems.reduce((sum, item) => sum + item.amount, 0);
let discountAmount = 0;
if (invoice.discountType === "percentage") {
discountAmount = Math.round(
subtotal * ((invoice.discountValue || 0) / 100),
);
} else {
discountAmount = Math.round((invoice.discountValue || 0) * 100);
if (invoice.discountValue && invoice.discountValue > 0) {
discountAmount = Math.round((invoice.discountValue || 0) * 100);
}
}
const taxableAmount = Math.max(0, subtotal - discountAmount);
const taxAmount = Math.round(
taxableAmount * ((invoice.taxRate || 0) / 100),
);
const total = subtotal - discountAmount + taxAmount;
await tx
.update(invoices)
.set({
subtotal,
discountAmount,
taxAmount,
total,
})
.where(eq(invoices.id, invoice.id));
});
return redirect(`/dashboard/invoices/${id}?success=imported`);
} catch (error) {

View File

@@ -3,6 +3,7 @@ import { db } from "../../../../../db";
import { invoiceItems, invoices, members } from "../../../../../db/schema";
import { eq, and } from "drizzle-orm";
import { recalculateInvoiceTotals } from "../../../../../utils/invoice";
import { MAX_LENGTHS, exceedsLength } from "../../../../../lib/validation";
export const POST: APIRoute = async ({
request,
@@ -61,6 +62,11 @@ export const POST: APIRoute = async ({
return new Response("Missing required fields", { status: 400 });
}
const lengthError = exceedsLength("Description", description, MAX_LENGTHS.itemDescription);
if (lengthError) {
return new Response(lengthError, { status: 400 });
}
const quantity = parseFloat(quantityStr);
const unitPriceMajor = parseFloat(unitPriceStr);

View File

@@ -60,6 +60,13 @@ export const POST: APIRoute = async ({
return new Response("Unauthorized", { status: 401 });
}
// Destructive status changes require owner/admin
const destructiveStatuses = ["void"];
const isAdminOrOwner = membership.role === "owner" || membership.role === "admin";
if (destructiveStatuses.includes(status) && !isAdminOrOwner) {
return new Response("Only owners and admins can void invoices", { status: 403 });
}
try {
await db
.update(invoices)

View File

@@ -3,6 +3,7 @@ import { db } from "../../../../db";
import { invoices, members } from "../../../../db/schema";
import { eq, and } from "drizzle-orm";
import { recalculateInvoiceTotals } from "../../../../utils/invoice";
import { MAX_LENGTHS, exceedsLength } from "../../../../lib/validation";
export const POST: APIRoute = async ({ request, redirect, locals, params }) => {
const user = locals.user;
@@ -56,6 +57,14 @@ export const POST: APIRoute = async ({ request, redirect, locals, params }) => {
return new Response("Missing required fields", { status: 400 });
}
const lengthError =
exceedsLength("Invoice number", number, MAX_LENGTHS.invoiceNumber) ||
exceedsLength("Currency", currency, MAX_LENGTHS.currency) ||
exceedsLength("Notes", notes, MAX_LENGTHS.invoiceNotes);
if (lengthError) {
return new Response(lengthError, { status: 400 });
}
try {
const issueDate = new Date(issueDateStr);
const dueDate = new Date(dueDateStr);