rubis/apps/api/tests/functional/imports.spec.ts
ordinarthur 554ae4ba4a test(api): tests fonctionnels Invoices + Imports + Dashboard
invoices.spec.ts (10 cas) :
- Création : 201 + rubisEarned=1 (bonus saisie) + status=pending
- Création client à la volée : si nom non matché + email fourni → client créé
- 422 client_email_required si pas d'email pour création à la volée
- Si planId fourni : RelanceTasks scheduled créées (assertion sur DB count)
- Numéro unique par org : 422 sur duplicate (testant l'exception handler)
- mark-paid idempotent : 2e appel ne re-bumpe pas rubisEarned ni org.rubis_count
- mark-paid annule les RelanceTasks scheduled (passe à cancelled)
- Cross-org : user B → 404 sur mark-paid d'une facture de A
- GET /invoices : pagination + meta total/page
- GET /invoices/counts : agrège par status

imports.spec.ts (6 cas) :
- POST /upload mock JSON : 1 batch + N drafts en pending, mock OCR rempli les fields
- Refus > 20 fichiers
- Validate transforme draft en Invoice + status=validated + invoiceId set
- Validate sur draft déjà processed → 409 draft_already_processed
- Skip → status=skipped, idempotent
- DELETE batch → CASCADE supprime les drafts

dashboard.spec.ts (6 cas) :
- KPIs zéros sur org vierge
- factureToRelance compte les invoices pending
- Après mark-paid : encaisseCents et rubisCount bumpent (org.rubisCount agrégé)
- Activity vide sur org sans actions
- Activity loggue invoice_paid après mark-paid (label dans le feed)
- Top-late liste les clients avec invoices actives en retard (dueDate < today)
2026-05-06 15:53:14 +02:00

165 lines
5.0 KiB
TypeScript

import { test } from '@japa/runner'
import testUtils from '@adonisjs/core/services/test_utils'
import ImportDraft from '#models/import_draft'
import { createTestUser } from '../helpers/auth.js'
import { body, type ApiOk } from '../helpers/response.js'
type DraftShape = {
id: string
filename: string
status: 'pending' | 'validated' | 'skipped'
extracted: { clientName: string; numero: string }
edited: { clientName: string; numero: string }
}
type BatchShape = {
id: string
drafts: DraftShape[]
}
test.group('Imports — POST /invoices/upload (mock JSON)', (group) => {
group.each.setup(() => testUtils.db().withGlobalTransaction())
test('crée 1 batch + N drafts depuis filenames (mock OCR)', async ({
client,
assert,
}) => {
const { bearer } = await createTestUser()
const r = await client
.post('/api/v1/invoices/upload')
.headers(bearer)
.json({ filenames: ['facture-001.pdf', 'facture-002.pdf'] })
r.assertStatus(201)
const batch = body<ApiOk<BatchShape>>(r).data
assert.lengthOf(batch.drafts, 2)
for (const d of batch.drafts) {
assert.equal(d.status, 'pending')
assert.exists(d.extracted.clientName)
assert.exists(d.extracted.numero)
}
})
test('refuse > 20 fichiers (422)', async ({ client }) => {
const { bearer } = await createTestUser()
const filenames = Array.from({ length: 21 }, (_, i) => `f-${i}.pdf`)
const r = await client
.post('/api/v1/invoices/upload')
.headers(bearer)
.json({ filenames })
r.assertStatus(422)
})
})
test.group('Imports — Validate / Skip draft', (group) => {
group.each.setup(() => testUtils.db().withGlobalTransaction())
test('validate transforme un draft en Invoice + status=validated', async ({
client,
assert,
}) => {
const { bearer } = await createTestUser()
const upload = await client
.post('/api/v1/invoices/upload')
.headers(bearer)
.json({ filenames: ['facture-validate.pdf'] })
const batch = body<ApiOk<BatchShape>>(upload).data
const draft = batch.drafts[0]
const r = await client
.post(`/api/v1/invoices/import-batch/${batch.id}/drafts/${draft.id}/validate`)
.headers(bearer)
.json({
clientId: null,
clientName: 'Client OCR',
clientEmail: 'ocr@spec.test',
numero: 'F-OCR-01',
amountTtcCents: 50000,
issueDate: '2026-04-01T09:00:00.000Z',
dueDate: '2026-05-01T09:00:00.000Z',
planId: null,
})
r.assertStatus(201)
const fresh = await ImportDraft.findOrFail(draft.id)
assert.equal(fresh.status, 'validated')
assert.isNotNull(fresh.invoiceId)
})
test('validate sur draft déjà processed → 409', async ({ client }) => {
const { bearer } = await createTestUser()
const upload = await client
.post('/api/v1/invoices/upload')
.headers(bearer)
.json({ filenames: ['facture-once.pdf'] })
const batch = body<ApiOk<BatchShape>>(upload).data
const draft = batch.drafts[0]
const payload = {
clientId: null,
clientName: 'Client',
clientEmail: 'c@spec.test',
numero: 'F-OCR-X',
amountTtcCents: 10000,
issueDate: '2026-04-01T09:00:00.000Z',
dueDate: '2026-05-01T09:00:00.000Z',
planId: null,
}
await client
.post(`/api/v1/invoices/import-batch/${batch.id}/drafts/${draft.id}/validate`)
.headers(bearer)
.json(payload)
const r2 = await client
.post(`/api/v1/invoices/import-batch/${batch.id}/drafts/${draft.id}/validate`)
.headers(bearer)
.json({ ...payload, numero: 'F-OCR-Y' })
r2.assertStatus(409)
})
test('skip → status=skipped, idempotent', async ({ client, assert }) => {
const { bearer } = await createTestUser()
const upload = await client
.post('/api/v1/invoices/upload')
.headers(bearer)
.json({ filenames: ['facture-skip.pdf'] })
const batch = body<ApiOk<BatchShape>>(upload).data
const draft = batch.drafts[0]
const r = await client
.post(`/api/v1/invoices/import-batch/${batch.id}/drafts/${draft.id}/skip`)
.headers(bearer)
r.assertStatus(200)
const data = body<ApiOk<DraftShape>>(r).data
assert.equal(data.status, 'skipped')
// Re-skip = idempotent (200 toujours)
const again = await client
.post(`/api/v1/invoices/import-batch/${batch.id}/drafts/${draft.id}/skip`)
.headers(bearer)
again.assertStatus(200)
})
test('DELETE /import-batch/:id supprime le batch + drafts (cascade)', async ({
client,
assert,
}) => {
const { bearer } = await createTestUser()
const upload = await client
.post('/api/v1/invoices/upload')
.headers(bearer)
.json({ filenames: ['a.pdf', 'b.pdf'] })
const batch = body<ApiOk<BatchShape>>(upload).data
const del = await client
.delete(`/api/v1/invoices/import-batch/${batch.id}`)
.headers(bearer)
del.assertStatus(204)
const remaining = await ImportDraft.query().where('batch_id', batch.id)
assert.lengthOf(remaining, 0)
})
})