Refactored a bunch of shit
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m57s
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m57s
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user