import assert from "node:assert/strict"; import { randomUUID } from "node:crypto"; import test from "node:test"; import { QueryTypes } from "sequelize"; import { DocumentSeriesNumberAssigner } from "../../../../../modules/document-series/src/api/application/services/document-series-number-assigner.ts"; import { SequelizeDocumentSeriesDomainMapper } from "../../../../../modules/document-series/src/api/infrastructure/persistence/sequelize/mappers/sequelize-document-series-domain.mapper.ts"; import { SequelizeDocumentSeriesRepository } from "../../../../../modules/document-series/src/api/infrastructure/persistence/sequelize/repositories/document-series.repository.ts"; import { buildCompanyHeaders, createTestAccount, createTestCompany, createTestMembership, loginByApi, } from "./http-test-fixtures.ts"; import { createHttpTestRuntime } from "./http-test-runtime.ts"; type SeriesRow = { id: string; company_id: string; document_type: string; code: string; name: string; prefix: string | null; next_number: number; is_default: number; is_active: number; has_assigned_numbers: number; }; async function createSession(runtime: Awaited>) { const account = await createTestAccount(runtime.database, { email: `document-series-${randomUUID()}@example.test`, password: "Password123!", }); const company = await createTestCompany(runtime.database, { legalName: "Document series test" }); await createTestMembership(runtime.database, { accountId: account.id, companyId: company.id }); const login = await loginByApi({ request: runtime.request, email: account.email, password: account.password, }); assert.equal(login.status, 200); assert.equal(typeof login.body, "object"); assert.notEqual(login.body, null); const accessToken = (login.body as { access_token: string }).access_token; return { company, headers: { ...buildCompanyHeaders({ accessToken, companyId: company.id }), "content-type": "application/json", }, }; } async function findSeries(runtime: Awaited>, id: string) { const rows = await runtime.database.query( "SELECT id, company_id, document_type, code, name, prefix, next_number, is_default, is_active, has_assigned_numbers FROM document_series WHERE id = :id", { replacements: { id }, type: QueryTypes.SELECT } ); return rows[0] ?? null; } async function createSeries(params: { runtime: Awaited>; headers: Record; code: string; documentType?: "proforma" | "issued_invoice"; nextNumber?: number; isDefault?: boolean; isActive?: boolean; }) { const response = await params.runtime.request("/api/v1/document-series", { method: "POST", headers: params.headers, body: JSON.stringify({ document_type: params.documentType ?? "proforma", code: params.code, name: `Series ${params.code}`, next_number: params.nextNumber ?? 100, padding: 3, is_default: params.isDefault ?? false, is_active: params.isActive ?? true, }), }); assert.equal(response.status, 201); assert.equal(typeof response.body, "object"); assert.notEqual(response.body, null); return response.body as { id: string }; } test("document-series HTTP persiste creación, asignación y avance en MariaDB", async (t) => { const runtime = await createHttpTestRuntime(); t.after(async () => runtime.stop()); const session = await createSession(runtime); const created = await createSeries({ runtime, headers: session.headers, code: "PF", nextNumber: 100, }); const createdRow = await findSeries(runtime, created.id); assert.ok(createdRow); assert.equal(createdRow.company_id, session.company.id); assert.equal(createdRow.code, "PF"); assert.equal(createdRow.document_type, "proforma"); assert.equal(createdRow.next_number, 100); assert.equal(createdRow.is_active, 1); assert.equal(createdRow.is_default, 0); assert.equal(createdRow.has_assigned_numbers, 0); const first = await runtime.request("/api/v1/document-series/assign-next", { method: "POST", headers: session.headers, body: JSON.stringify({ document_type: "proforma", series_code: "PF" }), }); assert.equal(first.status, 200); assert.deepEqual(first.body, { series_id: created.id, series_code: "PF", number: "100", reference: "100", }); const afterFirst = await findSeries(runtime, created.id); assert.ok(afterFirst); assert.equal(afterFirst.next_number, 101); assert.equal(afterFirst.has_assigned_numbers, 1); const second = await runtime.request("/api/v1/document-series/assign-next", { method: "POST", headers: session.headers, body: JSON.stringify({ document_type: "proforma", series_code: "PF" }), }); assert.equal(second.status, 200); assert.equal((second.body as { number: string }).number, "101"); const afterSecond = await findSeries(runtime, created.id); assert.ok(afterSecond); assert.equal(afterSecond.next_number, 102); assert.equal(afterSecond.has_assigned_numbers, 1); const advance = await runtime.request(`/api/v1/document-series/${created.id}/next-number`, { method: "PATCH", headers: session.headers, body: JSON.stringify({ next_number: 150 }), }); assert.equal(advance.status, 200); const afterAdvance = await findSeries(runtime, created.id); assert.ok(afterAdvance); assert.equal(afterAdvance.next_number, 150); assert.equal(afterAdvance.has_assigned_numbers, 1); for (const nextNumber of [150, 149]) { const rejected = await runtime.request(`/api/v1/document-series/${created.id}/next-number`, { method: "PATCH", headers: session.headers, body: JSON.stringify({ next_number: nextNumber }), }); assert.equal(rejected.status, 422); assert.equal((await findSeries(runtime, created.id))?.next_number, 150); } }); test("document-series HTTP mantiene default, estado, aislamiento y update restringido", async (t) => { const runtime = await createHttpTestRuntime(); t.after(async () => runtime.stop()); const session = await createSession(runtime); const a = await createSeries({ runtime, headers: session.headers, code: "A" }); const b = await createSeries({ runtime, headers: session.headers, code: "B" }); const invoice = await createSeries({ runtime, headers: session.headers, code: "F", documentType: "issued_invoice", isDefault: true, }); for (const id of [a.id, b.id]) { const response = await runtime.request(`/api/v1/document-series/${id}/default`, { method: "PATCH", headers: session.headers, }); assert.equal(response.status, 200); } assert.equal((await findSeries(runtime, a.id))?.is_default, 0); assert.equal((await findSeries(runtime, b.id))?.is_default, 1); assert.equal((await findSeries(runtime, invoice.id))?.is_default, 1); const createdDefault = await createSeries({ runtime, headers: session.headers, code: "C", isDefault: true, }); assert.equal((await findSeries(runtime, b.id))?.is_default, 0); assert.equal((await findSeries(runtime, createdDefault.id))?.is_default, 1); const disable = await runtime.request(`/api/v1/document-series/${b.id}/disable`, { method: "PATCH", headers: session.headers, }); assert.equal(disable.status, 204); assert.equal((await findSeries(runtime, b.id))?.is_active, 0); assert.equal((await findSeries(runtime, b.id))?.is_default, 0); const enable = await runtime.request(`/api/v1/document-series/${b.id}/enable`, { method: "PATCH", headers: session.headers, }); assert.equal(enable.status, 200); assert.equal((await findSeries(runtime, b.id))?.is_active, 1); assert.equal((await findSeries(runtime, b.id))?.is_default, 0); const update = await runtime.request(`/api/v1/document-series/${a.id}`, { method: "PUT", headers: session.headers, body: JSON.stringify({ name: "Renamed", prefix: "P-", code: "OTHER", next_number: 999, is_default: true, }), }); assert.equal(update.status, 200); const afterUpdate = await findSeries(runtime, a.id); assert.ok(afterUpdate); assert.equal(afterUpdate.name, "Renamed"); assert.equal(afterUpdate.prefix, "P-"); assert.equal(afterUpdate.code, "A"); assert.equal(afterUpdate.next_number, 100); assert.equal(afterUpdate.is_default, 0); const inactive = await createSeries({ runtime, headers: session.headers, code: "I", isActive: false, }); const inactiveDefault = await runtime.request(`/api/v1/document-series/${inactive.id}/default`, { method: "PATCH", headers: session.headers, }); assert.equal(inactiveDefault.status, 422); assert.equal((await findSeries(runtime, inactive.id))?.is_default, 0); const rejectedCreate = await runtime.request("/api/v1/document-series", { method: "POST", headers: session.headers, body: JSON.stringify({ document_type: "proforma", code: "INVALID", name: "Invalid default", next_number: 100, padding: 0, is_active: false, is_default: true, }), }); assert.equal(rejectedCreate.status, 422); const invalidRows = await runtime.database.query( "SELECT id, company_id, document_type, code, name, prefix, next_number, is_default, is_active, has_assigned_numbers FROM document_series WHERE company_id = :companyId AND code = :code", { replacements: { companyId: session.company.id, code: "INVALID" }, type: QueryTypes.SELECT } ); assert.equal(invalidRows.length, 0); const other = await createSession(runtime); const forbidden = await runtime.request(`/api/v1/document-series/${a.id}`, { headers: other.headers, }); assert.equal(forbidden.status, 404); assert.equal((await findSeries(runtime, a.id))?.company_id, session.company.id); }); test("document-series revierte contador y hasAssignedNumbers con una transacción MariaDB real", async (t) => { const runtime = await createHttpTestRuntime(); t.after(async () => runtime.stop()); const session = await createSession(runtime); const created = await createSeries({ runtime, headers: session.headers, code: "RB", nextNumber: 200, }); const assigner = new DocumentSeriesNumberAssigner( new SequelizeDocumentSeriesRepository( new SequelizeDocumentSeriesDomainMapper(), runtime.database ) ); await assert.rejects( () => runtime.database.transaction(async (transaction) => { const assigned = await assigner.assignNext({ companyId: session.company.id, documentType: "proforma", seriesCode: "RB", transaction, }); assert.equal(assigned.isSuccess, true); throw new Error("rollback requested by integration test"); }), /rollback requested by integration test/ ); const persisted = await findSeries(runtime, created.id); assert.ok(persisted); assert.equal(persisted.next_number, 200); assert.equal(persisted.has_assigned_numbers, 0); });