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)
165 lines
5.0 KiB
TypeScript
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)
|
|
})
|
|
})
|