feat(documents): file scanner inbox documents with folder and type
All checks were successful
Build and Release FEDEO / verify (push) Successful in 1m0s
Build and Release FEDEO / build-and-release (push) Successful in 5m56s

This commit is contained in:
2026-09-29 16:48:15 +02:00
parent 2192fa5bb4
commit 052a3a8446
9 changed files with 400 additions and 14 deletions

View File

@@ -132,15 +132,20 @@ test("scanner inbox integration: migration, persistence, isolation, retries and
await pool.query(`CREATE TABLE tenants (id bigint PRIMARY KEY, locked text);
INSERT INTO tenants VALUES (1, null), (2, null);
CREATE TABLE files (${columns}, CHECK (name <> 'reject.pdf'));
CREATE TABLE filetags (id uuid PRIMARY KEY, tenant bigint, name text, archived boolean DEFAULT false);
CREATE TABLE folders (id uuid PRIMARY KEY, tenant bigint, name text, parent uuid, "standardFiletype" uuid,
"standardFiletypeIsOptional" boolean DEFAULT true, archived boolean DEFAULT false);
CREATE TABLE auth_profiles (tenant_id bigint, user_id uuid, customer_for_portal bigint);`)
const migrationRoot = path.resolve(__dirname, "../db/migrations")
const journal = JSON.parse(await readFile(path.join(migrationRoot, "meta/_journal.json"), "utf8"))
const entry = journal.entries.find((item: any) => item.tag === "0073_scanner_document_inbox")
assert.ok(entry)
assert.ok(journal.entries.filter((item: any) => item.idx < entry.idx).every((item: any) => item.when < entry.when))
const entries = ["0073_scanner_document_inbox", "0074_scanner_inbox_filing"].map(tag => journal.entries.find((item: any) => item.tag === tag))
await mkdir(path.join(tempMigrations, "meta"))
await writeFile(path.join(tempMigrations, "meta/_journal.json"), JSON.stringify({ ...journal, entries: [entry] }))
await writeFile(path.join(tempMigrations, entry.tag + ".sql"), await readFile(path.join(migrationRoot, entry.tag + ".sql")))
for (const entry of entries) {
assert.ok(entry)
assert.ok(journal.entries.filter((item: any) => item.idx < entry.idx).every((item: any) => item.when < entry.when))
await writeFile(path.join(tempMigrations, entry.tag + ".sql"), await readFile(path.join(migrationRoot, entry.tag + ".sql")))
}
await writeFile(path.join(tempMigrations, "meta/_journal.json"), JSON.stringify({ ...journal, entries }))
const db = drizzle(pool) as any
await migrate(db, { migrationsFolder: tempMigrations })
await migrate(db, { migrationsFolder: tempMigrations })
@@ -219,6 +224,83 @@ test("scanner inbox integration: migration, persistence, isolation, retries and
assert.equal((await pool.query("SELECT count(*) FROM files WHERE name IN ('failed.pdf', 'reject.pdf')")).rows[0].count, "0")
assert.equal((await send("failed.pdf")).statusCode, 201)
})
const targetFolder = randomUUID()
const targetType = randomUUID()
const alternativeType = randomUUID()
const foreignFolder = randomUUID()
const foreignType = randomUUID()
const archivedFolder = randomUUID()
const archivedType = randomUUID()
await pool.query(`INSERT INTO filetags (id, tenant, name, archived) VALUES
($1, 1, 'Rechnung', false), ($2, 1, 'Sonstiges', false), ($3, 2, 'Fremder Typ', false), ($4, 1, 'Archiviert', true)`,
[targetType, alternativeType, foreignType, archivedType])
await pool.query(`INSERT INTO folders (id, tenant, name, "standardFiletype", "standardFiletypeIsOptional", archived) VALUES
($1, 1, 'Rechnungen', $2, false, false), ($3, 2, 'Fremder Ordner', null, true, false), ($4, 1, 'Archiviert', null, true, true)`,
[targetFolder, targetType, foreignFolder, archivedFolder])
const fileInto = (id: string, folder: string | null = targetFolder, type: string = targetType, headers = {}) =>
app.inject({ method: "POST", url: `/api/document-inbox/${id}/file`, payload: { folder, type }, headers })
await t.test("filing options and destinations exclude other tenants and archived entries", async () => {
const options = await app.inject({ method: "GET", url: "/api/document-inbox/filing-options" })
assert.equal(options.statusCode, 200)
assert.deepEqual(options.json().folders.map((item: any) => item.id), [targetFolder])
assert.equal(options.json().folders[0].standardFiletypeIsOptional, false)
assert.deepEqual(options.json().types.map((item: any) => item.id).sort(), [targetType, alternativeType].sort())
const id = (await service.list(1, 50, 0, "scan.pdf")).documents[0].id
for (const [folder, type] of [[foreignFolder, targetType], [archivedFolder, targetType], [targetFolder, foreignType],
[null, archivedType], [targetFolder, alternativeType], [randomUUID(), targetType], [null, randomUUID()]]) {
assert.equal((await fileInto(id, folder, type!)).statusCode, 400)
}
assert.equal((await fileInto(id, null, targetType, { "x-test-tenant": "2" })).statusCode, 404)
assert.equal((await fileInto(randomUUID())).statusCode, 404)
for (const payload of [{ folder: null }, { type: targetType }, { folder: "invalid", type: targetType }, { folder: null, type: null }]) {
assert.equal((await app.inject({ method: "POST", url: `/api/document-inbox/${id}/file`, payload })).statusCode, 400)
}
await pool.query("UPDATE tenants SET locked='maintenance_tenant' WHERE id=1")
assert.equal((await fileInto(id)).statusCode, 423)
await pool.query("UPDATE tenants SET locked=null WHERE id=1")
await pool.query("INSERT INTO auth_profiles VALUES (1, $1, 123)", [userId])
assert.equal((await fileInto(id)).statusCode, 403)
assert.equal((await app.inject({ method: "GET", url: "/api/document-inbox/filing-options" })).statusCode, 403)
await pool.query("DELETE FROM auth_profiles")
assert.equal((await service.list(1, 50, 0, "scan.pdf")).total, 1)
const row = (await pool.query("SELECT folder, type FROM files WHERE id=$1", [id])).rows[0]
assert.deepEqual(row, { folder: null, type: null })
})
await t.test("filing updates the original file once, keeps its content and cannot be undone by scanner retries", async () => {
const id = (await service.list(1, 50, 0, "scan.pdf")).documents[0].id
const before = (await pool.query("SELECT * FROM files WHERE id=$1", [id])).rows[0]
const countBefore = stored.size
const responses = await Promise.all([fileInto(id, targetFolder, targetType, { "x-test-admin": "false" }), fileInto(id)])
assert.deepEqual(responses.map(response => response.statusCode).sort(), [200, 409])
const row = (await pool.query("SELECT * FROM files WHERE id=$1", [id])).rows[0]
assert.equal(row.folder, targetFolder)
assert.equal(row.type, targetType)
assert.equal(row.updated_by, userId)
assert.ok(row.updated_at)
assert.equal(row.path, before.path)
assert.equal(row.name, before.name)
assert.deepEqual(stored.get(row.path), pdf)
assert.equal((await service.list(1, 50, 0, "scan.pdf")).total, 0)
assert.equal((await send("scan.pdf")).statusCode, 204)
assert.equal((await service.list(1, 50, 0, "scan.pdf")).total, 0)
assert.equal((await service.list(2, 50, 0, "scan.pdf")).total, 1)
assert.equal(stored.size, countBefore)
assert.equal((await app.inject({ method: "HEAD", url: "/scanner-inbox/scan.pdf", headers: basic(first.username, first.password) })).statusCode, 200)
const rootId = (await service.list(1, 50, 0, "parallel.pdf")).documents[0].id
assert.equal((await fileInto(rootId, null, alternativeType)).statusCode, 200)
assert.deepEqual((await pool.query("SELECT folder, type FROM files WHERE id=$1", [rootId])).rows[0], { folder: null, type: alternativeType })
})
await t.test("a failure marking the receipt rolls back file classification", async () => {
const id = (await service.list(1, 50, 0, "failed.pdf")).documents[0].id
await pool.query("ALTER TABLE scanner_inbox_documents ADD CONSTRAINT simulate_filing_failure CHECK (remote_filename <> 'failed.pdf' OR filed_at IS NULL)")
assert.equal((await fileInto(id)).statusCode, 500)
assert.deepEqual((await pool.query("SELECT folder, type FROM files WHERE id=$1", [id])).rows[0], { folder: null, type: null })
assert.equal((await service.list(1, 50, 0, "failed.pdf")).total, 1)
await pool.query("ALTER TABLE scanner_inbox_documents DROP CONSTRAINT simulate_filing_failure")
await pool.query("UPDATE files SET archived=true WHERE id=$1", [id])
assert.equal((await fileInto(id)).statusCode, 404)
await pool.query("UPDATE files SET archived=false WHERE id=$1", [id])
})
await t.test("maintenance, portal access and administrator-only settings are enforced", async () => {
await pool.query("UPDATE tenants SET locked='maintenance_tenant' WHERE id=1")
assert.equal((await send("maintenance.pdf")).statusCode, 423)
@@ -243,6 +325,6 @@ test("scanner inbox integration: migration, persistence, isolation, retries and
assert.equal((await send("old.pdf")).statusCode, 401)
await assert.rejects(service.accept(oldIdentity!, "late.pdf", pdf), { statusCode: 401 })
assert.equal((await send("new.pdf", pdf, basic(first.username, rotated.json().connection.password))).statusCode, 201)
assert.ok((await service.list(1, 50, 0, "")).total >= 4)
assert.equal((await service.list(1, 50, 0, "")).total, 2)
})
})