feat(accounting): allow cancelling serial invoice executions
This commit is contained in:
105
backend/tests/serialExecutionCancellation.test.ts
Normal file
105
backend/tests/serialExecutionCancellation.test.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { PgDialect } from 'drizzle-orm/pg-core'
|
||||
import * as schema from '../db/schema'
|
||||
import { cancelManualGeneration, finishManualGeneration } from '../src/modules/serialexecution.service'
|
||||
|
||||
const id = '00000000-0000-4000-8000-000000000001'
|
||||
const dialect = new PgDialect()
|
||||
|
||||
function fixture(status: string | null, options: { cancelDuringFinish?: boolean } = {}) {
|
||||
const execution = status ? { id, tenant: 7, status } : null
|
||||
const calls: any[] = []
|
||||
const db: any = {
|
||||
transaction: async (fn: any) => fn(db),
|
||||
select: () => query('select'),
|
||||
update: (table: any) => query('update', table),
|
||||
}
|
||||
function query(kind: string, table?: any) {
|
||||
let values: any
|
||||
let condition: any
|
||||
const builder: any = {
|
||||
from: (value: any) => { table = value; return builder },
|
||||
set: (value: any) => { values = value; return builder },
|
||||
where: (value: any) => { condition = value; return builder },
|
||||
limit: () => builder,
|
||||
for: (lock: string) => { assert.equal(lock, 'update'); return builder },
|
||||
returning: () => builder,
|
||||
then: (resolve: any, reject: any) => Promise.resolve().then(() => {
|
||||
const sql = condition ? dialect.sqlToQuery(condition) : null
|
||||
calls.push({ kind, table, values, sql })
|
||||
if (table === schema.serialExecutions) {
|
||||
if (!execution || !sql.params.includes(7)) return []
|
||||
if (values?.status === 'processing' && !['draft', 'error'].includes(execution.status)) return []
|
||||
if (values) Object.assign(execution, values)
|
||||
return [{ ...execution }]
|
||||
}
|
||||
if (table === schema.createddocuments && kind === 'select' && options.cancelDuringFinish) {
|
||||
execution!.status = 'cancelling'
|
||||
return [{ id: 123, state: 'Entwurf' }]
|
||||
}
|
||||
if (table === schema.tenants) return [{ id: 7 }]
|
||||
return []
|
||||
}).then(resolve, reject),
|
||||
}
|
||||
return builder
|
||||
}
|
||||
return { server: { db } as any, execution, calls }
|
||||
}
|
||||
|
||||
test('cancels drafts and archives only unarchived draft invoices of the same tenant and run', async () => {
|
||||
const f = fixture('draft')
|
||||
const result = await cancelManualGeneration(f.server, id, 7)
|
||||
assert.equal(result.execution.status, 'cancelled')
|
||||
const archive = f.calls.find(c => c.table === schema.createddocuments)
|
||||
assert.deepEqual(archive.values, { archived: true })
|
||||
assert.deepEqual(archive.sql.params, [id, 7, 'Entwurf', false])
|
||||
for (const column of ['serialexecution', 'tenant', 'state', 'archived']) {
|
||||
assert.ok(archive.sql.sql.includes(`"${column}"`))
|
||||
}
|
||||
})
|
||||
|
||||
for (const status of ['generating', 'processing']) {
|
||||
test(`requests cooperative cancellation while ${status}`, async () => {
|
||||
const f = fixture(status)
|
||||
const result = await cancelManualGeneration(f.server, id, 7)
|
||||
assert.equal(result.execution.status, 'cancelling')
|
||||
assert.equal(f.calls.some(c => c.table === schema.createddocuments), false)
|
||||
})
|
||||
}
|
||||
|
||||
for (const status of ['cancelled', 'cancelling']) {
|
||||
test(`repeated cancellation of ${status} is idempotent`, async () => {
|
||||
const f = fixture(status)
|
||||
await cancelManualGeneration(f.server, id, 7)
|
||||
assert.equal(f.calls.some(c => c.kind === 'update'), false)
|
||||
})
|
||||
}
|
||||
|
||||
test('rejects completed runs and missing or foreign-tenant executions without writes', async () => {
|
||||
for (const [status, tenant, code] of [['completed', 7, 409], [null, 7, 404], ['draft', 8, 404]] as const) {
|
||||
const f = fixture(status)
|
||||
await assert.rejects(cancelManualGeneration(f.server, id, tenant), { statusCode: code })
|
||||
assert.equal(f.calls.some(c => c.kind === 'update'), false)
|
||||
}
|
||||
})
|
||||
|
||||
test('cannot finalize cancelled, cancelling, completed or already processing runs', async () => {
|
||||
for (const status of ['cancelled', 'cancelling', 'completed', 'processing', 'generating']) {
|
||||
const f = fixture(status)
|
||||
await assert.rejects(finishManualGeneration(f.server, id, 7), { statusCode: 409 })
|
||||
assert.equal(f.execution!.status, status)
|
||||
assert.equal(f.calls.length, 1)
|
||||
}
|
||||
})
|
||||
|
||||
test('cancellation during finalization skips remaining invoices and settles as cancelled', async () => {
|
||||
const f = fixture('draft', { cancelDuringFinish: true })
|
||||
const result = await finishManualGeneration(f.server, id, 7)
|
||||
assert.equal(result.execution.status, 'cancelled')
|
||||
assert.equal(result.processed, 0)
|
||||
assert.equal(result.success, false)
|
||||
const writes = f.calls.filter(c => c.kind === 'update' && c.table === schema.createddocuments)
|
||||
assert.equal(writes.length, 1)
|
||||
assert.deepEqual(writes[0].values, { archived: true })
|
||||
})
|
||||
Reference in New Issue
Block a user