313 lines
11 KiB
TypeScript
313 lines
11 KiB
TypeScript
|
|
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<ReturnType<typeof createHttpTestRuntime>>) {
|
||
|
|
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<ReturnType<typeof createHttpTestRuntime>>, id: string) {
|
||
|
|
const rows = await runtime.database.query<SeriesRow>(
|
||
|
|
"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<ReturnType<typeof createHttpTestRuntime>>;
|
||
|
|
headers: Record<string, string>;
|
||
|
|
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<SeriesRow>(
|
||
|
|
"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);
|
||
|
|
});
|