Repaso de Verifactu

This commit is contained in:
David Arranz 2026-08-20 10:12:09 +02:00
parent bd630b29dd
commit 538c081d58
39 changed files with 419 additions and 667 deletions

View File

@ -0,0 +1,81 @@
-- Elimina las columnas legacy de snapshot VeriFactu de `issued_invoices`.
-- Ejecutar solo en una BD de desarrollo segura o mediante procedimiento controlado.
-- No ejecutar automaticamente en produccion.
SET @target_schema = DATABASE();
-- Comprobacion previa recomendada.
-- SELECT column_name, is_nullable, column_type
-- FROM information_schema.columns
-- WHERE table_schema = @target_schema
-- AND table_name = 'issued_invoices'
-- AND column_name IN (
-- 'verifactu_profile_status',
-- 'uses_verifactu',
-- 'verifactu_effective_at'
-- );
SET
@ddl = (
SELECT IF(
EXISTS (
SELECT 1
FROM information_schema.columns
WHERE
table_schema = @target_schema
AND table_name = 'issued_invoices'
AND column_name = 'verifactu_effective_at'
), 'ALTER TABLE issued_invoices DROP COLUMN verifactu_effective_at', 'SELECT ''verifactu_effective_at already dropped'''
)
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET
@ddl = (
SELECT IF(
EXISTS (
SELECT 1
FROM information_schema.columns
WHERE
table_schema = @target_schema
AND table_name = 'issued_invoices'
AND column_name = 'uses_verifactu'
), 'ALTER TABLE issued_invoices DROP COLUMN uses_verifactu', 'SELECT ''uses_verifactu already dropped'''
)
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET
@ddl = (
SELECT IF(
EXISTS (
SELECT 1
FROM information_schema.columns
WHERE
table_schema = @target_schema
AND table_name = 'issued_invoices'
AND column_name = 'verifactu_profile_status'
), 'ALTER TABLE issued_invoices DROP COLUMN verifactu_profile_status', 'SELECT ''verifactu_profile_status already dropped'''
)
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- Validacion minima posterior a la DDL.
-- SELECT column_name
-- FROM information_schema.columns
-- WHERE table_schema = DATABASE()
-- AND table_name = 'issued_invoices'
-- AND column_name IN (
-- 'verifactu_profile_status',
-- 'uses_verifactu',
-- 'verifactu_effective_at'
-- );

View File

@ -1,4 +1,3 @@
import type { CompanyVerifactuProfileStatus } from "../../../domain";
import type { Proforma } from "../../../domain"; import type { Proforma } from "../../../domain";
import type { import type {
InvocieTaxRegimeFullReadModel, InvocieTaxRegimeFullReadModel,
@ -22,9 +21,5 @@ export interface ProformaIssueReadModel {
operationQualificationCode: string | null; operationQualificationCode: string | null;
exemptionReasonCode: string | null; exemptionReasonCode: string | null;
}; };
verifactuPolicy: { usesVerifactu: boolean;
profileStatus: CompanyVerifactuProfileStatus;
usesVerifactu: boolean;
effectiveAt: Date | null;
};
} }

View File

@ -5,4 +5,4 @@ export * from "./issued-invoice-document-properties-factory";
export * from "./issued-invoice-finder"; export * from "./issued-invoice-finder";
export * from "./issued-invoice-public-services.interface"; export * from "./issued-invoice-public-services.interface";
export * from "./proforma-to-issued-invoice-props-converter"; export * from "./proforma-to-issued-invoice-props-converter";
export * from "./resolve-proforma-issue-verifactu-snapshots"; export * from "./resolve-proforma-issue-fiscal-context";

View File

@ -2,14 +2,12 @@ import assert from "node:assert/strict";
import test from "node:test"; import test from "node:test";
import { DiscountPercentage } from "@erp/core/api"; import { DiscountPercentage } from "@erp/core/api";
import { UniqueID, type UtcDate as UtcDateType } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import { CompanyVerifactuProfileStatus } from "../../../domain";
import { ProformaToIssuedInvoiceConverter } from "./proforma-to-issued-invoice-props-converter"; import { ProformaToIssuedInvoiceConverter } from "./proforma-to-issued-invoice-props-converter";
test("toCreateProps normaliza verifactu effectiveAt a YYYY-MM-DD antes de crear UtcDate", () => { test("toCreateProps no materializa VerifactuRecord ni policy en el converter", () => {
const converter = new ProformaToIssuedInvoiceConverter(); const converter = new ProformaToIssuedInvoiceConverter();
Object.assign(converter as object, { Object.assign(converter as object, {
@ -64,14 +62,9 @@ test("toCreateProps normaliza verifactu effectiveAt a YYYY-MM-DD antes de crear
operationQualificationCode: null, operationQualificationCode: null,
exemptionReasonCode: null, exemptionReasonCode: null,
}, },
verifactuPolicy: { usesVerifactu: true,
profileStatus: CompanyVerifactuProfileStatus.ObligatedWithVerifactu,
usesVerifactu: true,
effectiveAt: new Date("2026-08-20T08:30:00.000Z"),
},
}); });
assert.equal(result.isSuccess, true); assert.equal(result.isSuccess, true);
const effectiveAt = result.data.verifactuPolicy.unwrap().effectiveAt.unwrap() as UtcDateType; assert.equal(result.data.verifactu.isNone(), true);
assert.equal(effectiveAt.toPrimitive(), "2026-08-20");
}); });

View File

@ -15,7 +15,6 @@ import {
IssuedInvoiceRecipient, IssuedInvoiceRecipient,
IssuedInvoiceTax, IssuedInvoiceTax,
IssuedInvoiceTaxes, IssuedInvoiceTaxes,
IssuedInvoiceVerifactuPolicy,
type Proforma, type Proforma,
} from "../../../domain"; } from "../../../domain";
import type { ProformaIssueReadModel } from "../models"; import type { ProformaIssueReadModel } from "../models";
@ -85,12 +84,6 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic
return Result.fail(fiscalClassificationResult.error); return Result.fail(fiscalClassificationResult.error);
} }
const verifactuPolicyResult = this.resolveVerifactuPolicy(source);
if (verifactuPolicyResult.isFailure) {
return Result.fail(verifactuPolicyResult.error);
}
const proformaTotals = proforma.totals(); const proformaTotals = proforma.totals();
return Result.ok({ return Result.ok({
@ -115,7 +108,6 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic
paymentTerm: paymentTermResult.data, paymentTerm: paymentTermResult.data,
taxRegime: taxRegimeResult.data, taxRegime: taxRegimeResult.data,
fiscalClassification: Maybe.some(fiscalClassificationResult.data), fiscalClassification: Maybe.some(fiscalClassificationResult.data),
verifactuPolicy: Maybe.some(verifactuPolicyResult.data),
customerId: proforma.customerId, customerId: proforma.customerId,
recipient: recipientResult.data, recipient: recipientResult.data,
@ -361,28 +353,4 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic
: Maybe.some(source.fiscalClassification.exemptionReasonCode), : Maybe.some(source.fiscalClassification.exemptionReasonCode),
}); });
} }
private resolveVerifactuPolicy(
source: ProformaIssueReadModel
): Result<IssuedInvoiceVerifactuPolicy, Error> {
let effectiveAt = Maybe.none<UtcDate>();
if (source.verifactuPolicy.effectiveAt != null) {
const effectiveAtResult = UtcDate.createFromISO(
source.verifactuPolicy.effectiveAt.toISOString().slice(0, 10)
);
if (effectiveAtResult.isFailure) {
return Result.fail(effectiveAtResult.error);
}
effectiveAt = Maybe.some(effectiveAtResult.data);
}
return IssuedInvoiceVerifactuPolicy.create({
profileStatus: source.verifactuPolicy.profileStatus,
usesVerifactu: source.verifactuPolicy.usesVerifactu,
effectiveAt,
});
}
} }

View File

@ -8,7 +8,7 @@ import {
CompanyVerifactuProfile, CompanyVerifactuProfile,
CompanyVerifactuProfileStatus, CompanyVerifactuProfileStatus,
} from "../../../domain"; } from "../../../domain";
import { resolveProformaIssueVerifactuSnapshots } from "./resolve-proforma-issue-verifactu-snapshots"; import { resolveProformaIssueFiscalContext } from "./resolve-proforma-issue-fiscal-context";
function buildCompanyProfile(params?: { function buildCompanyProfile(params?: {
profileStatus?: CompanyVerifactuProfileStatus; profileStatus?: CompanyVerifactuProfileStatus;
@ -55,7 +55,7 @@ function buildCompanyProfile(params?: {
return result.data; return result.data;
} }
test("resolveProformaIssueVerifactuSnapshots prioriza document over customer and company", () => { test("resolveProformaIssueFiscalContext prioriza document over customer and company", () => {
const profile = buildCompanyProfile({ const profile = buildCompanyProfile({
taxRegimeCode: "03", taxRegimeCode: "03",
operationQualificationCode: "op-company", operationQualificationCode: "op-company",
@ -63,7 +63,7 @@ test("resolveProformaIssueVerifactuSnapshots prioriza document over customer and
verifactuEffectiveAt: new Date("2026-08-15T00:00:00.000Z"), verifactuEffectiveAt: new Date("2026-08-15T00:00:00.000Z"),
}); });
const result = resolveProformaIssueVerifactuSnapshots({ const result = resolveProformaIssueFiscalContext({
proformaTaxRegimeCode: Maybe.some("01"), proformaTaxRegimeCode: Maybe.some("01"),
customerTaxRegimeCode: Maybe.some("02"), customerTaxRegimeCode: Maybe.some("02"),
companyProfile: profile, companyProfile: profile,
@ -74,25 +74,23 @@ test("resolveProformaIssueVerifactuSnapshots prioriza document over customer and
assert.equal(result.fiscalClassification.exemptionReasonCode.unwrap(), "ex-company"); assert.equal(result.fiscalClassification.exemptionReasonCode.unwrap(), "ex-company");
}); });
test("resolveProformaIssueVerifactuSnapshots usa customer cuando documento no aporta tax regime", () => { test("resolveProformaIssueFiscalContext usa customer cuando documento no aporta tax regime", () => {
const profile = buildCompanyProfile({ const profile = buildCompanyProfile({
taxRegimeCode: "03", taxRegimeCode: "03",
profileStatus: CompanyVerifactuProfileStatus.ObligatedWithoutVerifactu, profileStatus: CompanyVerifactuProfileStatus.ObligatedWithoutVerifactu,
}); });
const result = resolveProformaIssueVerifactuSnapshots({ const result = resolveProformaIssueFiscalContext({
proformaTaxRegimeCode: Maybe.none(), proformaTaxRegimeCode: Maybe.none(),
customerTaxRegimeCode: Maybe.some("02"), customerTaxRegimeCode: Maybe.some("02"),
companyProfile: profile, companyProfile: profile,
}); });
assert.equal(result.taxRegimeCode.unwrap(), "02"); assert.equal(result.taxRegimeCode.unwrap(), "02");
assert.equal(result.verifactuPolicy.profileStatus, "obligated_without_verifactu"); assert.equal(result.usesVerifactu, false);
assert.equal(result.verifactuPolicy.usesVerifactu, false);
assert.equal(result.verifactuPolicy.effectiveAt.isNone(), true);
}); });
test("resolveProformaIssueVerifactuSnapshots usa company defaults y politica cuando faltan documento y customer", () => { test("resolveProformaIssueFiscalContext usa company defaults y politica cuando faltan documento y customer", () => {
const effectiveAt = new Date("2026-08-20T08:30:00.000Z"); const effectiveAt = new Date("2026-08-20T08:30:00.000Z");
const profile = buildCompanyProfile({ const profile = buildCompanyProfile({
taxRegimeCode: "05", taxRegimeCode: "05",
@ -101,20 +99,18 @@ test("resolveProformaIssueVerifactuSnapshots usa company defaults y politica cua
verifactuEffectiveAt: effectiveAt, verifactuEffectiveAt: effectiveAt,
}); });
const result = resolveProformaIssueVerifactuSnapshots({ const result = resolveProformaIssueFiscalContext({
proformaTaxRegimeCode: Maybe.none(), proformaTaxRegimeCode: Maybe.none(),
customerTaxRegimeCode: Maybe.none(), customerTaxRegimeCode: Maybe.none(),
companyProfile: profile, companyProfile: profile,
}); });
assert.equal(result.taxRegimeCode.unwrap(), "05"); assert.equal(result.taxRegimeCode.unwrap(), "05");
assert.equal(result.verifactuPolicy.profileStatus, "obligated_with_verifactu"); assert.equal(result.usesVerifactu, true);
assert.equal(result.verifactuPolicy.usesVerifactu, true);
assert.equal(result.verifactuPolicy.effectiveAt.unwrap(), effectiveAt);
}); });
test("resolveProformaIssueVerifactuSnapshots mantiene comportamiento legacy sin perfil", () => { test("resolveProformaIssueFiscalContext mantiene comportamiento legacy sin perfil", () => {
const result = resolveProformaIssueVerifactuSnapshots({ const result = resolveProformaIssueFiscalContext({
proformaTaxRegimeCode: Maybe.none(), proformaTaxRegimeCode: Maybe.none(),
customerTaxRegimeCode: Maybe.none(), customerTaxRegimeCode: Maybe.none(),
companyProfile: null, companyProfile: null,
@ -123,7 +119,5 @@ test("resolveProformaIssueVerifactuSnapshots mantiene comportamiento legacy sin
assert.equal(result.taxRegimeCode.isNone(), true); assert.equal(result.taxRegimeCode.isNone(), true);
assert.equal(result.fiscalClassification.operationQualificationCode.isNone(), true); assert.equal(result.fiscalClassification.operationQualificationCode.isNone(), true);
assert.equal(result.fiscalClassification.exemptionReasonCode.isNone(), true); assert.equal(result.fiscalClassification.exemptionReasonCode.isNone(), true);
assert.equal(result.verifactuPolicy.profileStatus, "not_configured"); assert.equal(result.usesVerifactu, false);
assert.equal(result.verifactuPolicy.usesVerifactu, false);
assert.equal(result.verifactuPolicy.effectiveAt.isNone(), true);
}); });

View File

@ -1,25 +1,20 @@
import { Maybe } from "@repo/rdx-utils"; import { Maybe } from "@repo/rdx-utils";
import type { CompanyVerifactuProfile } from "../../../domain"; import type { CompanyVerifactuProfile } from "../../../domain";
import { CompanyVerifactuProfileStatus } from "../../../domain";
export interface ResolveProformaIssueVerifactuSnapshotsInput { export interface ResolveProformaIssueFiscalContextInput {
proformaTaxRegimeCode: Maybe<string>; proformaTaxRegimeCode: Maybe<string>;
customerTaxRegimeCode: Maybe<string>; customerTaxRegimeCode: Maybe<string>;
companyProfile: CompanyVerifactuProfile | null; companyProfile: CompanyVerifactuProfile | null;
} }
export interface ResolveProformaIssueVerifactuSnapshotsOutput { export interface ResolveProformaIssueFiscalContextOutput {
taxRegimeCode: Maybe<string>; taxRegimeCode: Maybe<string>;
fiscalClassification: { fiscalClassification: {
operationQualificationCode: Maybe<string>; operationQualificationCode: Maybe<string>;
exemptionReasonCode: Maybe<string>; exemptionReasonCode: Maybe<string>;
}; };
verifactuPolicy: { usesVerifactu: boolean;
profileStatus: CompanyVerifactuProfileStatus;
usesVerifactu: boolean;
effectiveAt: Maybe<Date>;
};
} }
/** /**
@ -30,9 +25,9 @@ export interface ResolveProformaIssueVerifactuSnapshotsOutput {
* - operationQualificationCode: company -> sin valor * - operationQualificationCode: company -> sin valor
* - exemptionReasonCode: company default -> sin valor * - exemptionReasonCode: company default -> sin valor
*/ */
export function resolveProformaIssueVerifactuSnapshots( export function resolveProformaIssueFiscalContext(
input: ResolveProformaIssueVerifactuSnapshotsInput input: ResolveProformaIssueFiscalContextInput
): ResolveProformaIssueVerifactuSnapshotsOutput { ): ResolveProformaIssueFiscalContextOutput {
const companyProfile = input.companyProfile; const companyProfile = input.companyProfile;
const taxRegimeCode = input.proformaTaxRegimeCode.isSome() const taxRegimeCode = input.proformaTaxRegimeCode.isSome()
@ -46,24 +41,12 @@ export function resolveProformaIssueVerifactuSnapshots(
const exemptionReasonCode = const exemptionReasonCode =
companyProfile?.defaults.exemptionReasonCode ?? Maybe.none<string>(); companyProfile?.defaults.exemptionReasonCode ?? Maybe.none<string>();
const profileStatus = companyProfile?.profileStatus ?? CompanyVerifactuProfileStatus.NotConfigured;
const usesVerifactu =
profileStatus === CompanyVerifactuProfileStatus.ObligatedWithVerifactu;
const effectiveAt =
usesVerifactu && companyProfile?.verifactuEffectiveAt.isSome()
? companyProfile.verifactuEffectiveAt
: Maybe.none<Date>();
return { return {
taxRegimeCode, taxRegimeCode,
fiscalClassification: { fiscalClassification: {
operationQualificationCode, operationQualificationCode,
exemptionReasonCode, exemptionReasonCode,
}, },
verifactuPolicy: { usesVerifactu: companyProfile?.usesVerifactu() ?? false,
profileStatus,
usesVerifactu,
effectiveAt,
},
}; };
} }

View File

@ -53,9 +53,6 @@ export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnaps
const fiscal_classification = maybeToNullable(invoice.fiscalClassification, (value) => const fiscal_classification = maybeToNullable(invoice.fiscalClassification, (value) =>
value.toObjectString() value.toObjectString()
); );
const verifactu_policy = maybeToNullable(invoice.verifactuPolicy, (value) =>
value.toObjectString()
);
return { return {
id: invoice.id.toString(), id: invoice.id.toString(),
@ -89,7 +86,6 @@ export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnaps
payment_term, payment_term,
tax_regime, tax_regime,
fiscal_classification, fiscal_classification,
verifactu_policy,
subtotal_amount: invoice.subtotalAmount.toObjectString(), subtotal_amount: invoice.subtotalAmount.toObjectString(),

View File

@ -71,11 +71,6 @@ export interface IIssuedInvoiceFullSnapshot {
exemption_reason_code: string | null; exemption_reason_code: string | null;
exemption_reason_label: string | null; exemption_reason_label: string | null;
} | null; } | null;
verifactu_policy: {
profile_status: string;
uses_verifactu: boolean;
effective_at: string | null;
} | null;
subtotal_amount: { value: string; scale: string; currency_code: string }; subtotal_amount: { value: string; scale: string; currency_code: string };

View File

@ -69,7 +69,6 @@ test("reporting usa el payment term histórico del snapshot emitido", () => {
description: "Régimen general", description: "Régimen general",
}, },
fiscal_classification: null, fiscal_classification: null,
verifactu_policy: null,
subtotal_amount: MoneyDTOHelper.fromNumber(100, "EUR"), subtotal_amount: MoneyDTOHelper.fromNumber(100, "EUR"),
items_discount_amount: MoneyDTOHelper.fromNumber(0, "EUR"), items_discount_amount: MoneyDTOHelper.fromNumber(0, "EUR"),
global_discount_percentage: PercentageDTOHelper.fromNumber(0), global_discount_percentage: PercentageDTOHelper.fromNumber(0),

View File

@ -11,7 +11,7 @@ import type { Proforma } from "../../../../domain";
import type { InvoicePaymentTermReadModel } from "../../../common/models"; import type { InvoicePaymentTermReadModel } from "../../../common/models";
import type { ICompanyVerifactuProfileRepository } from "../../../company-verifactu-profile"; import type { ICompanyVerifactuProfileRepository } from "../../../company-verifactu-profile";
import type { ProformaIssueReadModel } from "../../../issued-invoices"; import type { ProformaIssueReadModel } from "../../../issued-invoices";
import { resolveProformaIssueVerifactuSnapshots } from "../../../issued-invoices"; import { resolveProformaIssueFiscalContext } from "../../../issued-invoices";
export interface IProformaIssueReadModelAssembler { export interface IProformaIssueReadModelAssembler {
assemble(params: { assemble(params: {
@ -94,7 +94,7 @@ export class ProformaIssueReadModelAssembler implements IProformaIssueReadModelA
return Result.fail(companyProfileResult.error); return Result.fail(companyProfileResult.error);
} }
const resolution = resolveProformaIssueVerifactuSnapshots({ const resolution = resolveProformaIssueFiscalContext({
proformaTaxRegimeCode: params.proforma.taxRegimeCode, proformaTaxRegimeCode: params.proforma.taxRegimeCode,
customerTaxRegimeCode: customerResult.data.taxRegimeCode, customerTaxRegimeCode: customerResult.data.taxRegimeCode,
companyProfile: companyProfileResult.data.getOrUndefined() ?? null, companyProfile: companyProfileResult.data.getOrUndefined() ?? null,
@ -126,11 +126,7 @@ export class ProformaIssueReadModelAssembler implements IProformaIssueReadModelA
exemptionReasonCode: exemptionReasonCode:
resolution.fiscalClassification.exemptionReasonCode.getOrUndefined() ?? null, resolution.fiscalClassification.exemptionReasonCode.getOrUndefined() ?? null,
}, },
verifactuPolicy: { usesVerifactu: resolution.usesVerifactu,
profileStatus: resolution.verifactuPolicy.profileStatus,
usesVerifactu: resolution.verifactuPolicy.usesVerifactu,
effectiveAt: resolution.verifactuPolicy.effectiveAt.getOrUndefined() ?? null,
},
}); });
} }

View File

@ -31,9 +31,6 @@ export class IssueProformaResponseSnapshotBuilder implements IIssueProformaRespo
fiscal_classification: maybeToNullable(invoice.fiscalClassification, (value) => fiscal_classification: maybeToNullable(invoice.fiscalClassification, (value) =>
value.toObjectString() value.toObjectString()
), ),
verifactu_policy: maybeToNullable(invoice.verifactuPolicy, (value) =>
value.toObjectString()
),
totals: { totals: {
taxable_base: invoice.taxableAmount.toObjectString(), taxable_base: invoice.taxableAmount.toObjectString(),
tax_total: invoice.taxesAmount.toObjectString(), tax_total: invoice.taxesAmount.toObjectString(),

View File

@ -6,8 +6,9 @@ import type { ITransactionManager } from "@erp/core/api";
import { CurrencyCode, LanguageCode, UtcDate, UniqueID } from "@repo/rdx-ddd"; import { CurrencyCode, LanguageCode, UtcDate, UniqueID } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import type { ProformaToIssuedInvoiceCreateProps } from "../../issued-invoices/services/proforma-to-issued-invoice-props-converter";
import type { IIssuedInvoicePublicServices } from "../../issued-invoices/services/issued-invoice-public-services.interface";
import { import {
CompanyVerifactuProfileStatus,
InvoiceNumber, InvoiceNumber,
InvoiceStatus, InvoiceStatus,
Proforma, Proforma,
@ -17,6 +18,8 @@ import {
import { ProformaIssuer } from "../services/proforma-issuer"; import { ProformaIssuer } from "../services/proforma-issuer";
import { IssueProformaUseCase } from "./issue-proforma.use-case"; import { IssueProformaUseCase } from "./issue-proforma.use-case";
type CreateIssuedInvoiceProps = Parameters<IIssuedInvoicePublicServices["createIssuedInvoice"]>[1];
const buildTransactionManager = (): ITransactionManager => ({ const buildTransactionManager = (): ITransactionManager => ({
start: async () => undefined, start: async () => undefined,
getTransaction: () => undefined, getTransaction: () => undefined,
@ -25,6 +28,11 @@ const buildTransactionManager = (): ITransactionManager => ({
rollback: async () => undefined, rollback: async () => undefined,
}); });
const buildCreateIssuedInvoiceProps = (): CreateIssuedInvoiceProps =>
({
verifactu: Maybe.none(),
}) as CreateIssuedInvoiceProps;
const buildIssueableProforma = (params?: { const buildIssueableProforma = (params?: {
operationDate?: Maybe<UtcDate>; operationDate?: Maybe<UtcDate>;
}) => { }) => {
@ -190,11 +198,7 @@ test("issue falla antes de persistir si la proforma no tiene operation date", as
operationQualificationCode: null, operationQualificationCode: null,
exemptionReasonCode: null, exemptionReasonCode: null,
}, },
verifactuPolicy: { usesVerifactu: false,
profileStatus: CompanyVerifactuProfileStatus.NotConfigured,
usesVerifactu: false,
effectiveAt: null,
},
}), }),
} as never, } as never,
issueResponseSnapshotBuilder: { issueResponseSnapshotBuilder: {
@ -219,3 +223,185 @@ test("issue falla antes de persistir si la proforma no tiene operation date", as
assert.equal(createIssuedInvoiceCalls, 0); assert.equal(createIssuedInvoiceCalls, 0);
assert.equal(markAsIssuedCalls, 0); assert.equal(markAsIssuedCalls, 0);
}); });
test("issue crea VerifactuRecord pending cuando la empresa usa VeriFactu", async () => {
const proforma = buildIssueableProforma();
let capturedProps: CreateIssuedInvoiceProps | undefined;
const useCase = new IssueProformaUseCase({
issuedInvoiceServices: {
createIssuedInvoice: async (_id: UniqueID, props: CreateIssuedInvoiceProps) => {
capturedProps = props;
return Result.ok({ id: UniqueID.generateNewID() } as never);
},
} as never,
finder: {
findProformaById: async () => Result.ok(proforma),
} as never,
issuer: {
issueProforma: (): Result<ProformaToIssuedInvoiceCreateProps, Error> =>
Result.ok(buildCreateIssuedInvoiceProps()),
} as never,
issueReadModelAssembler: {
assemble: async () =>
Result.ok({
proforma,
paymentMethod: {
id: UniqueID.generateNewID(),
name: "Transferencia",
description: Maybe.none(),
},
paymentTerm: Maybe.none(),
taxRegime: {
code: "01",
description: "Régimen general",
},
fiscalClassification: {
operationQualificationCode: null,
exemptionReasonCode: null,
},
usesVerifactu: true,
}),
} as never,
issueResponseSnapshotBuilder: {
toOutput: () => ({ id: "unused" }),
} as never,
repository: {
markAsIssuedByIdInCompany: async () => Result.ok(undefined),
} as never,
transactionManager: buildTransactionManager(),
});
const result = await useCase.execute({
companyId: proforma.companyId.toPrimitive(),
proformaId: proforma.id.toPrimitive(),
});
assert.equal(result.isSuccess, true);
if (!capturedProps) {
assert.fail("Expected createIssuedInvoice to be called");
}
assert.equal(capturedProps.verifactu.isSome(), true);
const verifactuRecord = capturedProps.verifactu.unwrap();
assert.equal(verifactuRecord.estado.toPrimitive(), "Pendiente");
});
test("issue no crea VerifactuRecord cuando la empresa no usa VeriFactu", async () => {
const proforma = buildIssueableProforma();
let capturedProps: CreateIssuedInvoiceProps | undefined;
const useCase = new IssueProformaUseCase({
issuedInvoiceServices: {
createIssuedInvoice: async (_id: UniqueID, props: CreateIssuedInvoiceProps) => {
capturedProps = props;
return Result.ok({ id: UniqueID.generateNewID() } as never);
},
} as never,
finder: {
findProformaById: async () => Result.ok(proforma),
} as never,
issuer: {
issueProforma: (): Result<ProformaToIssuedInvoiceCreateProps, Error> =>
Result.ok(buildCreateIssuedInvoiceProps()),
} as never,
issueReadModelAssembler: {
assemble: async () =>
Result.ok({
proforma,
paymentMethod: {
id: UniqueID.generateNewID(),
name: "Transferencia",
description: Maybe.none(),
},
paymentTerm: Maybe.none(),
taxRegime: {
code: "01",
description: "Régimen general",
},
fiscalClassification: {
operationQualificationCode: null,
exemptionReasonCode: null,
},
usesVerifactu: false,
}),
} as never,
issueResponseSnapshotBuilder: {
toOutput: () => ({ id: "unused" }),
} as never,
repository: {
markAsIssuedByIdInCompany: async () => Result.ok(undefined),
} as never,
transactionManager: buildTransactionManager(),
});
const result = await useCase.execute({
companyId: proforma.companyId.toPrimitive(),
proformaId: proforma.id.toPrimitive(),
});
assert.equal(result.isSuccess, true);
if (!capturedProps) {
assert.fail("Expected createIssuedInvoice to be called");
}
assert.equal(capturedProps.verifactu.isNone(), true);
});
test("issue propaga el fallo de persistencia VeriFactu y no marca la proforma como emitida", async () => {
const proforma = buildIssueableProforma();
let markAsIssuedCalls = 0;
const useCase = new IssueProformaUseCase({
issuedInvoiceServices: {
createIssuedInvoice: async () => Result.fail(new Error("verifactu insert failed")),
} as never,
finder: {
findProformaById: async () => Result.ok(proforma),
} as never,
issuer: {
issueProforma: (): Result<ProformaToIssuedInvoiceCreateProps, Error> =>
Result.ok(buildCreateIssuedInvoiceProps()),
} as never,
issueReadModelAssembler: {
assemble: async () =>
Result.ok({
proforma,
paymentMethod: {
id: UniqueID.generateNewID(),
name: "Transferencia",
description: Maybe.none(),
},
paymentTerm: Maybe.none(),
taxRegime: {
code: "01",
description: "Régimen general",
},
fiscalClassification: {
operationQualificationCode: null,
exemptionReasonCode: null,
},
usesVerifactu: true,
}),
} as never,
issueResponseSnapshotBuilder: {
toOutput: () => ({ id: "unused" }),
} as never,
repository: {
markAsIssuedByIdInCompany: async () => {
markAsIssuedCalls += 1;
return Result.ok(undefined);
},
} as never,
transactionManager: buildTransactionManager(),
});
const result = await useCase.execute({
companyId: proforma.companyId.toPrimitive(),
proformaId: proforma.id.toPrimitive(),
});
assert.equal(result.isFailure, true);
assert.match(result.error.message, /verifactu insert failed/i);
assert.equal(markAsIssuedCalls, 0);
});

View File

@ -1,8 +1,9 @@
import type { ITransactionManager } from "@erp/core/api"; import type { ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import type { IIssuedInvoicePublicServices } from "../../issued-invoices"; import type { IIssuedInvoicePublicServices } from "../../issued-invoices";
import { VerifactuRecord, VerifactuRecordEstado } from "../../../domain";
import type { IssueProformaCommand } from "../commands"; import type { IssueProformaCommand } from "../commands";
import type { IProformaRepository } from "../repositories"; import type { IProformaRepository } from "../repositories";
import type { import type {
@ -87,6 +88,22 @@ export class IssueProformaUseCase {
return Result.fail(createPropsResult.error); return Result.fail(createPropsResult.error);
} }
if (issueReadModelResult.data.usesVerifactu) {
const pendingVerifactuRecordResult = VerifactuRecord.create({
estado: VerifactuRecordEstado.createPendiente(),
qrCode: Maybe.none(),
url: Maybe.none(),
uuid: Maybe.none(),
operacion: Maybe.none(),
});
if (pendingVerifactuRecordResult.isFailure) {
return Result.fail(pendingVerifactuRecordResult.error);
}
createPropsResult.data.verifactu = Maybe.some(pendingVerifactuRecordResult.data);
}
const invoiceResult = await this.deps.issuedInvoiceServices.createIssuedInvoice( const invoiceResult = await this.deps.issuedInvoiceServices.createIssuedInvoice(
issuedInvoiceId, issuedInvoiceId,
createPropsResult.data, createPropsResult.data,

View File

@ -31,7 +31,6 @@ import { IssuedInvoiceItemMismatch } from "../errors";
import type { import type {
IssuedInvoiceFiscalClassification, IssuedInvoiceFiscalClassification,
IssuedInvoiceRecipient, IssuedInvoiceRecipient,
IssuedInvoiceVerifactuPolicy,
} from "../value-objects"; } from "../value-objects";
export interface IIssuedInvoiceCreateProps { export interface IIssuedInvoiceCreateProps {
@ -82,7 +81,6 @@ export interface IIssuedInvoiceCreateProps {
totalAmount: InvoiceAmount; totalAmount: InvoiceAmount;
fiscalClassification: Maybe<IssuedInvoiceFiscalClassification>; fiscalClassification: Maybe<IssuedInvoiceFiscalClassification>;
verifactuPolicy: Maybe<IssuedInvoiceVerifactuPolicy>;
verifactu: Maybe<VerifactuRecord>; verifactu: Maybe<VerifactuRecord>;
} }
@ -182,16 +180,6 @@ export class IssuedInvoice
); );
} }
if (props.verifactuPolicy.isNone()) {
return Result.fail(
new DomainValidationError(
"MISSING_VERIFACTU_POLICY",
"verifactuPolicy",
"Issued invoice requires a VeriFactu policy snapshot"
)
);
}
return Result.ok(); return Result.ok();
} }
@ -307,10 +295,6 @@ export class IssuedInvoice
return this.props.fiscalClassification; return this.props.fiscalClassification;
} }
public get verifactuPolicy(): Maybe<IssuedInvoiceVerifactuPolicy> {
return this.props.verifactuPolicy;
}
public get subtotalAmount(): InvoiceAmount { public get subtotalAmount(): InvoiceAmount {
return this.props.subtotalAmount; return this.props.subtotalAmount;
} }

View File

@ -1,5 +1,4 @@
export * from "./issued-invoice-fiscal-classification.vo"; export * from "./issued-invoice-fiscal-classification.vo";
export * from "./issued-invoice-recipient.vo"; export * from "./issued-invoice-recipient.vo";
export * from "./issued-invoice-tax-group.vo"; export * from "./issued-invoice-tax-group.vo";
export * from "./issued-invoice-verifactu-policy.vo";
export * from "./verifactu-status.vo"; export * from "./verifactu-status.vo";

View File

@ -1,59 +0,0 @@
import assert from "node:assert/strict";
import test from "node:test";
import { UtcDate } from "@repo/rdx-ddd";
import { Maybe } from "@repo/rdx-utils";
import { IssuedInvoiceVerifactuPolicy } from "./issued-invoice-verifactu-policy.vo";
test("IssuedInvoiceVerifactuPolicy exige effectiveAt cuando usesVerifactu es true", () => {
const result = IssuedInvoiceVerifactuPolicy.create({
profileStatus: "obligated_with_verifactu",
usesVerifactu: true,
effectiveAt: Maybe.none(),
});
assert.equal(result.isFailure, true);
assert.match(result.error.message, /requires effectiveAt/i);
});
test("IssuedInvoiceVerifactuPolicy rechaza effectiveAt cuando profileStatus no usa VeriFactu", async () => {
const effectiveAt = UtcDate.createFromISO("2026-08-20T08:30:00.000Z");
if (effectiveAt.isFailure) {
throw effectiveAt.error;
}
const result = IssuedInvoiceVerifactuPolicy.create({
profileStatus: "not_configured",
usesVerifactu: false,
effectiveAt: Maybe.some(effectiveAt.data),
});
assert.equal(result.isFailure, true);
assert.match(result.error.message, /cannot define effectiveAt/i);
});
test("IssuedInvoiceVerifactuPolicy serializa la politica vigente", async () => {
const effectiveAt = UtcDate.createFromISO("2026-08-20T08:30:00.000Z");
if (effectiveAt.isFailure) {
throw effectiveAt.error;
}
const result = IssuedInvoiceVerifactuPolicy.create({
profileStatus: "obligated_with_verifactu",
usesVerifactu: true,
effectiveAt: Maybe.some(effectiveAt.data),
});
if (result.isFailure) {
throw result.error;
}
assert.deepEqual(result.data.toObjectString(), {
profile_status: "obligated_with_verifactu",
uses_verifactu: true,
effective_at: "2026-08-20T08:30:00.000Z",
});
});

View File

@ -1,72 +0,0 @@
import { type UtcDate, ValueObject, maybeToNullable } from "@repo/rdx-ddd";
import { type Maybe, Result } from "@repo/rdx-utils";
import type { CompanyVerifactuProfileStatus } from "../../company-verifactu-profile";
import { CompanyVerifactuProfileStatus as CompanyVerifactuProfileStatusValues } from "../../company-verifactu-profile";
export type IssuedInvoiceVerifactuPolicyProps = {
profileStatus: CompanyVerifactuProfileStatus;
usesVerifactu: boolean;
effectiveAt: Maybe<UtcDate>;
};
export class IssuedInvoiceVerifactuPolicy extends ValueObject<IssuedInvoiceVerifactuPolicyProps> {
protected static validate(values: IssuedInvoiceVerifactuPolicyProps) {
const expectedUsesVerifactu =
values.profileStatus === CompanyVerifactuProfileStatusValues.ObligatedWithVerifactu;
if (values.usesVerifactu !== expectedUsesVerifactu) {
return Result.fail(new Error("Issued invoice VeriFactu policy usesVerifactu does not match profile status"));
}
if (values.usesVerifactu && values.effectiveAt.isNone()) {
return Result.fail(new Error("Issued invoice VeriFactu policy requires effectiveAt when VeriFactu is used"));
}
if (!values.usesVerifactu && values.effectiveAt.isSome()) {
return Result.fail(new Error("Issued invoice VeriFactu policy cannot define effectiveAt when VeriFactu is not used"));
}
return Result.ok(values);
}
public static create(
values: IssuedInvoiceVerifactuPolicyProps
): Result<IssuedInvoiceVerifactuPolicy, Error> {
const validationResult = IssuedInvoiceVerifactuPolicy.validate(values);
if (validationResult.isFailure) {
return Result.fail(validationResult.error);
}
return Result.ok(new IssuedInvoiceVerifactuPolicy(values));
}
public get profileStatus(): CompanyVerifactuProfileStatus {
return this.props.profileStatus;
}
public get usesVerifactu(): boolean {
return this.props.usesVerifactu;
}
public get effectiveAt(): Maybe<UtcDate> {
return this.props.effectiveAt;
}
public getProps(): IssuedInvoiceVerifactuPolicyProps {
return this.props;
}
public toPrimitive(): IssuedInvoiceVerifactuPolicyProps {
return this.getProps();
}
public toObjectString() {
return {
profile_status: this.profileStatus,
uses_verifactu: this.usesVerifactu,
effective_at: maybeToNullable(this.effectiveAt, (value) => value.toISOString()),
};
}
}

View File

@ -75,9 +75,6 @@ export class IssuedInvoiceModel extends Model<
declare tax_regime_description: string; declare tax_regime_description: string;
declare operation_qualification_code: CreationOptional<string | null>; declare operation_qualification_code: CreationOptional<string | null>;
declare exemption_reason_code: CreationOptional<string | null>; declare exemption_reason_code: CreationOptional<string | null>;
declare verifactu_profile_status: CreationOptional<string | null>;
declare uses_verifactu: CreationOptional<boolean | null>;
declare verifactu_effective_at: CreationOptional<Date | null>;
// Subtotal // Subtotal
declare subtotal_amount_value: number; declare subtotal_amount_value: number;
@ -351,21 +348,6 @@ export default (database: Sequelize) => {
allowNull: true, allowNull: true,
defaultValue: null, defaultValue: null,
}, },
verifactu_profile_status: {
type: DataTypes.STRING(64),
allowNull: true,
defaultValue: null,
},
uses_verifactu: {
type: DataTypes.BOOLEAN,
allowNull: true,
defaultValue: null,
},
verifactu_effective_at: {
type: DataTypes.DATE,
allowNull: true,
defaultValue: null,
},
subtotal_amount_value: { subtotal_amount_value: {
type: new DataTypes.BIGINT(), type: new DataTypes.BIGINT(),
allowNull: false, allowNull: false,

View File

@ -1,7 +1,6 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import test from "node:test"; import test from "node:test";
import { UtcDate } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import { SequelizeIssuedInvoiceV2DomainMapper } from "./sequelize-issued-invoice-v2-domain.mapper"; import { SequelizeIssuedInvoiceV2DomainMapper } from "./sequelize-issued-invoice-v2-domain.mapper";
@ -120,7 +119,6 @@ const buildIssuedInvoiceSource = (params: {
description: "Régimen general", description: "Régimen general",
}, },
fiscalClassification: Maybe.none(), fiscalClassification: Maybe.none(),
verifactuPolicy: Maybe.none(),
subtotalAmount: { value: "10000", scale: 2 }, subtotalAmount: { value: "10000", scale: 2 },
itemsDiscountAmount: { value: "0", scale: 2 }, itemsDiscountAmount: { value: "0", scale: 2 },
globalDiscountPercentage: { globalDiscountPercentage: {
@ -270,26 +268,17 @@ test("rehidrata el snapshot persistido histórico sin depender del catálogo viv
]); ]);
}); });
test("normaliza verifactu_effective_at Date a YYYY-MM-DD al rehidratar", () => { test("rehidrata fiscalClassification exclusivamente desde sus columnas fiscales", () => {
const mapper = buildMapper(); const mapper = buildMapper();
const raw = { const internals = mapper as unknown as {
verifactu_effective_at: new Date("2027-01-12T00:00:00.000Z"),
} as {
verifactu_effective_at: Date;
};
const result = mapper.mapToDomain.bind(mapper) as unknown;
assert.ok(result);
const effectiveAt = mapper as unknown as {
_mapAttributesToDomain: ( _mapAttributesToDomain: (
source: unknown, source: unknown,
params: { errors: Array<{ message: string; path: string; value?: unknown }> } params: { errors: Array<{ message: string; path: string; value?: unknown }> }
) => { hasVerifactuSnapshots: boolean }; ) => { taxRegime: { code: string; description: string } };
}; };
const errors: Array<{ message: string; path: string; value?: unknown }> = []; const errors: Array<{ message: string; path: string; value?: unknown }> = [];
const attributes = effectiveAt._mapAttributesToDomain( const attributes = internals._mapAttributesToDomain(
{ {
id: "11111111-1111-4111-8111-111111111111", id: "11111111-1111-4111-8111-111111111111",
company_id: "22222222-2222-4222-8222-222222222222", company_id: "22222222-2222-4222-8222-222222222222",
@ -327,22 +316,13 @@ test("normaliza verifactu_effective_at Date a YYYY-MM-DD al rehidratar", () => {
retention_amount_value: 0, retention_amount_value: 0,
taxes_amount_value: 2100, taxes_amount_value: 2100,
total_amount_value: 12100, total_amount_value: 12100,
verifactu_profile_status: "obligated_with_verifactu", operation_qualification_code: "S1",
uses_verifactu: true, exemption_reason_code: "E1",
verifactu_effective_at: raw.verifactu_effective_at,
}, },
{ errors } { errors }
) as {
hasVerifactuSnapshots: boolean;
};
assert.equal(attributes.hasVerifactuSnapshots, true);
const utcDateResult = UtcDate.createFromISO(
raw.verifactu_effective_at.toISOString().slice(0, 10)
); );
assert.equal(utcDateResult.isSuccess, true);
assert.equal(utcDateResult.data.toPrimitive(), "2027-01-12"); assert.equal(attributes.taxRegime.code, "04");
assert.deepEqual(errors, []); assert.deepEqual(errors, []);
}); });

View File

@ -15,7 +15,6 @@ import {
import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils"; import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
import { import {
type CompanyVerifactuProfileStatus,
type InternalIssuedInvoiceProps, type InternalIssuedInvoiceProps,
InvoiceAmount, InvoiceAmount,
InvoiceNumber, InvoiceNumber,
@ -28,7 +27,6 @@ import {
IssuedInvoiceFiscalClassification, IssuedInvoiceFiscalClassification,
IssuedInvoiceItems, IssuedInvoiceItems,
IssuedInvoiceTaxes, IssuedInvoiceTaxes,
IssuedInvoiceVerifactuPolicy,
} from "../../../../../../domain"; } from "../../../../../../domain";
import type { IssuedInvoiceCreationAttributes, IssuedInvoiceModel } from "../../../../../common"; import type { IssuedInvoiceCreationAttributes, IssuedInvoiceModel } from "../../../../../common";
@ -143,17 +141,17 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper<
} }
const taxRegime = extractOrPushError( const taxRegime = extractOrPushError(
isNullishOrEmpty(raw.tax_regime_code) isNullishOrEmpty(raw.tax_regime_description)
? Result.fail( ? Result.fail(
new DomainValidationError( new DomainValidationError(
"MISSING_TAX_REGIME", "MISSING_TAX_REGIME_DESCRIPTION",
"tax_regime", "tax_regime_description",
"Issued invoice requires persisted tax regime" "Issued invoice requires persisted tax regime description"
) )
) )
: InvoiceTaxRegime.create({ : InvoiceTaxRegime.create({
code: String(raw.tax_regime_code), code: String(raw.tax_regime_code),
description: String(raw.tax_regime_description ?? raw.tax_regime_code), description: String(raw.tax_regime_description),
}), }),
"tax_regime", "tax_regime",
errors errors
@ -226,8 +224,6 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper<
"total_amount_value", "total_amount_value",
errors errors
); );
const hasVerifactuSnapshots = raw.verifactu_profile_status != null;
return { return {
invoiceId, invoiceId,
companyId, companyId,
@ -259,7 +255,6 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper<
retentionAmount, retentionAmount,
taxesAmount, taxesAmount,
totalAmount, totalAmount,
hasVerifactuSnapshots,
}; };
} }
@ -308,60 +303,24 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper<
languageCode: attributes.languageCode!, languageCode: attributes.languageCode!,
currencyCode: attributes.currencyCode!, currencyCode: attributes.currencyCode!,
}); });
let fiscalClassification = Maybe.none<IssuedInvoiceFiscalClassification>(); const fiscalClassificationResult = IssuedInvoiceFiscalClassification.create({
let verifactuPolicy = Maybe.none<IssuedInvoiceVerifactuPolicy>(); taxRegimeCode: attributes.taxRegime!.code,
taxRegimeLabel:
attributes.taxRegime!.description.trim().length > 0
? Maybe.some(attributes.taxRegime!.description)
: Maybe.none(),
operationQualificationCode:
raw.operation_qualification_code == null
? Maybe.none()
: Maybe.some(String(raw.operation_qualification_code)),
exemptionReasonCode:
raw.exemption_reason_code == null
? Maybe.none()
: Maybe.some(String(raw.exemption_reason_code)),
});
if (attributes.hasVerifactuSnapshots) { if (fiscalClassificationResult.isFailure) {
const fiscalClassificationResult = IssuedInvoiceFiscalClassification.create({ return Result.fail(fiscalClassificationResult.error);
taxRegimeCode: attributes.taxRegime!.code,
taxRegimeLabel:
attributes.taxRegime!.description.trim().length > 0
? Maybe.some(attributes.taxRegime!.description)
: Maybe.none(),
operationQualificationCode:
raw.operation_qualification_code == null
? Maybe.none()
: Maybe.some(String(raw.operation_qualification_code)),
exemptionReasonCode:
raw.exemption_reason_code == null
? Maybe.none()
: Maybe.some(String(raw.exemption_reason_code)),
});
if (fiscalClassificationResult.isFailure) {
return Result.fail(fiscalClassificationResult.error);
}
fiscalClassification = Maybe.some(fiscalClassificationResult.data);
const effectiveAt = extractOrPushError(
maybeFromNullableResult(raw.verifactu_effective_at, (value) =>
UtcDate.createFromISO(
value instanceof Date ? value.toISOString().slice(0, 10) : String(value).slice(0, 10)
)
),
"verifactu_effective_at",
errors
);
if (errors.length > 0) {
console.error(errors);
return Result.fail(
new ValidationErrorCollection("Issued invoice V2 mapping failed [mapToDomain]", errors)
);
}
const verifactuPolicyResult = IssuedInvoiceVerifactuPolicy.create({
profileStatus: String(raw.verifactu_profile_status) as CompanyVerifactuProfileStatus,
usesVerifactu: Boolean(raw.uses_verifactu),
effectiveAt: effectiveAt ?? Maybe.none<UtcDate>(),
});
if (verifactuPolicyResult.isFailure) {
return Result.fail(verifactuPolicyResult.error);
}
verifactuPolicy = Maybe.some(verifactuPolicyResult.data);
} }
const invoiceProps: InternalIssuedInvoiceProps = { const invoiceProps: InternalIssuedInvoiceProps = {
@ -394,8 +353,7 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper<
totalAmount: attributes.totalAmount!, totalAmount: attributes.totalAmount!,
paymentMethod: attributes.paymentMethod!, paymentMethod: attributes.paymentMethod!,
taxRegime: attributes.taxRegime!, taxRegime: attributes.taxRegime!,
fiscalClassification, fiscalClassification: Maybe.some(fiscalClassificationResult.data),
verifactuPolicy,
taxes, taxes,
verifactu: verifactuResult.data, verifactu: verifactuResult.data,
linkedProformaId: attributes.linkedProformaId!, linkedProformaId: attributes.linkedProformaId!,
@ -487,15 +445,6 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper<
exemption_reason_code: source.fiscalClassification.isSome() exemption_reason_code: source.fiscalClassification.isSome()
? (source.fiscalClassification.unwrap().exemptionReasonCode.getOrUndefined() ?? null) ? (source.fiscalClassification.unwrap().exemptionReasonCode.getOrUndefined() ?? null)
: null, : null,
verifactu_profile_status: source.verifactuPolicy.isSome()
? source.verifactuPolicy.unwrap().profileStatus
: null,
uses_verifactu: source.verifactuPolicy.isSome()
? source.verifactuPolicy.unwrap().usesVerifactu
: null,
verifactu_effective_at: source.verifactuPolicy.isSome()
? (source.verifactuPolicy.unwrap().effectiveAt.getOrUndefined()?.toDate() ?? null)
: null,
subtotal_amount_value: source.subtotalAmount.value, subtotal_amount_value: source.subtotalAmount.value,
subtotal_amount_scale: source.subtotalAmount.scale, subtotal_amount_scale: source.subtotalAmount.scale,
items_discount_amount_value: source.itemsDiscountAmount.value, items_discount_amount_value: source.itemsDiscountAmount.value,

View File

@ -0,0 +1,55 @@
import assert from "node:assert/strict";
import test from "node:test";
import { Maybe } from "@repo/rdx-utils";
import { SequelizeIssuedInvoiceV2VerifactuDomainMapper } from "./sequelize-issued-invoice-v2-verifactu-domain.mapper";
test("rehidrata un VerifactuRecord histórico cuando existe en persistencia", () => {
const mapper = new SequelizeIssuedInvoiceV2VerifactuDomainMapper();
const result = mapper.mapToDomain({
id: "11111111-1111-4111-8111-111111111111",
estado: "Correcto",
qr: "qr-value",
url: "https://example.com/verifactu/1",
uuid: "vf-uuid",
operacion: "alta",
} as never, {
errors: [],
attributes: {},
});
assert.equal(result.isSuccess, true);
assert.equal(result.data.isSome(), true);
assert.equal(result.data.unwrap().estado.toPrimitive(), "Correcto");
assert.equal(result.data.unwrap().qrCode.getOrUndefined(), "qr-value");
});
test("devuelve ausencia cuando la factura histórica no tiene VerifactuRecord persistido", () => {
const mapper = new SequelizeIssuedInvoiceV2VerifactuDomainMapper();
const result = mapper.mapToDomain(null, {
errors: [],
attributes: {},
});
assert.equal(result.isSuccess, true);
assert.equal(result.data.isNone(), true);
});
test("no inventa un registro pending al persistir una factura que no usa VeriFactu", () => {
const mapper = new SequelizeIssuedInvoiceV2VerifactuDomainMapper();
const result = mapper.mapToPersistence(Maybe.none(), {
parent: {
id: {
toPrimitive: () => "11111111-1111-4111-8111-111111111111",
},
},
errors: [],
} as never);
assert.equal(result.isSuccess, true);
assert.equal(result.data, undefined);
});

View File

@ -21,7 +21,7 @@ import type { VerifactuRecordCreationAttributes, VerifactuRecordModel } from "..
export class SequelizeIssuedInvoiceV2VerifactuDomainMapper extends SequelizeDomainMapper< export class SequelizeIssuedInvoiceV2VerifactuDomainMapper extends SequelizeDomainMapper<
VerifactuRecordModel, VerifactuRecordModel,
VerifactuRecordCreationAttributes, VerifactuRecordCreationAttributes | undefined,
Maybe<VerifactuRecord> Maybe<VerifactuRecord>
> { > {
public mapToDomain( public mapToDomain(
@ -91,23 +91,14 @@ export class SequelizeIssuedInvoiceV2VerifactuDomainMapper extends SequelizeDoma
mapToPersistence( mapToPersistence(
source: Maybe<VerifactuRecord>, source: Maybe<VerifactuRecord>,
params?: MapperParamsType params?: MapperParamsType
): Result<VerifactuRecordCreationAttributes, Error> { ): Result<VerifactuRecordCreationAttributes | undefined, Error> {
const { parent } = params as { const { parent } = params as {
parent: IssuedInvoice; parent: IssuedInvoice;
errors: ValidationErrorDetail[]; errors: ValidationErrorDetail[];
}; };
if (source.isNone()) { if (source.isNone()) {
return Result.ok({ return Result.ok(undefined);
id: UniqueID.generateNewID().toPrimitive(),
invoice_id: null,
issued_invoice_id: parent.id.toPrimitive(),
estado: VerifactuRecordEstado.createPendiente().toPrimitive(),
qr: "",
url: "",
uuid: "",
operacion: "",
});
} }
const verifactu = source.unwrap(); const verifactu = source.unwrap();

View File

@ -1,135 +0,0 @@
import type { MapperParamsType } from "@erp/core/api";
import { SequelizeDomainMapper } from "@erp/core/api";
import {
URLAddress,
UniqueID,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableResult,
maybeToEmptyString,
} from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import {
type IIssuedInvoiceCreateProps,
type IssuedInvoice,
VerifactuRecord,
VerifactuRecordEstado,
} from "../../../../../../domain";
import type {
VerifactuRecordCreationAttributes,
VerifactuRecordModel,
} from "../../../../../common";
export class SequelizeIssuedInvoiceVerifactuDomainMapper extends SequelizeDomainMapper<
VerifactuRecordModel,
VerifactuRecordCreationAttributes,
Maybe<VerifactuRecord>
> {
public mapToDomain(
source: VerifactuRecordModel | null | undefined,
params?: MapperParamsType
): Result<Maybe<VerifactuRecord>, Error> {
const { errors, attributes } = params as {
errors: ValidationErrorDetail[];
attributes: Partial<IIssuedInvoiceCreateProps>;
};
if (!source) {
return Result.ok(Maybe.none());
}
const recordId = extractOrPushError(UniqueID.create(source.id), "id", errors);
const estado = extractOrPushError(
VerifactuRecordEstado.create(source.estado),
"estado",
errors
);
const qr = extractOrPushError(
maybeFromNullableResult(source.qr, (value) => Result.ok(String(value))),
"qr",
errors
);
const url = extractOrPushError(
maybeFromNullableResult(source.url, (value) => URLAddress.create(value)),
"url",
errors
);
const uuid = extractOrPushError(
maybeFromNullableResult(source.uuid, (value) => Result.ok(String(value))),
"uuid",
errors
);
const operacion = extractOrPushError(
maybeFromNullableResult(source.operacion, (value) => Result.ok(String(value))),
"operacion",
errors
);
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Verifactu record mapping failed [mapToDTO]", errors)
);
}
const createResult = VerifactuRecord.create(
{
estado: estado!,
qrCode: qr!,
url: url!,
uuid: uuid!,
operacion: operacion!,
},
recordId!
);
if (createResult.isFailure) {
return Result.fail(
new ValidationErrorCollection("Invoice verifactu entity creation failed", [
{ path: "verifactu", message: createResult.error.message },
])
);
}
return Result.ok(Maybe.some(createResult.data));
}
mapToPersistence(
source: Maybe<VerifactuRecord>,
params?: MapperParamsType
): Result<VerifactuRecordCreationAttributes, Error> {
const { errors, parent } = params as {
parent: IssuedInvoice;
errors: ValidationErrorDetail[];
};
if (source.isNone()) {
return Result.ok({
id: UniqueID.generateNewID().toPrimitive(),
invoice_id: parent.id.toPrimitive(),
estado: VerifactuRecordEstado.createPendiente().toPrimitive(),
qr: "",
url: "",
uuid: "",
operacion: "",
});
}
const verifactu = source.unwrap();
return Result.ok({
id: verifactu.id.toPrimitive(),
invoice_id: parent.id.toPrimitive(),
estado: verifactu.estado.toPrimitive(),
qr: maybeToEmptyString(verifactu.qrCode, (v) => v),
url: maybeToEmptyString(verifactu.url, (v) => v.toPrimitive()),
uuid: maybeToEmptyString(verifactu.uuid, (v) => v),
operacion: maybeToEmptyString(verifactu.operacion, (v) => v),
});
}
}

View File

@ -17,7 +17,6 @@ import {
PaymentMethodRefSchema, PaymentMethodRefSchema,
TaxRegimeRefSchema, TaxRegimeRefSchema,
TaxesBreakdownSchema, TaxesBreakdownSchema,
IssuedInvoiceVerifactuPolicySchema,
VerifactuRecordSchema, VerifactuRecordSchema,
} from "../../shared"; } from "../../shared";
@ -54,7 +53,6 @@ export const GetIssuedInvoiceByIdResponseSchema = z.object({
tax_regime: TaxRegimeRefSchema, tax_regime: TaxRegimeRefSchema,
fiscal_classification: IssuedInvoiceFiscalClassificationSchema.nullable(), fiscal_classification: IssuedInvoiceFiscalClassificationSchema.nullable(),
verifactu_policy: IssuedInvoiceVerifactuPolicySchema.nullable(),
subtotal_amount: MoneySchema, subtotal_amount: MoneySchema,

View File

@ -4,7 +4,6 @@ import { z } from "zod/v4";
import { import {
IssuedInvoiceFiscalClassificationSchema, IssuedInvoiceFiscalClassificationSchema,
IssuedInvoiceStatusSchema, IssuedInvoiceStatusSchema,
IssuedInvoiceVerifactuPolicySchema,
TaxRegimeRefSchema, TaxRegimeRefSchema,
} from "../../shared"; } from "../../shared";
@ -34,7 +33,6 @@ export const IssueProformaIssuedInvoiceSchema = z.object({
recipient: IssueProformaIssuedInvoiceRecipientSchema, recipient: IssueProformaIssuedInvoiceRecipientSchema,
tax_regime: TaxRegimeRefSchema, tax_regime: TaxRegimeRefSchema,
fiscal_classification: IssuedInvoiceFiscalClassificationSchema.nullable(), fiscal_classification: IssuedInvoiceFiscalClassificationSchema.nullable(),
verifactu_policy: IssuedInvoiceVerifactuPolicySchema.nullable(),
totals: IssueProformaIssuedInvoiceTotalsSchema, totals: IssueProformaIssuedInvoiceTotalsSchema,
}); });

View File

@ -4,5 +4,4 @@ export * from "./issued-invoice-payment-term.dto";
export * from "./issued-invoice-recipient-summary.dto"; export * from "./issued-invoice-recipient-summary.dto";
export * from "./issued-invoice-status.dto"; export * from "./issued-invoice-status.dto";
export * from "./issued-invoice-summary.dto"; export * from "./issued-invoice-summary.dto";
export * from "./issued-invoice-verifactu-policy.dto";
export * from "./verifactu-record.dto"; export * from "./verifactu-record.dto";

View File

@ -1,18 +0,0 @@
import { z } from "zod/v4";
const CompanyVerifactuProfileStatusSchema = z.enum([
"not_configured",
"exempt",
"obligated_without_verifactu",
"obligated_with_verifactu",
]);
export const IssuedInvoiceVerifactuPolicySchema = z.object({
profile_status: CompanyVerifactuProfileStatusSchema,
uses_verifactu: z.boolean(),
effective_at: z.iso.datetime().nullable(),
});
export type IssuedInvoiceVerifactuPolicyDTO = z.infer<
typeof IssuedInvoiceVerifactuPolicySchema
>;

View File

@ -844,9 +844,6 @@
"fiscal_tax_regime": "Historical tax regime", "fiscal_tax_regime": "Historical tax regime",
"operation_qualification": "Operation qualification", "operation_qualification": "Operation qualification",
"exemption_reason": "Exemption reason", "exemption_reason": "Exemption reason",
"verifactu_profile_status": "VeriFactu status",
"verifactu_policy": "VeriFactu policy",
"verifactu_effective_at": "VeriFactu effective date",
"subtotal_amount": "Subtotal", "subtotal_amount": "Subtotal",
"items_discount_amount": "Item discount", "items_discount_amount": "Item discount",
"global_discount_amount": "Global discount", "global_discount_amount": "Global discount",
@ -888,15 +885,7 @@
"verifactu": { "verifactu": {
"empty": "This invoice has no VeriFactu block available in the current detail view.", "empty": "This invoice has no VeriFactu block available in the current detail view.",
"qr_alt": "VeriFactu QR code", "qr_alt": "VeriFactu QR code",
"available": "Available", "available": "Available"
"uses_verifactu": "Uses VeriFactu",
"does_not_use_verifactu": "Does not use VeriFactu",
"policy_status": {
"not_configured": "Not configured",
"exempt": "Exempt or not applicable",
"obligated_without_verifactu": "Obligated without VeriFactu",
"obligated_with_verifactu": "Obligated with VeriFactu"
}
}, },
"fiscal": { "fiscal": {
"unknown_tax_code": "Tax" "unknown_tax_code": "Tax"

View File

@ -845,9 +845,6 @@
"fiscal_tax_regime": "Régimen fiscal histórico", "fiscal_tax_regime": "Régimen fiscal histórico",
"operation_qualification": "Calificación de operación", "operation_qualification": "Calificación de operación",
"exemption_reason": "Motivo de exención", "exemption_reason": "Motivo de exención",
"verifactu_profile_status": "Situación VeriFactu",
"verifactu_policy": "Política VeriFactu",
"verifactu_effective_at": "Fecha efectiva VeriFactu",
"items_discount_amount": "Descuento de líneas", "items_discount_amount": "Descuento de líneas",
"global_discount_amount": "Descuento global", "global_discount_amount": "Descuento global",
"subtotal_amount": "Subtotal", "subtotal_amount": "Subtotal",
@ -889,15 +886,7 @@
"verifactu": { "verifactu": {
"empty": "Esta factura no tiene un bloque VeriFactu disponible en el detalle actual.", "empty": "Esta factura no tiene un bloque VeriFactu disponible en el detalle actual.",
"qr_alt": "Código QR de VeriFactu", "qr_alt": "Código QR de VeriFactu",
"available": "Disponible", "available": "Disponible"
"uses_verifactu": "Usa VeriFactu",
"does_not_use_verifactu": "No usa VeriFactu",
"policy_status": {
"not_configured": "No configurado",
"exempt": "Exenta o no aplicable",
"obligated_without_verifactu": "Obligada sin VeriFactu",
"obligated_with_verifactu": "Obligada con VeriFactu"
}
}, },
"fiscal": { "fiscal": {
"unknown_tax_code": "Impuesto" "unknown_tax_code": "Impuesto"
@ -1195,4 +1184,4 @@
} }
} }
} }
} }

View File

@ -95,7 +95,6 @@ export interface IssuedInvoiceDetailFiscalBreakdownItemViewModel {
export interface IssuedInvoiceDetailFiscalViewModel { export interface IssuedInvoiceDetailFiscalViewModel {
taxRegimeLabel: string; taxRegimeLabel: string;
fiscalClassification: IssuedInvoiceDetailFiscalFieldViewModel[]; fiscalClassification: IssuedInvoiceDetailFiscalFieldViewModel[];
verifactuPolicy: IssuedInvoiceDetailFiscalFieldViewModel[];
subtotalLabel: string; subtotalLabel: string;
itemsDiscountLabel: string | null; itemsDiscountLabel: string | null;
globalDiscountLabel: string | null; globalDiscountLabel: string | null;

View File

@ -65,7 +65,6 @@ const buildController = () => ({
fiscal: { fiscal: {
taxRegimeLabel: "Régimen general", taxRegimeLabel: "Régimen general",
fiscalClassification: [], fiscalClassification: [],
verifactuPolicy: [],
subtotalLabel: "100,00 €", subtotalLabel: "100,00 €",
itemsDiscountLabel: null, itemsDiscountLabel: null,
globalDiscountLabel: null, globalDiscountLabel: null,

View File

@ -24,9 +24,6 @@ export const IssuedInvoiceFiscalSection = ({
{fiscal.fiscalClassification.map((field) => ( {fiscal.fiscalClassification.map((field) => (
<IssuedInvoiceDetailField key={field.key} label={field.label} value={field.value} /> <IssuedInvoiceDetailField key={field.key} label={field.label} value={field.value} />
))} ))}
{fiscal.verifactuPolicy.map((field) => (
<IssuedInvoiceDetailField key={field.key} label={field.label} value={field.value} />
))}
</div> </div>
</IssuedInvoiceDetailSection> </IssuedInvoiceDetailSection>
); );

View File

@ -119,11 +119,6 @@ const buildInvoice = (): IssuedInvoice => ({
exemptionReasonCode: null, exemptionReasonCode: null,
exemptionReasonLabel: null, exemptionReasonLabel: null,
}, },
verifactuPolicy: {
profileStatus: "obligated_with_verifactu",
usesVerifactu: true,
effectiveAt: "2026-01-01T00:00:00.000Z",
},
subtotalAmount: 150, subtotalAmount: 150,
itemsDiscountAmount: 0, itemsDiscountAmount: 0,
globalDiscountPercentage: 0, globalDiscountPercentage: 0,
@ -209,7 +204,6 @@ test("construye el view model general con resumen fiscal, unidades y enlace a pr
assert.equal(viewModel.commercial.paymentTermDueRules[0]?.dueDaysLabel, "30 días"); assert.equal(viewModel.commercial.paymentTermDueRules[0]?.dueDaysLabel, "30 días");
assert.equal(viewModel.fiscal.taxRegimeLabel, "general"); assert.equal(viewModel.fiscal.taxRegimeLabel, "general");
assert.equal(viewModel.fiscal.fiscalClassification[0]?.value, "Régimen general"); assert.equal(viewModel.fiscal.fiscalClassification[0]?.value, "Régimen general");
assert.equal(viewModel.fiscal.verifactuPolicy[0]?.value, "obligated_with_verifactu");
assert.equal(viewModel.fiscal.usesRec, true); assert.equal(viewModel.fiscal.usesRec, true);
assert.equal(viewModel.fiscal.usesRetention, true); assert.equal(viewModel.fiscal.usesRetention, true);
assert.equal(viewModel.fiscal.breakdown[1]?.taxLabel, "Impuesto"); assert.equal(viewModel.fiscal.breakdown[1]?.taxLabel, "Impuesto");

View File

@ -109,15 +109,6 @@ const buildPercentageLabel = (value: number | null, scale = 2) => {
return `${NumberHelper.formatNumber(value, scale)}%`; return `${NumberHelper.formatNumber(value, scale)}%`;
}; };
const buildPolicyStatusLabel = (
profileStatus: NonNullable<IssuedInvoice["verifactuPolicy"]>["profileStatus"],
translate: IssuedInvoiceDetailTranslate
) => {
return translate(`pages.issued_invoices.detail.verifactu.policy_status.${profileStatus}`, {
defaultValue: profileStatus,
});
};
const buildItemTaxesLabel = ( const buildItemTaxesLabel = (
item: IssuedInvoice["items"][number], item: IssuedInvoice["items"][number],
translate: IssuedInvoiceDetailTranslate translate: IssuedInvoiceDetailTranslate
@ -369,32 +360,9 @@ export const buildIssuedInvoiceDetailGeneralViewModel = (
), ),
].filter((item): item is IssuedInvoiceDetailFiscalFieldViewModel => item !== null); ].filter((item): item is IssuedInvoiceDetailFiscalFieldViewModel => item !== null);
const verifactuPolicy = issuedInvoice.verifactuPolicy
? [
buildFiscalField(
"profile-status",
context.translate("pages.issued_invoices.detail.fields.verifactu_profile_status"),
buildPolicyStatusLabel(issuedInvoice.verifactuPolicy.profileStatus, context.translate)
),
buildFiscalField(
"uses-verifactu",
context.translate("pages.issued_invoices.detail.fields.verifactu_policy"),
issuedInvoice.verifactuPolicy.usesVerifactu
? context.translate("pages.issued_invoices.detail.verifactu.uses_verifactu")
: context.translate("pages.issued_invoices.detail.verifactu.does_not_use_verifactu")
),
buildFiscalField(
"effective-at",
context.translate("pages.issued_invoices.detail.fields.verifactu_effective_at"),
formatDate(issuedInvoice.verifactuPolicy.effectiveAt)
),
].filter((item): item is IssuedInvoiceDetailFiscalFieldViewModel => item !== null)
: [];
const fiscal: IssuedInvoiceDetailFiscalViewModel = { const fiscal: IssuedInvoiceDetailFiscalViewModel = {
taxRegimeLabel: issuedInvoice.taxRegimeDescription ?? issuedInvoice.taxRegimeCode, taxRegimeLabel: issuedInvoice.taxRegimeDescription ?? issuedInvoice.taxRegimeCode,
fiscalClassification, fiscalClassification,
verifactuPolicy,
subtotalLabel: formatMoney(issuedInvoice, issuedInvoice.subtotalAmount), subtotalLabel: formatMoney(issuedInvoice, issuedInvoice.subtotalAmount),
itemsDiscountLabel, itemsDiscountLabel,
globalDiscountLabel, globalDiscountLabel,

View File

@ -78,11 +78,6 @@ const buildDto = (): GetIssuedInvoiceByIdResponseDTO => ({
exemption_reason_code: null, exemption_reason_code: null,
exemption_reason_label: null, exemption_reason_label: null,
}, },
verifactu_policy: {
profile_status: "obligated_with_verifactu",
uses_verifactu: true,
effective_at: "2026-01-01T00:00:00.000Z",
},
subtotal_amount: MoneyDTOHelper.fromNumber(100, "EUR"), subtotal_amount: MoneyDTOHelper.fromNumber(100, "EUR"),
items_discount_amount: MoneyDTOHelper.fromNumber(5, "EUR"), items_discount_amount: MoneyDTOHelper.fromNumber(5, "EUR"),
global_discount_percentage: PercentageDTOHelper.fromNumber(10), global_discount_percentage: PercentageDTOHelper.fromNumber(10),
@ -139,11 +134,10 @@ test("adapta el DTO de detalle a una entidad con nullables y valores numéricos
assert.equal(invoice.paymentTerm?.name, "30 días"); assert.equal(invoice.paymentTerm?.name, "30 días");
assert.equal(invoice.paymentTerm?.dueRules[0]?.dueDays, "30"); assert.equal(invoice.paymentTerm?.dueRules[0]?.dueDays, "30");
assert.equal(invoice.fiscalClassification?.operationQualificationCode, "S1"); assert.equal(invoice.fiscalClassification?.operationQualificationCode, "S1");
assert.equal(invoice.verifactuPolicy?.usesVerifactu, true);
assert.equal(invoice.globalDiscountPercentage, 10); assert.equal(invoice.globalDiscountPercentage, 10);
assert.equal(invoice.taxableAmount, 85); assert.equal(invoice.taxableAmount, 85);
assert.equal(invoice.totalAmount, 91); assert.equal(invoice.totalAmount, 91);
assert.equal(invoice.verifactu.url, null); assert.equal(invoice.verifactu?.url, null);
assert.equal(invoice.items[0]?.quantity, 2); assert.equal(invoice.items[0]?.quantity, 2);
assert.equal(invoice.items[0]?.unitAmount, 50); assert.equal(invoice.items[0]?.unitAmount, 50);
assert.equal(invoice.items[0]?.retentionPercentage, 15); assert.equal(invoice.items[0]?.retentionPercentage, 15);

View File

@ -1,6 +1,5 @@
import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core"; import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core";
import type { GetIssuedInvoiceByIdResponseDTO } from "../../../../common";
import type { GetIssuedInvoiceByIdResult } from "../api"; import type { GetIssuedInvoiceByIdResult } from "../api";
import type { import type {
IssuedInvoice, IssuedInvoice,
@ -10,7 +9,6 @@ import type {
IssuedInvoiceRecipientDetail, IssuedInvoiceRecipientDetail,
IssuedInvoiceStatus, IssuedInvoiceStatus,
IssuedInvoiceTaxSummary, IssuedInvoiceTaxSummary,
IssuedInvoiceVerifactuPolicy,
IssuedInvoiceVerifactuRecord, IssuedInvoiceVerifactuRecord,
VerifactuRecordStatus, VerifactuRecordStatus,
} from "../entities"; } from "../entities";
@ -44,7 +42,6 @@ export const GetIssuedInvoiceByIdAdapter = {
taxRegimeCode: dto.tax_regime.code, taxRegimeCode: dto.tax_regime.code,
taxRegimeDescription: dto.tax_regime.description, taxRegimeDescription: dto.tax_regime.description,
fiscalClassification: mapFiscalClassification(dto.fiscal_classification), fiscalClassification: mapFiscalClassification(dto.fiscal_classification),
verifactuPolicy: mapVerifactuPolicy(dto.verifactu_policy),
subtotalAmount: MoneyDTOHelper.toNumber(dto.subtotal_amount), subtotalAmount: MoneyDTOHelper.toNumber(dto.subtotal_amount),
itemsDiscountAmount: MoneyDTOHelper.toNumber(dto.items_discount_amount), itemsDiscountAmount: MoneyDTOHelper.toNumber(dto.items_discount_amount),
globalDiscountPercentage: PercentageDTOHelper.toNumber(dto.global_discount_percentage), globalDiscountPercentage: PercentageDTOHelper.toNumber(dto.global_discount_percentage),
@ -62,7 +59,7 @@ export const GetIssuedInvoiceByIdAdapter = {
}, },
}; };
const mapRecipient = (dto: GetIssuedInvoiceByIdResponseDTO): IssuedInvoiceRecipientDetail => { const mapRecipient = (dto: GetIssuedInvoiceByIdResult): IssuedInvoiceRecipientDetail => {
return { return {
id: dto.recipient.id, id: dto.recipient.id,
name: dto.recipient.name, name: dto.recipient.name,
@ -77,7 +74,7 @@ const mapRecipient = (dto: GetIssuedInvoiceByIdResponseDTO): IssuedInvoiceRecipi
}; };
const mapTaxSummary = ( const mapTaxSummary = (
dto: GetIssuedInvoiceByIdResponseDTO["taxes"][number] dto: GetIssuedInvoiceByIdResult["taxes"][number]
): IssuedInvoiceTaxSummary => { ): IssuedInvoiceTaxSummary => {
return { return {
taxableAmount: MoneyDTOHelper.toNumber(dto.taxable_amount), taxableAmount: MoneyDTOHelper.toNumber(dto.taxable_amount),
@ -94,7 +91,7 @@ const mapTaxSummary = (
}; };
}; };
const mapItem = (dto: GetIssuedInvoiceByIdResponseDTO["items"][number]): IssuedInvoiceItem => { const mapItem = (dto: GetIssuedInvoiceByIdResult["items"][number]): IssuedInvoiceItem => {
return { return {
id: dto.id, id: dto.id,
position: Number(dto.position), position: Number(dto.position),
@ -123,7 +120,7 @@ const mapItem = (dto: GetIssuedInvoiceByIdResponseDTO["items"][number]): IssuedI
}; };
const mapPaymentTerm = ( const mapPaymentTerm = (
dto: GetIssuedInvoiceByIdResponseDTO["payment_term"] dto: GetIssuedInvoiceByIdResult["payment_term"]
): IssuedInvoicePaymentTermDetail | null => { ): IssuedInvoicePaymentTermDetail | null => {
if (dto == null) { if (dto == null) {
return null; return null;
@ -144,7 +141,7 @@ const mapPaymentTerm = (
}; };
const mapVerifactu = ( const mapVerifactu = (
dto: GetIssuedInvoiceByIdResponseDTO["verifactu"] | null dto: GetIssuedInvoiceByIdResult["verifactu"]
): IssuedInvoiceVerifactuRecord | null => { ): IssuedInvoiceVerifactuRecord | null => {
if (dto == null) { if (dto == null) {
return null; return null;
@ -158,7 +155,7 @@ const mapVerifactu = (
}; };
const mapFiscalClassification = ( const mapFiscalClassification = (
dto: GetIssuedInvoiceByIdResponseDTO["fiscal_classification"] dto: GetIssuedInvoiceByIdResult["fiscal_classification"]
): IssuedInvoiceFiscalClassification | null => { ): IssuedInvoiceFiscalClassification | null => {
if (dto == null) { if (dto == null) {
return null; return null;
@ -173,17 +170,3 @@ const mapFiscalClassification = (
exemptionReasonLabel: dto.exemption_reason_label, exemptionReasonLabel: dto.exemption_reason_label,
}; };
}; };
const mapVerifactuPolicy = (
dto: GetIssuedInvoiceByIdResponseDTO["verifactu_policy"]
): IssuedInvoiceVerifactuPolicy | null => {
if (dto == null) {
return null;
}
return {
profileStatus: dto.profile_status,
usesVerifactu: dto.uses_verifactu,
effectiveAt: dto.effective_at,
};
};

View File

@ -24,5 +24,5 @@ export function getIssuedInvoiceById(
throw new Error("issuedInvoiceId is required"); throw new Error("issuedInvoiceId is required");
} }
return dataSource.getOne<GetIssuedInvoiceByIdResponseDTO>("issued-invoices", id, { signal }); return dataSource.getOne<GetIssuedInvoiceByIdResult>("issued-invoices", id, { signal });
} }

View File

@ -68,16 +68,6 @@ export interface IssuedInvoiceFiscalClassification {
exemptionReasonLabel: string | null; exemptionReasonLabel: string | null;
} }
export interface IssuedInvoiceVerifactuPolicy {
profileStatus:
| "not_configured"
| "exempt"
| "obligated_without_verifactu"
| "obligated_with_verifactu";
usesVerifactu: boolean;
effectiveAt: string | null;
}
export interface IssuedInvoicePaymentTermDueRule { export interface IssuedInvoicePaymentTermDueRule {
dueDays: string; dueDays: string;
percentage: { percentage: {
@ -123,7 +113,6 @@ export interface IssuedInvoice {
taxRegimeCode: string; taxRegimeCode: string;
taxRegimeDescription: string | null; taxRegimeDescription: string | null;
fiscalClassification: IssuedInvoiceFiscalClassification | null; fiscalClassification: IssuedInvoiceFiscalClassification | null;
verifactuPolicy: IssuedInvoiceVerifactuPolicy | null;
subtotalAmount: number; subtotalAmount: number;
itemsDiscountAmount: number; itemsDiscountAmount: number;
globalDiscountPercentage: number; globalDiscountPercentage: number;