Compare commits

...

3 Commits

Author SHA1 Message Date
kdaniel2410 85441c0dfe fix: replace remaining d1 transactions 2026-06-27 10:11:32 +01:00
kdaniel2410 85223bc3b2 fix: batch billing cycle writes 2026-06-27 10:11:32 +01:00
kdaniel2410 193bce0cc9 fix: batch contract database writes 2026-06-27 10:11:32 +01:00
4 changed files with 114 additions and 89 deletions
+40 -28
View File
@@ -1,4 +1,5 @@
import { fail } from '@sveltejs/kit'; import { fail } from '@sveltejs/kit';
import type { BatchItem } from 'drizzle-orm/batch';
import { and, asc, desc, eq, gte, inArray, isNull, like, lte, ne } from 'drizzle-orm'; import { and, asc, desc, eq, gte, inArray, isNull, like, lte, ne } from 'drizzle-orm';
import { createSearchParamsSchema, validateSearchParams } from 'runed/kit'; import { createSearchParamsSchema, validateSearchParams } from 'runed/kit';
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
@@ -458,14 +459,17 @@ export const actions: Actions = {
const invoicePrefix = invoiceNumberPrefix(invoiceDate); const invoicePrefix = invoiceNumberPrefix(invoiceDate);
const latestSequence = await latestInvoiceSequence(activeOrganizationId, invoicePrefix); const latestSequence = await latestInvoiceSequence(activeOrganizationId, invoicePrefix);
await db.transaction(async (tx) => { const now = new Date();
let invoiceOffset = 0; const queries: BatchItem<'sqlite'>[] = [];
for (const client of groupedClients) { let invoiceOffset = 0;
const invoiceId = crypto.randomUUID();
const invoiceNumber = `${invoicePrefix}-${String(latestSequence + invoiceOffset + 1).padStart(3, '0')}`;
invoiceOffset += 1;
await tx.insert(invoices).values({ for (const client of groupedClients) {
const invoiceId = crypto.randomUUID();
const invoiceNumber = `${invoicePrefix}-${String(latestSequence + invoiceOffset + 1).padStart(3, '0')}`;
invoiceOffset += 1;
queries.push(
db.insert(invoices).values({
id: invoiceId, id: invoiceId,
organizationId: activeOrganizationId, organizationId: activeOrganizationId,
clientId: client.clientId, clientId: client.clientId,
@@ -476,11 +480,13 @@ export const actions: Actions = {
taxGbp: 0, taxGbp: 0,
totalGbp: client.totalGbp, totalGbp: client.totalGbp,
notes: `Generated from billing run. Contract billing through ${preview.contractsCutoff}; services through ${preview.servicesCutoff}.`, notes: `Generated from billing run. Contract billing through ${preview.contractsCutoff}; services through ${preview.servicesCutoff}.`,
createdAt: new Date(), createdAt: now,
updatedAt: new Date() updatedAt: now
}); })
);
await tx.insert(invoiceLines).values( queries.push(
db.insert(invoiceLines).values(
client.lines.map((line) => ({ client.lines.map((line) => ({
id: crypto.randomUUID(), id: crypto.randomUUID(),
organizationId: activeOrganizationId, organizationId: activeOrganizationId,
@@ -494,29 +500,35 @@ export const actions: Actions = {
quantity: line.quantity, quantity: line.quantity,
unitPriceGbp: line.unitPriceGbp, unitPriceGbp: line.unitPriceGbp,
totalGbp: line.totalGbp, totalGbp: line.totalGbp,
createdAt: new Date(), createdAt: now,
updatedAt: new Date() updatedAt: now
})) }))
); )
} );
}
const contractBilledTo = new Map<string, string>(); const contractBilledTo = new Map<string, string>();
for (const line of includedLines) { for (const line of includedLines) {
if (line.sourceType !== 'contract') continue; if (line.sourceType !== 'contract') continue;
const current = contractBilledTo.get(line.sourceId); const current = contractBilledTo.get(line.sourceId);
if (!current || line.periodEnd > current) if (!current || line.periodEnd > current) contractBilledTo.set(line.sourceId, line.periodEnd);
contractBilledTo.set(line.sourceId, line.periodEnd); }
}
for (const [contractId, billedTo] of contractBilledTo) { for (const [contractId, billedTo] of contractBilledTo) {
await tx queries.push(
db
.update(contracts) .update(contracts)
.set({ billedTo, updatedAt: new Date() }) .set({ billedTo, updatedAt: now })
.where( .where(
and(eq(contracts.id, contractId), eq(contracts.organizationId, activeOrganizationId)) and(eq(contracts.id, contractId), eq(contracts.organizationId, activeOrganizationId))
); )
} );
}); }
const [firstQuery, ...remainingQueries] = queries;
if (firstQuery) {
await db.batch([firstQuery, ...remainingQueries]);
}
return { return {
message: `Billing run complete. Created ${groupedClients.length} invoice${groupedClients.length === 1 ? '' : 's'}.` message: `Billing run complete. Created ${groupedClients.length} invoice${groupedClients.length === 1 ? '' : 's'}.`
@@ -157,8 +157,8 @@ export const actions: Actions = {
if (contact.isPrimary) return message(form, 'Contact is already primary.'); if (contact.isPrimary) return message(form, 'Contact is already primary.');
const now = new Date(); const now = new Date();
await db.transaction(async (tx) => { await db.batch([
await tx db
.update(contacts) .update(contacts)
.set({ isPrimary: false, updatedAt: now }) .set({ isPrimary: false, updatedAt: now })
.where( .where(
@@ -168,9 +168,9 @@ export const actions: Actions = {
eq(contacts.isPrimary, true), eq(contacts.isPrimary, true),
isNull(contacts.archivedAt) isNull(contacts.archivedAt)
) )
); ),
await tx db
.update(contacts) .update(contacts)
.set({ isPrimary: true, updatedAt: now }) .set({ isPrimary: true, updatedAt: now })
.where( .where(
@@ -180,8 +180,8 @@ export const actions: Actions = {
eq(contacts.organizationId, activeOrganizationId), eq(contacts.organizationId, activeOrganizationId),
isNull(contacts.archivedAt) isNull(contacts.archivedAt)
) )
); )
}); ]);
return message(form, 'Primary contact updated.'); return message(form, 'Primary contact updated.');
}, },
@@ -213,32 +213,37 @@ export const actions: Actions = {
try { try {
const contractId = crypto.randomUUID(); const contractId = crypto.randomUUID();
await db.transaction(async (tx) => { const now = new Date();
await tx.insert(contracts).values({ const createContract = db.insert(contracts).values({
id: contractId, id: contractId,
organizationId: activeOrganizationId, organizationId: activeOrganizationId,
clientId: form.data.clientId, clientId: form.data.clientId,
serviceId: form.data.serviceId || null, serviceId: form.data.serviceId || null,
licenseFeeGbp: form.data.licenseFeeGbp, licenseFeeGbp: form.data.licenseFeeGbp,
depositGbp: form.data.depositGbp, depositGbp: form.data.depositGbp,
startDate: form.data.startDate, startDate: form.data.startDate,
endDate: form.data.endDate, endDate: form.data.endDate,
status: 'draft', status: 'draft',
updatedAt: new Date(), updatedAt: now,
createdAt: new Date() createdAt: now
}); });
const uniqueRoomIds = [...new Set(form.data.roomIds)]; const uniqueRoomIds = [...new Set(form.data.roomIds)];
if (uniqueRoomIds.length > 0) { if (uniqueRoomIds.length > 0) {
await tx.insert(contractRooms).values( await db.batch([
createContract,
db.insert(contractRooms).values(
uniqueRoomIds.map((roomId) => ({ uniqueRoomIds.map((roomId) => ({
contractId, contractId,
roomId roomId
})) }))
); )
} ]);
}); } else {
} catch { await createContract;
}
} catch (cause) {
console.error('Unable to create contract', cause);
return message(form, 'Unable to create contract.', { status: 400 }); return message(form, 'Unable to create contract.', { status: 400 });
} }
@@ -295,39 +300,47 @@ export const actions: Actions = {
} }
try { try {
await db.transaction(async (tx) => { const now = new Date();
await tx const updateContract = db
.update(contracts) .update(contracts)
.set({ .set({
clientId: form.data.clientId, clientId: form.data.clientId,
serviceId: form.data.serviceId || null, serviceId: form.data.serviceId || null,
licenseFeeGbp: form.data.licenseFeeGbp, licenseFeeGbp: form.data.licenseFeeGbp,
depositGbp: form.data.depositGbp, depositGbp: form.data.depositGbp,
startDate: form.data.startDate, startDate: form.data.startDate,
endDate: form.data.endDate, endDate: form.data.endDate,
updatedAt: new Date() updatedAt: now
}) })
.where( .where(
and( and(
eq(contracts.id, form.data.id), eq(contracts.id, form.data.id),
eq(contracts.clientId, params.id), eq(contracts.clientId, params.id),
eq(contracts.organizationId, activeOrganizationId), eq(contracts.organizationId, activeOrganizationId),
isNull(contracts.archivedAt) isNull(contracts.archivedAt)
) )
); );
const deleteRoomLinks = db
.delete(contractRooms)
.where(eq(contractRooms.contractId, form.data.id));
const uniqueRoomIds = [...new Set(form.data.roomIds)];
await tx.delete(contractRooms).where(eq(contractRooms.contractId, form.data.id)); if (uniqueRoomIds.length > 0) {
const uniqueRoomIds = [...new Set(form.data.roomIds)]; await db.batch([
if (uniqueRoomIds.length > 0) { updateContract,
await tx.insert(contractRooms).values( deleteRoomLinks,
db.insert(contractRooms).values(
uniqueRoomIds.map((roomId) => ({ uniqueRoomIds.map((roomId) => ({
contractId: form.data.id, contractId: form.data.id,
roomId roomId
})) }))
); )
} ]);
}); } else {
} catch { await db.batch([updateContract, deleteRoomLinks]);
}
} catch (cause) {
console.error('Unable to update contract', cause);
return message(form, 'Unable to update contract.', { status: 400 }); return message(form, 'Unable to update contract.', { status: 400 });
} }
@@ -24,8 +24,8 @@ export const POST: RequestHandler = async ({ locals, request }) => {
const id = crypto.randomUUID(); const id = crypto.randomUUID();
const now = new Date(); const now = new Date();
await db.transaction(async (tx) => { await db.batch([
await tx.insert(organizations).values({ db.insert(organizations).values({
id, id,
name: parsed.data.name, name: parsed.data.name,
addressLine1: parsed.data.addressLine1, addressLine1: parsed.data.addressLine1,
@@ -43,16 +43,16 @@ export const POST: RequestHandler = async ({ locals, request }) => {
bankSwift: parsed.data.bankSwift || null, bankSwift: parsed.data.bankSwift || null,
createdAt: now, createdAt: now,
updatedAt: now updatedAt: now
}); }),
await tx.insert(organizationMemberships).values({ db.insert(organizationMemberships).values({
organizationId: id, organizationId: id,
userId: locals.user.id, userId: locals.user.id,
role: 'admin', role: 'admin',
createdAt: now, createdAt: now,
updatedAt: now updatedAt: now
}); })
}); ]);
await setActiveOrganization(locals, id); await setActiveOrganization(locals, id);