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)
This commit is contained in:
parent
691b5fd09f
commit
554ae4ba4a
156
apps/api/tests/functional/dashboard.spec.ts
Normal file
156
apps/api/tests/functional/dashboard.spec.ts
Normal file
@ -0,0 +1,156 @@
|
||||
import { test } from '@japa/runner'
|
||||
import testUtils from '@adonisjs/core/services/test_utils'
|
||||
import { createTestUser } from '../helpers/auth.js'
|
||||
import { body, type ApiOk } from '../helpers/response.js'
|
||||
|
||||
type Kpis = {
|
||||
rubisCount: number
|
||||
rubisThisMonth: number
|
||||
encaisseCents: number
|
||||
factureToRelance: number
|
||||
factureInRelance: number
|
||||
miseEnDemeurePending: number
|
||||
monthlyGoalProgress: number
|
||||
}
|
||||
|
||||
type ActivityEvent = { id: string; kind: string; label: string }
|
||||
type LatePayer = { clientId: string; name: string; lateInvoicesCount: number }
|
||||
|
||||
test.group('Dashboard — GET /dashboard/kpis', (group) => {
|
||||
group.each.setup(() => testUtils.db().withGlobalTransaction())
|
||||
|
||||
test('zéros sur une org vierge', async ({ client, assert }) => {
|
||||
const { bearer } = await createTestUser()
|
||||
const r = await client.get('/api/v1/dashboard/kpis').headers(bearer)
|
||||
r.assertStatus(200)
|
||||
|
||||
const k = body<ApiOk<Kpis>>(r).data
|
||||
assert.equal(k.rubisCount, 0)
|
||||
assert.equal(k.rubisThisMonth, 0)
|
||||
assert.equal(k.encaisseCents, 0)
|
||||
assert.equal(k.factureToRelance, 0)
|
||||
assert.equal(k.factureInRelance, 0)
|
||||
assert.equal(k.miseEnDemeurePending, 0)
|
||||
})
|
||||
|
||||
test('factureToRelance compte les pending', async ({ client, assert }) => {
|
||||
const { bearer } = await createTestUser()
|
||||
const c = await client
|
||||
.post('/api/v1/clients')
|
||||
.headers(bearer)
|
||||
.json({ name: 'Dash', email: 'dash@spec.test' })
|
||||
const cid = body<ApiOk<{ id: string }>>(c).data.id
|
||||
|
||||
await client.post('/api/v1/invoices').headers(bearer).json({
|
||||
clientId: cid,
|
||||
clientName: 'Dash',
|
||||
numero: 'F-DASH-01',
|
||||
amountTtcCents: 10000,
|
||||
issueDate: '2026-04-01T09:00:00.000Z',
|
||||
dueDate: '2026-05-01T09:00:00.000Z',
|
||||
})
|
||||
|
||||
const r = await client.get('/api/v1/dashboard/kpis').headers(bearer)
|
||||
assert.equal(body<ApiOk<Kpis>>(r).data.factureToRelance, 1)
|
||||
})
|
||||
|
||||
test('encaisseCents et rubisCount bumpent après mark-paid', async ({
|
||||
client,
|
||||
assert,
|
||||
}) => {
|
||||
const { bearer } = await createTestUser()
|
||||
const c = await client
|
||||
.post('/api/v1/clients')
|
||||
.headers(bearer)
|
||||
.json({ name: 'Paid Spec', email: 'paid@spec.test' })
|
||||
const cid = body<ApiOk<{ id: string }>>(c).data.id
|
||||
|
||||
const inv = await client.post('/api/v1/invoices').headers(bearer).json({
|
||||
clientId: cid,
|
||||
clientName: 'Paid Spec',
|
||||
numero: 'F-PAID-01',
|
||||
amountTtcCents: 250000,
|
||||
// issueDate/dueDate dans le mois courant pour rentrer dans rubisThisMonth
|
||||
issueDate: new Date().toISOString(),
|
||||
dueDate: new Date(Date.now() + 86_400_000).toISOString(),
|
||||
})
|
||||
const invId = body<ApiOk<{ id: string }>>(inv).data.id
|
||||
|
||||
await client.post(`/api/v1/invoices/${invId}/mark-paid`).headers(bearer)
|
||||
|
||||
const r = await client.get('/api/v1/dashboard/kpis').headers(bearer)
|
||||
const k = body<ApiOk<Kpis>>(r).data
|
||||
assert.equal(k.rubisCount, 1)
|
||||
assert.equal(k.encaisseCents, 250000)
|
||||
})
|
||||
})
|
||||
|
||||
test.group('Dashboard — GET /dashboard/activity', (group) => {
|
||||
group.each.setup(() => testUtils.db().withGlobalTransaction())
|
||||
|
||||
test('vide sur une org sans actions', async ({ client, assert }) => {
|
||||
const { bearer } = await createTestUser()
|
||||
const r = await client.get('/api/v1/dashboard/activity').headers(bearer)
|
||||
r.assertStatus(200)
|
||||
assert.deepEqual(body<ApiOk<ActivityEvent[]>>(r).data, [])
|
||||
})
|
||||
|
||||
test('mark-paid logge un event invoice_paid', async ({ client, assert }) => {
|
||||
const { bearer } = await createTestUser()
|
||||
const c = await client
|
||||
.post('/api/v1/clients')
|
||||
.headers(bearer)
|
||||
.json({ name: 'Activity', email: 'activity@spec.test' })
|
||||
const cid = body<ApiOk<{ id: string }>>(c).data.id
|
||||
|
||||
const inv = await client.post('/api/v1/invoices').headers(bearer).json({
|
||||
clientId: cid,
|
||||
clientName: 'Activity',
|
||||
numero: 'F-ACT-01',
|
||||
amountTtcCents: 50000,
|
||||
issueDate: '2026-04-01T09:00:00.000Z',
|
||||
dueDate: '2026-05-01T09:00:00.000Z',
|
||||
})
|
||||
const invId = body<ApiOk<{ id: string }>>(inv).data.id
|
||||
|
||||
await client.post(`/api/v1/invoices/${invId}/mark-paid`).headers(bearer)
|
||||
|
||||
const r = await client.get('/api/v1/dashboard/activity').headers(bearer)
|
||||
const events = body<ApiOk<ActivityEvent[]>>(r).data
|
||||
assert.isAbove(events.length, 0)
|
||||
assert.equal(events[0].kind, 'invoice_paid')
|
||||
})
|
||||
})
|
||||
|
||||
test.group('Dashboard — GET /dashboard/top-late', (group) => {
|
||||
group.each.setup(() => testUtils.db().withGlobalTransaction())
|
||||
|
||||
test('liste les clients avec factures actives en retard', async ({
|
||||
client,
|
||||
assert,
|
||||
}) => {
|
||||
const { bearer } = await createTestUser()
|
||||
const c = await client
|
||||
.post('/api/v1/clients')
|
||||
.headers(bearer)
|
||||
.json({ name: 'En Retard', email: 'late@spec.test' })
|
||||
const cid = body<ApiOk<{ id: string }>>(c).data.id
|
||||
|
||||
// dueDate dans le passé → en retard
|
||||
await client.post('/api/v1/invoices').headers(bearer).json({
|
||||
clientId: cid,
|
||||
clientName: 'En Retard',
|
||||
numero: 'F-LATE-01',
|
||||
amountTtcCents: 50000,
|
||||
issueDate: '2025-12-01T09:00:00.000Z',
|
||||
dueDate: '2025-12-31T09:00:00.000Z',
|
||||
})
|
||||
|
||||
const r = await client.get('/api/v1/dashboard/top-late').headers(bearer)
|
||||
r.assertStatus(200)
|
||||
const list = body<ApiOk<LatePayer[]>>(r).data
|
||||
assert.lengthOf(list, 1)
|
||||
assert.equal(list[0].name, 'En Retard')
|
||||
assert.equal(list[0].lateInvoicesCount, 1)
|
||||
})
|
||||
})
|
||||
164
apps/api/tests/functional/imports.spec.ts
Normal file
164
apps/api/tests/functional/imports.spec.ts
Normal file
@ -0,0 +1,164 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
294
apps/api/tests/functional/invoices.spec.ts
Normal file
294
apps/api/tests/functional/invoices.spec.ts
Normal file
@ -0,0 +1,294 @@
|
||||
import { test } from '@japa/runner'
|
||||
import testUtils from '@adonisjs/core/services/test_utils'
|
||||
import RelanceTask from '#models/relance_task'
|
||||
import Organization from '#models/organization'
|
||||
import { createTestUser, createTwoOrgs } from '../helpers/auth.js'
|
||||
import { body, type ApiOk, type ApiOkPaged } from '../helpers/response.js'
|
||||
|
||||
type InvoiceShape = {
|
||||
id: string
|
||||
organizationId: string
|
||||
clientId: string
|
||||
numero: string
|
||||
amountTtcCents: number
|
||||
status: string
|
||||
rubisEarned: number
|
||||
paidAt: string | null
|
||||
}
|
||||
|
||||
type ClientShape = { id: string; name: string }
|
||||
type PlanShape = { id: string; slug: string }
|
||||
|
||||
async function createClient(client: any, headers: Record<string, string>, name: string) {
|
||||
const r = await client
|
||||
.post('/api/v1/clients')
|
||||
.headers(headers)
|
||||
.json({ name, email: `${name.toLowerCase().replace(/\s+/g, '')}@spec.test` })
|
||||
return body<ApiOk<ClientShape>>(r).data
|
||||
}
|
||||
|
||||
async function getStandardPlan(client: any, headers: Record<string, string>) {
|
||||
const r = await client.get('/api/v1/plans/standard-30j').headers(headers)
|
||||
return body<ApiOk<PlanShape>>(r).data
|
||||
}
|
||||
|
||||
test.group('Invoices — POST /invoices', (group) => {
|
||||
group.each.setup(() => testUtils.db().withGlobalTransaction())
|
||||
|
||||
test('crée une facture (201) avec rubisEarned=1 (bonus saisie)', async ({
|
||||
client,
|
||||
assert,
|
||||
}) => {
|
||||
const { bearer } = await createTestUser()
|
||||
const c = await createClient(client, bearer, 'Boulangerie Spec')
|
||||
|
||||
const r = await client
|
||||
.post('/api/v1/invoices')
|
||||
.headers(bearer)
|
||||
.json({
|
||||
clientId: c.id,
|
||||
clientName: c.name,
|
||||
numero: 'F-2026-T01',
|
||||
amountTtcCents: 124000,
|
||||
issueDate: '2026-04-01T09:00:00.000Z',
|
||||
dueDate: '2026-05-01T09:00:00.000Z',
|
||||
})
|
||||
|
||||
r.assertStatus(201)
|
||||
const inv = body<ApiOk<InvoiceShape>>(r).data
|
||||
assert.equal(inv.rubisEarned, 1)
|
||||
assert.equal(inv.status, 'pending')
|
||||
})
|
||||
|
||||
test('crée à la volée un client si nom non matché + email fourni', async ({
|
||||
client,
|
||||
assert,
|
||||
}) => {
|
||||
const { bearer, accessToken } = await createTestUser()
|
||||
|
||||
const r = await client
|
||||
.post('/api/v1/invoices')
|
||||
.headers(bearer)
|
||||
.json({
|
||||
clientName: 'Nouveau Client',
|
||||
clientEmail: 'nouveau@spec.test',
|
||||
numero: 'F-2026-T02',
|
||||
amountTtcCents: 50000,
|
||||
issueDate: '2026-04-01T09:00:00.000Z',
|
||||
dueDate: '2026-05-01T09:00:00.000Z',
|
||||
})
|
||||
|
||||
r.assertStatus(201)
|
||||
|
||||
// Le client doit exister maintenant
|
||||
const list = await client.get('/api/v1/clients').headers(bearer)
|
||||
const names = body<ApiOk<Array<{ name: string }>>>(list).data.map((c) => c.name)
|
||||
assert.include(names, 'Nouveau Client')
|
||||
|
||||
// assert le token est bien valide
|
||||
assert.isString(accessToken)
|
||||
})
|
||||
|
||||
test('refuse création client à la volée sans email (422 client_email_required)', async ({
|
||||
client,
|
||||
assert,
|
||||
}) => {
|
||||
const { bearer } = await createTestUser()
|
||||
|
||||
const r = await client
|
||||
.post('/api/v1/invoices')
|
||||
.headers(bearer)
|
||||
.json({
|
||||
clientName: 'Sans Email',
|
||||
numero: 'F-2026-T03',
|
||||
amountTtcCents: 50000,
|
||||
issueDate: '2026-04-01T09:00:00.000Z',
|
||||
dueDate: '2026-05-01T09:00:00.000Z',
|
||||
})
|
||||
|
||||
r.assertStatus(422)
|
||||
const errors = body<{ errors: Array<{ code: string }> }>(r).errors
|
||||
assert.equal(errors[0].code, 'client_email_required')
|
||||
})
|
||||
|
||||
test('schedule des RelanceTasks si planId est fourni', async ({ client, assert }) => {
|
||||
const { bearer } = await createTestUser()
|
||||
const c = await createClient(client, bearer, 'Avec Plan')
|
||||
const plan = await getStandardPlan(client, bearer)
|
||||
|
||||
const r = await client
|
||||
.post('/api/v1/invoices')
|
||||
.headers(bearer)
|
||||
.json({
|
||||
clientId: c.id,
|
||||
clientName: c.name,
|
||||
planId: plan.id,
|
||||
numero: 'F-2026-T04',
|
||||
amountTtcCents: 50000,
|
||||
issueDate: '2026-04-01T09:00:00.000Z',
|
||||
dueDate: '2026-05-01T09:00:00.000Z',
|
||||
})
|
||||
|
||||
r.assertStatus(201)
|
||||
const inv = body<ApiOk<InvoiceShape>>(r).data
|
||||
const tasks = await RelanceTask.query().where('invoice_id', inv.id)
|
||||
// Le plan standard-30j a 3 steps
|
||||
assert.lengthOf(tasks, 3)
|
||||
for (const t of tasks) assert.equal(t.status, 'scheduled')
|
||||
})
|
||||
|
||||
test('numéro unique par org (422 duplicate)', async ({ client }) => {
|
||||
const { bearer } = await createTestUser()
|
||||
const c = await createClient(client, bearer, 'Dup Test')
|
||||
|
||||
const payload = {
|
||||
clientId: c.id,
|
||||
clientName: c.name,
|
||||
numero: 'F-2026-DUP',
|
||||
amountTtcCents: 50000,
|
||||
issueDate: '2026-04-01T09:00:00.000Z',
|
||||
dueDate: '2026-05-01T09:00:00.000Z',
|
||||
}
|
||||
|
||||
await client.post('/api/v1/invoices').headers(bearer).json(payload)
|
||||
const r2 = await client.post('/api/v1/invoices').headers(bearer).json(payload)
|
||||
r2.assertStatus(422)
|
||||
})
|
||||
})
|
||||
|
||||
test.group('Invoices — POST /invoices/:id/mark-paid', (group) => {
|
||||
group.each.setup(() => testUtils.db().withGlobalTransaction())
|
||||
|
||||
test('idempotent : 2e appel ne re-bumpe pas rubisEarned', async ({ client, assert }) => {
|
||||
const { bearer, org } = await createTestUser()
|
||||
const c = await createClient(client, bearer, 'Idem')
|
||||
const created = await client
|
||||
.post('/api/v1/invoices')
|
||||
.headers(bearer)
|
||||
.json({
|
||||
clientId: c.id,
|
||||
clientName: c.name,
|
||||
numero: 'F-2026-T10',
|
||||
amountTtcCents: 100000,
|
||||
issueDate: '2026-04-01T09:00:00.000Z',
|
||||
dueDate: '2026-05-01T09:00:00.000Z',
|
||||
})
|
||||
const id = body<ApiOk<InvoiceShape>>(created).data.id
|
||||
|
||||
const first = await client.post(`/api/v1/invoices/${id}/mark-paid`).headers(bearer)
|
||||
first.assertStatus(200)
|
||||
const afterFirst = body<ApiOk<InvoiceShape>>(first).data
|
||||
assert.equal(afterFirst.status, 'paid')
|
||||
assert.equal(afterFirst.rubisEarned, 2) // 1 saisie + 1 encaissement
|
||||
|
||||
const second = await client.post(`/api/v1/invoices/${id}/mark-paid`).headers(bearer)
|
||||
second.assertStatus(200)
|
||||
assert.equal(body<ApiOk<InvoiceShape>>(second).data.rubisEarned, 2) // pas re-bumpé
|
||||
|
||||
// Org rubisCount n'a été incrémenté qu'une fois
|
||||
const orgFresh = await Organization.findOrFail(org.id)
|
||||
assert.equal(orgFresh.rubisCount, 1)
|
||||
})
|
||||
|
||||
test("annule les RelanceTasks scheduled de la facture", async ({ client, assert }) => {
|
||||
const { bearer } = await createTestUser()
|
||||
const c = await createClient(client, bearer, 'Cancel Tasks')
|
||||
const plan = await getStandardPlan(client, bearer)
|
||||
|
||||
const created = await client
|
||||
.post('/api/v1/invoices')
|
||||
.headers(bearer)
|
||||
.json({
|
||||
clientId: c.id,
|
||||
clientName: c.name,
|
||||
planId: plan.id,
|
||||
numero: 'F-2026-T11',
|
||||
amountTtcCents: 100000,
|
||||
issueDate: '2026-04-01T09:00:00.000Z',
|
||||
dueDate: '2026-05-01T09:00:00.000Z',
|
||||
})
|
||||
const id = body<ApiOk<InvoiceShape>>(created).data.id
|
||||
|
||||
const beforeTasks = await RelanceTask.query().where('invoice_id', id)
|
||||
assert.lengthOf(beforeTasks, 3)
|
||||
|
||||
await client.post(`/api/v1/invoices/${id}/mark-paid`).headers(bearer)
|
||||
|
||||
const afterTasks = await RelanceTask.query().where('invoice_id', id)
|
||||
for (const t of afterTasks) assert.equal(t.status, 'cancelled')
|
||||
})
|
||||
|
||||
test('cross-org : user B ne peut pas mark-paid une facture de A (404)', async ({
|
||||
client,
|
||||
}) => {
|
||||
const { a, b } = await createTwoOrgs()
|
||||
const c = await createClient(client, a.bearer, 'Of A')
|
||||
const created = await client
|
||||
.post('/api/v1/invoices')
|
||||
.headers(a.bearer)
|
||||
.json({
|
||||
clientId: c.id,
|
||||
clientName: c.name,
|
||||
numero: 'F-2026-XO1',
|
||||
amountTtcCents: 50000,
|
||||
issueDate: '2026-04-01T09:00:00.000Z',
|
||||
dueDate: '2026-05-01T09:00:00.000Z',
|
||||
})
|
||||
const id = body<ApiOk<InvoiceShape>>(created).data.id
|
||||
|
||||
const r = await client.post(`/api/v1/invoices/${id}/mark-paid`).headers(b.bearer)
|
||||
r.assertStatus(404)
|
||||
})
|
||||
})
|
||||
|
||||
test.group('Invoices — GET /invoices', (group) => {
|
||||
group.each.setup(() => testUtils.db().withGlobalTransaction())
|
||||
|
||||
test('liste paginée + meta total/page', async ({ client, assert }) => {
|
||||
const { bearer } = await createTestUser()
|
||||
const c = await createClient(client, bearer, 'Liste Spec')
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await client
|
||||
.post('/api/v1/invoices')
|
||||
.headers(bearer)
|
||||
.json({
|
||||
clientId: c.id,
|
||||
clientName: c.name,
|
||||
numero: `F-2026-L${i}`,
|
||||
amountTtcCents: 10000 + i,
|
||||
issueDate: '2026-04-01T09:00:00.000Z',
|
||||
dueDate: '2026-05-01T09:00:00.000Z',
|
||||
})
|
||||
}
|
||||
|
||||
const r = await client.get('/api/v1/invoices').headers(bearer)
|
||||
r.assertStatus(200)
|
||||
const payload = body<ApiOkPaged<InvoiceShape[]>>(r)
|
||||
assert.equal(payload.meta.total, 3)
|
||||
assert.equal(payload.meta.page, 1)
|
||||
})
|
||||
|
||||
test('GET /invoices/counts agrège par status', async ({ client, assert }) => {
|
||||
const { bearer } = await createTestUser()
|
||||
const c = await createClient(client, bearer, 'Counts Spec')
|
||||
|
||||
await client
|
||||
.post('/api/v1/invoices')
|
||||
.headers(bearer)
|
||||
.json({
|
||||
clientId: c.id,
|
||||
clientName: c.name,
|
||||
numero: 'F-2026-C01',
|
||||
amountTtcCents: 10000,
|
||||
issueDate: '2026-04-01T09:00:00.000Z',
|
||||
dueDate: '2026-05-01T09:00:00.000Z',
|
||||
})
|
||||
|
||||
const r = await client.get('/api/v1/invoices/counts').headers(bearer)
|
||||
r.assertStatus(200)
|
||||
const c2 = body<ApiOk<{ all: number; pending: number }>>(r).data
|
||||
assert.equal(c2.all, 1)
|
||||
assert.equal(c2.pending, 1)
|
||||
})
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user