Files
seminarhof-e2e-mqo7np5j/packages/functions/src/functions/cancel-invoice.function.ts
2026-06-21 22:00:02 +02:00

64 lines
1.8 KiB
TypeScript

import { z } from 'zod'
import { pikkuFunc } from '#pikku'
import { InvoiceAlreadyPaidError } from '../errors.js'
import { isAdminOrOwner } from '../lib/permissions.js'
export const CancelInvoiceInput = z.object({
invoiceId: z.string(),
})
export const CancelInvoiceOutput = z.object({
invoiceId: z.string(),
status: z.literal('cancelled'),
})
const uid = () =>
globalThis.crypto?.randomUUID?.() ??
`audit_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`
export const cancelInvoice = pikkuFunc({
expose: true,
description: 'Admin/owner: cancel an outstanding invoice (typo, duplicate, etc.).',
permissions: { isAdminOrOwner },
input: CancelInvoiceInput,
output: CancelInvoiceOutput,
func: async ({ kysely }, { invoiceId }, { session }) => {
const inv = await kysely
.selectFrom('invoice')
.innerJoin('booking', 'booking.bookingId', 'invoice.bookingId')
.where('invoice.invoiceId', '=', invoiceId)
.select(['invoice.invoiceId', 'invoice.status', 'booking.venueId'])
.executeTakeFirstOrThrow()
if (inv.status === 'paid') {
throw new InvoiceAlreadyPaidError()
}
if (inv.status === 'cancelled') {
return { invoiceId, status: 'cancelled' as const }
}
await kysely
.updateTable('invoice')
.set({ status: 'cancelled' })
.where('invoiceId', '=', invoiceId)
.execute()
await kysely
.insertInto('auditLog')
.values({
auditId: uid(),
venueId: inv.venueId,
userId: session.userId,
entity: 'invoice',
entityId: invoiceId,
field: 'status',
beforeValue: inv.status,
afterValue: 'cancelled',
action: 'update',
})
.execute()
return { invoiceId, status: 'cancelled' as const }
},
})