En proformas, cambiar "documentSeriesId" de string a UniqueID

This commit is contained in:
David Arranz 2026-09-18 10:51:04 +02:00
parent 66074c2fd0
commit 847fa60709
12 changed files with 149 additions and 50 deletions

View File

@ -1,5 +1,4 @@
import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import test from "node:test";
import type { CatalogsPublicServicesType } from "@erp/catalogs/api";
@ -77,8 +76,8 @@ async function seedProformaDependencies(
const company = await createTestCompany(runtime.database, {
legalName: "Proforma transaction company",
});
const customerId = randomUUID();
const documentSeriesId = randomUUID();
const customerId = UniqueID.generateNewID().toPrimitive();
const documentSeriesId = UniqueID.generateNewID().toPrimitive();
await CustomerModel.create({
id: customerId,
@ -90,7 +89,7 @@ async function seedProformaDependencies(
city: "Madrid",
province: "Madrid",
postal_code: "28001",
country: "ES",
country_code: "ES",
status: "active",
language_code: "es",
currency_code: "EUR",
@ -114,7 +113,7 @@ async function seedProformaDependencies(
is_active: true,
});
await TaxRegimeModel.create({
id: randomUUID(),
id: UniqueID.generateNewID().toPrimitive(),
company_id: company.id,
code: "01",
description: "General",
@ -307,7 +306,7 @@ test("ProformaCreator persiste una Proforma mínima y registra Audit en MariaDB"
});
await TaxRegimeModel.create({
id: randomUUID(),
id: UniqueID.generateNewID().toPrimitive(),
company_id: company.id,
code: "01",
description: "General",
@ -322,7 +321,7 @@ test("ProformaCreator persiste una Proforma mínima y registra Audit en MariaDB"
});
assert.equal(loginResponse.status, 200);
const accessToken = (loginResponse.body as { access_token: string }).access_token;
const proformaId = randomUUID();
const proformaId = UniqueID.generateNewID().toPrimitive();
const response = await runtime.request("/api/v1/proformas", {
method: "POST",
@ -517,7 +516,7 @@ test("ProformaCreator revierte la creación si Audit falla en MariaDB", async (t
t.after(async () => runtime.stop());
const { company, customerId, documentSeriesId } = await seedProformaDependencies(runtime);
const proformaId = randomUUID();
const proformaId = UniqueID.generateNewID().toPrimitive();
const companyIdResult = UniqueID.create(company.id);
assert.equal(companyIdResult.isSuccess, true);
const mappedInput = buildProformaInputMappers().createInputMapper.map(
@ -567,7 +566,7 @@ test("ProformaUpdater revierte la actualización si Audit falla en MariaDB", asy
const { company, customerId } = await seedProformaDependencies(runtime);
const account = await createTestAccount(runtime.database, {
email: `proforma-updater-${randomUUID()}@example.test`,
email: `proforma-updater-${UniqueID.generateNewID().toPrimitive()}@example.test`,
password: "Password123!",
});
await createTestMembership(runtime.database, {
@ -581,7 +580,7 @@ test("ProformaUpdater revierte la actualización si Audit falla en MariaDB", asy
});
assert.equal(loginResponse.status, 200);
const accessToken = (loginResponse.body as { access_token: string }).access_token;
const proformaId = randomUUID();
const proformaId = UniqueID.generateNewID().toPrimitive();
const baselineResponse = await runtime.request("/api/v1/proformas", {
method: "POST",
headers: {

View File

@ -47,6 +47,7 @@ test("serializa el estado Audit de una Proforma real con sus items en orden", ()
assert.equal(state.currencyCode, "EUR");
assert.equal(state.paymentTermId, paymentTermId.toPrimitive());
assert.equal(state.paymentMethodId, null);
assert.equal(state.documentSeriesId, "01900000-0000-7000-8000-000000000002");
assert.deepEqual(state.recipient, {
name: "Cliente de prueba",

View File

@ -77,6 +77,14 @@ const toNullableString = (value: {
() => null
);
const toNullableUniqueId = (value: {
match: <R>(some: (item: { toPrimitive(): string }) => R, none: () => R) => R;
}): string | null =>
value.match(
(item) => item.toPrimitive(),
() => null
);
const toNullableScaledString = (value: {
match: <R>(some: (item: { value: number; scale: number }) => R, none: () => R) => R;
}): string | null =>
@ -115,7 +123,7 @@ export const toProformaAuditState = (proforma: Proforma): ProformaAuditState =>
return {
status: proforma.status.toPrimitive(),
archivedAt: toNullablePrimitive(proforma.archivedAt),
documentSeriesId: toNullableString(proforma.documentSeriesId),
documentSeriesId: toNullableUniqueId(proforma.documentSeriesId),
proformaSeriesCode: toNullablePrimitive(proforma.proformaSeriesCode),
proformaNumber: toNullablePrimitive(proforma.proformaNumber),
proformaReference: proforma.proformaReference.toPrimitive(),

View File

@ -5,7 +5,6 @@ import { UniqueID, type UniqueID as UniqueIDType } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import {
type IProformaCreateProps,
type IProformaItemCreateProps,
InvoiceNumber,
InvoiceSerie,
@ -64,6 +63,7 @@ export class ProformaCreator implements IProformaCreator {
const resolvedProps = resolvedPropsResult.data;
// Comprobamos que podemos crear la proforma con la serie indicada (si hay alguna)
const proformaSeriesValidationResult =
await this.deps.proformaSeriesValidator.ensureValidProformaSeries({
companyId,
@ -75,7 +75,7 @@ export class ProformaCreator implements IProformaCreator {
return Result.fail(proformaSeriesValidationResult.error);
}
// 1. Obtener siguiente número
// Obtener siguiente número de la serie
const numberResult = await this.deps.numberService.getNextForCompany(
companyId,
resolvedProps.proformaSeriesCode,
@ -86,6 +86,15 @@ export class ProformaCreator implements IProformaCreator {
return Result.fail(numberResult.error);
}
const documentSeriesIdResult = UniqueID.create(numberResult.data.documentSeriesId);
if (documentSeriesIdResult.isFailure) {
return Result.fail(documentSeriesIdResult.error);
}
const nextNumber = numberResult.data.number;
// Comprobamos que podemos emitir la proforma con la serie de factura objetivo indicada (si hay alguna)
const targetInvoiceSeriesValidationResult =
await this.deps.targetInvoiceSeriesValidator.ensureValidIssuedInvoiceSeries({
companyId,
@ -97,25 +106,31 @@ export class ProformaCreator implements IProformaCreator {
return Result.fail(targetInvoiceSeriesValidationResult.error);
}
// Creamos la referencia de proforma
const proformaReferenceResult = InvoiceNumber.create(numberResult.data.reference);
if (proformaReferenceResult.isFailure) {
return Result.fail(proformaReferenceResult.error);
}
const proformaNumberResult = InvoiceNumber.create(numberResult.data.number);
const proformaReference = proformaReferenceResult.data;
// Creamos el número de proforma
const proformaNumberResult = InvoiceNumber.create(nextNumber);
if (proformaNumberResult.isFailure) {
return Result.fail(proformaNumberResult.error);
}
const proformaNumber = proformaNumberResult.data;
const proformaSeriesCodeResult = InvoiceSerie.create(numberResult.data.documentSeriesCode);
if (proformaSeriesCodeResult.isFailure) {
return Result.fail(proformaSeriesCodeResult.error);
}
const proformaReference = proformaReferenceResult.data;
const proformaSeriesCode = proformaSeriesCodeResult.data;
// 2. Crear agregado
const proformaResult = Proforma.create(
@ -123,9 +138,9 @@ export class ProformaCreator implements IProformaCreator {
...resolvedPropsResult.data,
companyId,
archivedAt: Maybe.none(),
documentSeriesId: Maybe.some(numberResult.data.documentSeriesId),
proformaSeriesCode: Maybe.some(proformaSeriesCodeResult.data),
proformaNumber: Maybe.some(proformaNumberResult.data),
documentSeriesId: Maybe.some(documentSeriesIdResult.data),
proformaSeriesCode: Maybe.some(proformaSeriesCode),
proformaNumber: Maybe.some(proformaNumber),
proformaReference,
targetInvoiceSeriesCode: resolvedPropsResult.data.targetInvoiceSeriesCode,
},
@ -166,18 +181,7 @@ export class ProformaCreator implements IProformaCreator {
return Result.ok(verifyResult.data);
}
private async resolveCreateProps(
props: ProformaCreateInputProps,
transaction?: unknown
): Promise<
Result<
Omit<
IProformaCreateProps,
"proformaReference" | "proformaNumber" | "documentSeriesId" | "proformaSeriesCode"
>,
Error
>
> {
private async resolveCreateProps(props: ProformaCreateInputProps, transaction?: unknown) {
const customerResult = await this.deps.customerServices.getCustomerById(props.customerId, {
companyId: props.companyId,
transaction,

View File

@ -146,6 +146,10 @@ export class ProformaDuplicator implements IProformaDuplicator {
if (numberResult.isFailure) {
return Result.fail(numberResult.error);
}
const documentSeriesIdResult = UniqueID.create(numberResult.data.documentSeriesId);
if (documentSeriesIdResult.isFailure) {
return Result.fail(documentSeriesIdResult.error);
}
const referenceResult = InvoiceNumber.create(numberResult.data.reference);
const numberValueResult = InvoiceNumber.create(numberResult.data.number);
const seriesResult = InvoiceSerie.create(numberResult.data.documentSeriesCode);
@ -157,7 +161,7 @@ export class ProformaDuplicator implements IProformaDuplicator {
companyId: params.companyId,
status: InvoiceStatus.draft(),
archivedAt: Maybe.none(),
documentSeriesId: Maybe.some(numberResult.data.documentSeriesId),
documentSeriesId: Maybe.some(documentSeriesIdResult.data),
proformaSeriesCode: Maybe.some(seriesResult.data),
proformaNumber: Maybe.some(numberValueResult.data),
proformaReference: referenceResult.data,
@ -268,7 +272,7 @@ export class ProformaDuplicator implements IProformaDuplicator {
private async resolveDuplicateSeriesCode(params: {
companyId: UniqueID;
sourceDocumentSeriesId: Maybe<string>;
sourceDocumentSeriesId: Maybe<UniqueID>;
transaction?: unknown;
}): Promise<Result<Maybe<InvoiceSerie>, Error>> {
const listResult = await this.deps.documentSeriesServices.listActiveSeries({
@ -280,7 +284,7 @@ export class ProformaDuplicator implements IProformaDuplicator {
return Result.fail(listResult.error);
}
const sourceSeries = listResult.data.find(
(series) => series.id === params.sourceDocumentSeriesId.getOrUndefined()
(series) => series.id === params.sourceDocumentSeriesId.getOrUndefined()?.toPrimitive()
);
const selectedSeries = sourceSeries ?? listResult.data.find((series) => series.is_default);
if (!selectedSeries) {

View File

@ -1,6 +1,7 @@
import assert from "node:assert/strict";
import test from "node:test";
import { UniqueID } from "@repo/rdx-ddd";
import { Maybe } from "@repo/rdx-utils";
import { ProformaFullSnapshotBuilder } from "./proforma-full-snapshot-builder";
@ -22,7 +23,7 @@ test("detail devuelve payment_term enriquecido cuando existe", () => {
proforma: {
id: { toString: () => "11111111-1111-4111-8111-111111111111" },
companyId: { toString: () => "22222222-2222-4222-8222-222222222222" },
documentSeriesId: Maybe.none(),
documentSeriesId: Maybe.some(UniqueID.generateNewID()),
proformaSeriesCode: Maybe.none(),
proformaNumber: Maybe.none(),
proformaReference: { toString: () => "PF-1" },
@ -84,6 +85,7 @@ test("detail devuelve payment_term enriquecido cuando existe", () => {
});
assert.equal(snapshot.payment_term?.id, "44444444-4444-4444-8444-444444444444");
assert.equal(typeof snapshot.document_series_id, "string");
assert.equal(snapshot.payment_term?.name, "50/50");
assert.equal(snapshot.payment_term?.is_active, false);
assert.equal(snapshot.payment_term?.due_rules.length, 1);

View File

@ -40,7 +40,9 @@ export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder
id: proforma.id.toString(),
company_id: proforma.companyId.toString(),
document_series_id: maybeToNullable(proforma.documentSeriesId, (value) => value),
document_series_id: maybeToNullable(proforma.documentSeriesId, (value) =>
value.toPrimitive()
),
proforma_series_code: maybeToNullable(proforma.proformaSeriesCode, (value) =>
value.toString()
),

View File

@ -52,11 +52,12 @@ export interface IProformaCreateProps {
status: InvoiceStatus;
archivedAt: Maybe<UtcDate>;
documentSeriesId: Maybe<string>;
proformaSeriesCode: Maybe<InvoiceSerie>;
proformaNumber: Maybe<InvoiceNumber>;
proformaReference: InvoiceNumber;
targetInvoiceSeriesCode: Maybe<InvoiceSerie>;
documentSeriesId: Maybe<UniqueID>; // ID de la serie de documentos para la proforma (si se asigna automáticamente)
proformaSeriesCode: Maybe<InvoiceSerie>; // Serie de proforma (p.e. "PF-")
proformaNumber: Maybe<InvoiceNumber>; // Número de proforma (p.e. "65")
proformaReference: InvoiceNumber; // Referencia de la proforma (serie + numero, p.e. "PF-0065")
targetInvoiceSeriesCode: Maybe<InvoiceSerie>; // Serie de factura objetivo para la emisión de la proforma (si se va a emitir)
proformaDate: UtcDate;
operationDate: Maybe<UtcDate>;
@ -71,7 +72,7 @@ export interface IProformaCreateProps {
languageCode: LanguageCode;
currencyCode: CurrencyCode;
linkedInvoiceId: Maybe<UniqueID>;
linkedInvoiceId: Maybe<UniqueID>; // ID de la factura emitida asociada.
paymentMethodId: Maybe<UniqueID>;
paymentTermId: Maybe<UniqueID>;
@ -133,7 +134,7 @@ export interface IProforma {
status: InvoiceStatus;
archivedAt: Maybe<UtcDate>;
documentSeriesId: Maybe<string>;
documentSeriesId: Maybe<UniqueID>;
proformaSeriesCode: Maybe<InvoiceSerie>;
proformaNumber: Maybe<InvoiceNumber>;
targetInvoiceSeriesCode: Maybe<InvoiceSerie>;
@ -262,10 +263,8 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
private hasSameProps(candidateProps: ProformaInternalProps): boolean {
return (
this.sameMaybe(
this.documentSeriesId,
candidateProps.documentSeriesId,
(left, right) => left === right
this.sameMaybe(this.documentSeriesId, candidateProps.documentSeriesId, (left, right) =>
left.equals(right)
) &&
this.sameMaybe(this.proformaSeriesCode, candidateProps.proformaSeriesCode, (left, right) =>
left.equals(right)
@ -469,7 +468,7 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
return this.props.archivedAt;
}
public get documentSeriesId(): Maybe<string> {
public get documentSeriesId(): Maybe<UniqueID> {
return this.props.documentSeriesId;
}

View File

@ -1,6 +1,9 @@
import assert from "node:assert/strict";
import test from "node:test";
import { UniqueID } from "@repo/rdx-ddd";
import { Maybe } from "@repo/rdx-utils";
import { Proforma } from "../aggregates/proforma.aggregate";
import { ProformaRecipient } from "../value-objects";
@ -25,3 +28,17 @@ test("createProformaTestFixture construye una Proforma real", () => {
assert.equal(proforma.items.getAll().length, 0);
assert.equal(proforma.taxConfig.taxMode, "single");
});
test("Proforma compara documentSeriesId por valor", () => {
const proforma = createProformaTestFixture();
const sameSeriesId = UniqueID.create("01900000-0000-7000-8000-000000000002");
assert.equal(sameSeriesId.isSuccess, true);
const updateResult = proforma.update({
documentSeriesId: Maybe.some(sameSeriesId.data),
});
assert.equal(updateResult.isSuccess, true);
assert.equal(updateResult.data, false);
});

View File

@ -63,7 +63,9 @@ export const createProformaTestFixture = (
companyId: required(UniqueID.create("01900000-0000-7000-8000-000000000001")),
status: overrides.status ?? required(InvoiceStatus.create("draft")),
archivedAt: overrides.archivedAt ?? Maybe.none(),
documentSeriesId: Maybe.some("01900000-0000-7000-8000-000000000002"),
documentSeriesId: Maybe.some(
required(UniqueID.create("01900000-0000-7000-8000-000000000002"))
),
proformaSeriesCode: Maybe.none(),
proformaNumber: Maybe.none(),
proformaReference: required(InvoiceNumber.create("PF-0042")),

View File

@ -0,0 +1,61 @@
import assert from "node:assert/strict";
import test from "node:test";
import { createProformaTestFixture } from "../../../../../../domain/proformas/testing/proforma-test-fixture";
import type { ProformaModel } from "../../../../../common";
import { SequelizeProformaV2DomainMapper } from "./sequelize-proforma-v2-domain.mapper";
const createRawProforma = (documentSeriesId: string | null): ProformaModel => {
const mapper = new SequelizeProformaV2DomainMapper();
const persistence = mapper.mapToPersistence(createProformaTestFixture());
assert.equal(persistence.isSuccess, true);
return {
...persistence.data,
document_series_id: documentSeriesId,
items: [],
current_customer: {
name: "Cliente de prueba",
tin: "ESB1234567",
street: "Calle Uno 1",
street2: "Piso 2",
city: "Madrid",
postal_code: "28001",
province: "Madrid",
country_code: "ES",
},
} as unknown as ProformaModel;
};
test("SequelizeProformaV2DomainMapper conserva documentSeriesId UUID en el round trip", () => {
const documentSeriesId = "01900000-0000-7000-8000-000000000002";
const mapper = new SequelizeProformaV2DomainMapper();
const result = mapper.mapToDomain(createRawProforma(documentSeriesId));
assert.ok(result.isSuccess, result.isFailure ? result.error.message : "");
assert.equal(result.data.documentSeriesId.unwrap().toPrimitive(), documentSeriesId);
const persistence = mapper.mapToPersistence(result.data);
assert.equal(persistence.isSuccess, true);
assert.equal(persistence.data.document_series_id, documentSeriesId);
});
test("SequelizeProformaV2DomainMapper conserva documentSeriesId nulo", () => {
const mapper = new SequelizeProformaV2DomainMapper();
const result = mapper.mapToDomain(createRawProforma(null));
assert.ok(result.isSuccess, result.isFailure ? result.error.message : "");
assert.equal(result.data.documentSeriesId.isNone(), true);
const persistence = mapper.mapToPersistence(result.data);
assert.equal(persistence.isSuccess, true);
assert.equal(persistence.data.document_series_id, null);
});
test("SequelizeProformaV2DomainMapper rechaza documentSeriesId inválido", () => {
const result = new SequelizeProformaV2DomainMapper().mapToDomain(createRawProforma("invalid"));
assert.equal(result.isFailure, true);
});

View File

@ -57,7 +57,7 @@ export class SequelizeProformaV2DomainMapper extends SequelizeDomainMapper<
errors
);
const documentSeriesId = extractOrPushError(
maybeFromNullableResult(raw.document_series_id, (value) => Result.ok(String(value))),
maybeFromNullableResult(raw.document_series_id, (value) => UniqueID.create(String(value))),
"document_series_id",
errors
);
@ -299,7 +299,7 @@ export class SequelizeProformaV2DomainMapper extends SequelizeDomainMapper<
// Flags / estado / serie / número
status: source.status.toPrimitive(),
archived_at: maybeToNullable(source.archivedAt, (value) => value.toPrimitive()),
document_series_id: maybeToNullable(source.documentSeriesId, (value) => value),
document_series_id: maybeToNullable(source.documentSeriesId, (value) => value.toPrimitive()),
proforma_series_code: maybeToNullable(source.proformaSeriesCode, (value) =>
value.toPrimitive()
),