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 {
InvocieTaxRegimeFullReadModel,
@ -22,9 +21,5 @@ export interface ProformaIssueReadModel {
operationQualificationCode: string | null;
exemptionReasonCode: string | null;
};
verifactuPolicy: {
profileStatus: CompanyVerifactuProfileStatus;
usesVerifactu: boolean;
effectiveAt: Date | null;
};
usesVerifactu: boolean;
}

View File

@ -5,4 +5,4 @@ export * from "./issued-invoice-document-properties-factory";
export * from "./issued-invoice-finder";
export * from "./issued-invoice-public-services.interface";
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 { 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 { CompanyVerifactuProfileStatus } from "../../../domain";
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();
Object.assign(converter as object, {
@ -64,14 +62,9 @@ test("toCreateProps normaliza verifactu effectiveAt a YYYY-MM-DD antes de crear
operationQualificationCode: null,
exemptionReasonCode: null,
},
verifactuPolicy: {
profileStatus: CompanyVerifactuProfileStatus.ObligatedWithVerifactu,
usesVerifactu: true,
effectiveAt: new Date("2026-08-20T08:30:00.000Z"),
},
usesVerifactu: true,
});
assert.equal(result.isSuccess, true);
const effectiveAt = result.data.verifactuPolicy.unwrap().effectiveAt.unwrap() as UtcDateType;
assert.equal(effectiveAt.toPrimitive(), "2026-08-20");
assert.equal(result.data.verifactu.isNone(), true);
});

View File

@ -15,7 +15,6 @@ import {
IssuedInvoiceRecipient,
IssuedInvoiceTax,
IssuedInvoiceTaxes,
IssuedInvoiceVerifactuPolicy,
type Proforma,
} from "../../../domain";
import type { ProformaIssueReadModel } from "../models";
@ -85,12 +84,6 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic
return Result.fail(fiscalClassificationResult.error);
}
const verifactuPolicyResult = this.resolveVerifactuPolicy(source);
if (verifactuPolicyResult.isFailure) {
return Result.fail(verifactuPolicyResult.error);
}
const proformaTotals = proforma.totals();
return Result.ok({
@ -115,7 +108,6 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic
paymentTerm: paymentTermResult.data,
taxRegime: taxRegimeResult.data,
fiscalClassification: Maybe.some(fiscalClassificationResult.data),
verifactuPolicy: Maybe.some(verifactuPolicyResult.data),
customerId: proforma.customerId,
recipient: recipientResult.data,
@ -361,28 +353,4 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic
: 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,
CompanyVerifactuProfileStatus,
} from "../../../domain";
import { resolveProformaIssueVerifactuSnapshots } from "./resolve-proforma-issue-verifactu-snapshots";
import { resolveProformaIssueFiscalContext } from "./resolve-proforma-issue-fiscal-context";
function buildCompanyProfile(params?: {
profileStatus?: CompanyVerifactuProfileStatus;
@ -55,7 +55,7 @@ function buildCompanyProfile(params?: {
return result.data;
}
test("resolveProformaIssueVerifactuSnapshots prioriza document over customer and company", () => {
test("resolveProformaIssueFiscalContext prioriza document over customer and company", () => {
const profile = buildCompanyProfile({
taxRegimeCode: "03",
operationQualificationCode: "op-company",
@ -63,7 +63,7 @@ test("resolveProformaIssueVerifactuSnapshots prioriza document over customer and
verifactuEffectiveAt: new Date("2026-08-15T00:00:00.000Z"),
});
const result = resolveProformaIssueVerifactuSnapshots({
const result = resolveProformaIssueFiscalContext({
proformaTaxRegimeCode: Maybe.some("01"),
customerTaxRegimeCode: Maybe.some("02"),
companyProfile: profile,
@ -74,25 +74,23 @@ test("resolveProformaIssueVerifactuSnapshots prioriza document over customer and
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({
taxRegimeCode: "03",
profileStatus: CompanyVerifactuProfileStatus.ObligatedWithoutVerifactu,
});
const result = resolveProformaIssueVerifactuSnapshots({
const result = resolveProformaIssueFiscalContext({
proformaTaxRegimeCode: Maybe.none(),
customerTaxRegimeCode: Maybe.some("02"),
companyProfile: profile,
});
assert.equal(result.taxRegimeCode.unwrap(), "02");
assert.equal(result.verifactuPolicy.profileStatus, "obligated_without_verifactu");
assert.equal(result.verifactuPolicy.usesVerifactu, false);
assert.equal(result.verifactuPolicy.effectiveAt.isNone(), true);
assert.equal(result.usesVerifactu, false);
});
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 profile = buildCompanyProfile({
taxRegimeCode: "05",
@ -101,20 +99,18 @@ test("resolveProformaIssueVerifactuSnapshots usa company defaults y politica cua
verifactuEffectiveAt: effectiveAt,
});
const result = resolveProformaIssueVerifactuSnapshots({
const result = resolveProformaIssueFiscalContext({
proformaTaxRegimeCode: Maybe.none(),
customerTaxRegimeCode: Maybe.none(),
companyProfile: profile,
});
assert.equal(result.taxRegimeCode.unwrap(), "05");
assert.equal(result.verifactuPolicy.profileStatus, "obligated_with_verifactu");
assert.equal(result.verifactuPolicy.usesVerifactu, true);
assert.equal(result.verifactuPolicy.effectiveAt.unwrap(), effectiveAt);
assert.equal(result.usesVerifactu, true);
});
test("resolveProformaIssueVerifactuSnapshots mantiene comportamiento legacy sin perfil", () => {
const result = resolveProformaIssueVerifactuSnapshots({
test("resolveProformaIssueFiscalContext mantiene comportamiento legacy sin perfil", () => {
const result = resolveProformaIssueFiscalContext({
proformaTaxRegimeCode: Maybe.none(),
customerTaxRegimeCode: Maybe.none(),
companyProfile: null,
@ -123,7 +119,5 @@ test("resolveProformaIssueVerifactuSnapshots mantiene comportamiento legacy sin
assert.equal(result.taxRegimeCode.isNone(), true);
assert.equal(result.fiscalClassification.operationQualificationCode.isNone(), true);
assert.equal(result.fiscalClassification.exemptionReasonCode.isNone(), true);
assert.equal(result.verifactuPolicy.profileStatus, "not_configured");
assert.equal(result.verifactuPolicy.usesVerifactu, false);
assert.equal(result.verifactuPolicy.effectiveAt.isNone(), true);
assert.equal(result.usesVerifactu, false);
});

View File

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

View File

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

View File

@ -71,11 +71,6 @@ export interface IIssuedInvoiceFullSnapshot {
exemption_reason_code: string | null;
exemption_reason_label: string | null;
} | null;
verifactu_policy: {
profile_status: string;
uses_verifactu: boolean;
effective_at: string | null;
} | null;
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",
},
fiscal_classification: null,
verifactu_policy: null,
subtotal_amount: MoneyDTOHelper.fromNumber(100, "EUR"),
items_discount_amount: MoneyDTOHelper.fromNumber(0, "EUR"),
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 { ICompanyVerifactuProfileRepository } from "../../../company-verifactu-profile";
import type { ProformaIssueReadModel } from "../../../issued-invoices";
import { resolveProformaIssueVerifactuSnapshots } from "../../../issued-invoices";
import { resolveProformaIssueFiscalContext } from "../../../issued-invoices";
export interface IProformaIssueReadModelAssembler {
assemble(params: {
@ -94,7 +94,7 @@ export class ProformaIssueReadModelAssembler implements IProformaIssueReadModelA
return Result.fail(companyProfileResult.error);
}
const resolution = resolveProformaIssueVerifactuSnapshots({
const resolution = resolveProformaIssueFiscalContext({
proformaTaxRegimeCode: params.proforma.taxRegimeCode,
customerTaxRegimeCode: customerResult.data.taxRegimeCode,
companyProfile: companyProfileResult.data.getOrUndefined() ?? null,
@ -126,11 +126,7 @@ export class ProformaIssueReadModelAssembler implements IProformaIssueReadModelA
exemptionReasonCode:
resolution.fiscalClassification.exemptionReasonCode.getOrUndefined() ?? null,
},
verifactuPolicy: {
profileStatus: resolution.verifactuPolicy.profileStatus,
usesVerifactu: resolution.verifactuPolicy.usesVerifactu,
effectiveAt: resolution.verifactuPolicy.effectiveAt.getOrUndefined() ?? null,
},
usesVerifactu: resolution.usesVerifactu,
});
}

View File

@ -31,9 +31,6 @@ export class IssueProformaResponseSnapshotBuilder implements IIssueProformaRespo
fiscal_classification: maybeToNullable(invoice.fiscalClassification, (value) =>
value.toObjectString()
),
verifactu_policy: maybeToNullable(invoice.verifactuPolicy, (value) =>
value.toObjectString()
),
totals: {
taxable_base: invoice.taxableAmount.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 { 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 {
CompanyVerifactuProfileStatus,
InvoiceNumber,
InvoiceStatus,
Proforma,
@ -17,6 +18,8 @@ import {
import { ProformaIssuer } from "../services/proforma-issuer";
import { IssueProformaUseCase } from "./issue-proforma.use-case";
type CreateIssuedInvoiceProps = Parameters<IIssuedInvoicePublicServices["createIssuedInvoice"]>[1];
const buildTransactionManager = (): ITransactionManager => ({
start: async () => undefined,
getTransaction: () => undefined,
@ -25,6 +28,11 @@ const buildTransactionManager = (): ITransactionManager => ({
rollback: async () => undefined,
});
const buildCreateIssuedInvoiceProps = (): CreateIssuedInvoiceProps =>
({
verifactu: Maybe.none(),
}) as CreateIssuedInvoiceProps;
const buildIssueableProforma = (params?: {
operationDate?: Maybe<UtcDate>;
}) => {
@ -190,11 +198,7 @@ test("issue falla antes de persistir si la proforma no tiene operation date", as
operationQualificationCode: null,
exemptionReasonCode: null,
},
verifactuPolicy: {
profileStatus: CompanyVerifactuProfileStatus.NotConfigured,
usesVerifactu: false,
effectiveAt: null,
},
usesVerifactu: false,
}),
} as never,
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(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 { 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 { VerifactuRecord, VerifactuRecordEstado } from "../../../domain";
import type { IssueProformaCommand } from "../commands";
import type { IProformaRepository } from "../repositories";
import type {
@ -87,6 +88,22 @@ export class IssueProformaUseCase {
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(
issuedInvoiceId,
createPropsResult.data,

View File

@ -31,7 +31,6 @@ import { IssuedInvoiceItemMismatch } from "../errors";
import type {
IssuedInvoiceFiscalClassification,
IssuedInvoiceRecipient,
IssuedInvoiceVerifactuPolicy,
} from "../value-objects";
export interface IIssuedInvoiceCreateProps {
@ -82,7 +81,6 @@ export interface IIssuedInvoiceCreateProps {
totalAmount: InvoiceAmount;
fiscalClassification: Maybe<IssuedInvoiceFiscalClassification>;
verifactuPolicy: Maybe<IssuedInvoiceVerifactuPolicy>;
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();
}
@ -307,10 +295,6 @@ export class IssuedInvoice
return this.props.fiscalClassification;
}
public get verifactuPolicy(): Maybe<IssuedInvoiceVerifactuPolicy> {
return this.props.verifactuPolicy;
}
public get subtotalAmount(): InvoiceAmount {
return this.props.subtotalAmount;
}

View File

@ -1,5 +1,4 @@
export * from "./issued-invoice-fiscal-classification.vo";
export * from "./issued-invoice-recipient.vo";
export * from "./issued-invoice-tax-group.vo";
export * from "./issued-invoice-verifactu-policy.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 operation_qualification_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
declare subtotal_amount_value: number;
@ -351,21 +348,6 @@ export default (database: Sequelize) => {
allowNull: true,
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: {
type: new DataTypes.BIGINT(),
allowNull: false,

View File

@ -1,7 +1,6 @@
import assert from "node:assert/strict";
import test from "node:test";
import { UtcDate } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import { SequelizeIssuedInvoiceV2DomainMapper } from "./sequelize-issued-invoice-v2-domain.mapper";
@ -120,7 +119,6 @@ const buildIssuedInvoiceSource = (params: {
description: "Régimen general",
},
fiscalClassification: Maybe.none(),
verifactuPolicy: Maybe.none(),
subtotalAmount: { value: "10000", scale: 2 },
itemsDiscountAmount: { value: "0", scale: 2 },
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 raw = {
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 {
const internals = mapper as unknown as {
_mapAttributesToDomain: (
source: 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 attributes = effectiveAt._mapAttributesToDomain(
const attributes = internals._mapAttributesToDomain(
{
id: "11111111-1111-4111-8111-111111111111",
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,
taxes_amount_value: 2100,
total_amount_value: 12100,
verifactu_profile_status: "obligated_with_verifactu",
uses_verifactu: true,
verifactu_effective_at: raw.verifactu_effective_at,
operation_qualification_code: "S1",
exemption_reason_code: "E1",
},
{ 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, []);
});

View File

@ -15,7 +15,6 @@ import {
import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
import {
type CompanyVerifactuProfileStatus,
type InternalIssuedInvoiceProps,
InvoiceAmount,
InvoiceNumber,
@ -28,7 +27,6 @@ import {
IssuedInvoiceFiscalClassification,
IssuedInvoiceItems,
IssuedInvoiceTaxes,
IssuedInvoiceVerifactuPolicy,
} from "../../../../../../domain";
import type { IssuedInvoiceCreationAttributes, IssuedInvoiceModel } from "../../../../../common";
@ -143,17 +141,17 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper<
}
const taxRegime = extractOrPushError(
isNullishOrEmpty(raw.tax_regime_code)
isNullishOrEmpty(raw.tax_regime_description)
? Result.fail(
new DomainValidationError(
"MISSING_TAX_REGIME",
"tax_regime",
"Issued invoice requires persisted tax regime"
"MISSING_TAX_REGIME_DESCRIPTION",
"tax_regime_description",
"Issued invoice requires persisted tax regime description"
)
)
: InvoiceTaxRegime.create({
code: String(raw.tax_regime_code),
description: String(raw.tax_regime_description ?? raw.tax_regime_code),
description: String(raw.tax_regime_description),
}),
"tax_regime",
errors
@ -226,8 +224,6 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper<
"total_amount_value",
errors
);
const hasVerifactuSnapshots = raw.verifactu_profile_status != null;
return {
invoiceId,
companyId,
@ -259,7 +255,6 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper<
retentionAmount,
taxesAmount,
totalAmount,
hasVerifactuSnapshots,
};
}
@ -308,60 +303,24 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper<
languageCode: attributes.languageCode!,
currencyCode: attributes.currencyCode!,
});
let fiscalClassification = Maybe.none<IssuedInvoiceFiscalClassification>();
let verifactuPolicy = Maybe.none<IssuedInvoiceVerifactuPolicy>();
const fiscalClassificationResult = IssuedInvoiceFiscalClassification.create({
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) {
const fiscalClassificationResult = IssuedInvoiceFiscalClassification.create({
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);
if (fiscalClassificationResult.isFailure) {
return Result.fail(fiscalClassificationResult.error);
}
const invoiceProps: InternalIssuedInvoiceProps = {
@ -394,8 +353,7 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper<
totalAmount: attributes.totalAmount!,
paymentMethod: attributes.paymentMethod!,
taxRegime: attributes.taxRegime!,
fiscalClassification,
verifactuPolicy,
fiscalClassification: Maybe.some(fiscalClassificationResult.data),
taxes,
verifactu: verifactuResult.data,
linkedProformaId: attributes.linkedProformaId!,
@ -487,15 +445,6 @@ export class SequelizeIssuedInvoiceV2DomainMapper extends SequelizeDomainMapper<
exemption_reason_code: source.fiscalClassification.isSome()
? (source.fiscalClassification.unwrap().exemptionReasonCode.getOrUndefined() ?? 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_scale: source.subtotalAmount.scale,
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<
VerifactuRecordModel,
VerifactuRecordCreationAttributes,
VerifactuRecordCreationAttributes | undefined,
Maybe<VerifactuRecord>
> {
public mapToDomain(
@ -91,23 +91,14 @@ export class SequelizeIssuedInvoiceV2VerifactuDomainMapper extends SequelizeDoma
mapToPersistence(
source: Maybe<VerifactuRecord>,
params?: MapperParamsType
): Result<VerifactuRecordCreationAttributes, Error> {
): Result<VerifactuRecordCreationAttributes | undefined, Error> {
const { parent } = params as {
parent: IssuedInvoice;
errors: ValidationErrorDetail[];
};
if (source.isNone()) {
return Result.ok({
id: UniqueID.generateNewID().toPrimitive(),
invoice_id: null,
issued_invoice_id: parent.id.toPrimitive(),
estado: VerifactuRecordEstado.createPendiente().toPrimitive(),
qr: "",
url: "",
uuid: "",
operacion: "",
});
return Result.ok(undefined);
}
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,
TaxRegimeRefSchema,
TaxesBreakdownSchema,
IssuedInvoiceVerifactuPolicySchema,
VerifactuRecordSchema,
} from "../../shared";
@ -54,7 +53,6 @@ export const GetIssuedInvoiceByIdResponseSchema = z.object({
tax_regime: TaxRegimeRefSchema,
fiscal_classification: IssuedInvoiceFiscalClassificationSchema.nullable(),
verifactu_policy: IssuedInvoiceVerifactuPolicySchema.nullable(),
subtotal_amount: MoneySchema,

View File

@ -4,7 +4,6 @@ import { z } from "zod/v4";
import {
IssuedInvoiceFiscalClassificationSchema,
IssuedInvoiceStatusSchema,
IssuedInvoiceVerifactuPolicySchema,
TaxRegimeRefSchema,
} from "../../shared";
@ -34,7 +33,6 @@ export const IssueProformaIssuedInvoiceSchema = z.object({
recipient: IssueProformaIssuedInvoiceRecipientSchema,
tax_regime: TaxRegimeRefSchema,
fiscal_classification: IssuedInvoiceFiscalClassificationSchema.nullable(),
verifactu_policy: IssuedInvoiceVerifactuPolicySchema.nullable(),
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-status.dto";
export * from "./issued-invoice-summary.dto";
export * from "./issued-invoice-verifactu-policy.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",
"operation_qualification": "Operation qualification",
"exemption_reason": "Exemption reason",
"verifactu_profile_status": "VeriFactu status",
"verifactu_policy": "VeriFactu policy",
"verifactu_effective_at": "VeriFactu effective date",
"subtotal_amount": "Subtotal",
"items_discount_amount": "Item discount",
"global_discount_amount": "Global discount",
@ -888,15 +885,7 @@
"verifactu": {
"empty": "This invoice has no VeriFactu block available in the current detail view.",
"qr_alt": "VeriFactu QR code",
"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"
}
"available": "Available"
},
"fiscal": {
"unknown_tax_code": "Tax"

View File

@ -845,9 +845,6 @@
"fiscal_tax_regime": "Régimen fiscal histórico",
"operation_qualification": "Calificación de operació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",
"global_discount_amount": "Descuento global",
"subtotal_amount": "Subtotal",
@ -889,15 +886,7 @@
"verifactu": {
"empty": "Esta factura no tiene un bloque VeriFactu disponible en el detalle actual.",
"qr_alt": "Código QR de VeriFactu",
"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"
}
"available": "Disponible"
},
"fiscal": {
"unknown_tax_code": "Impuesto"
@ -1195,4 +1184,4 @@
}
}
}
}
}

View File

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

View File

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

View File

@ -24,9 +24,6 @@ export const IssuedInvoiceFiscalSection = ({
{fiscal.fiscalClassification.map((field) => (
<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>
</IssuedInvoiceDetailSection>
);

View File

@ -119,11 +119,6 @@ const buildInvoice = (): IssuedInvoice => ({
exemptionReasonCode: null,
exemptionReasonLabel: null,
},
verifactuPolicy: {
profileStatus: "obligated_with_verifactu",
usesVerifactu: true,
effectiveAt: "2026-01-01T00:00:00.000Z",
},
subtotalAmount: 150,
itemsDiscountAmount: 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.fiscal.taxRegimeLabel, "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.usesRetention, true);
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)}%`;
};
const buildPolicyStatusLabel = (
profileStatus: NonNullable<IssuedInvoice["verifactuPolicy"]>["profileStatus"],
translate: IssuedInvoiceDetailTranslate
) => {
return translate(`pages.issued_invoices.detail.verifactu.policy_status.${profileStatus}`, {
defaultValue: profileStatus,
});
};
const buildItemTaxesLabel = (
item: IssuedInvoice["items"][number],
translate: IssuedInvoiceDetailTranslate
@ -369,32 +360,9 @@ export const buildIssuedInvoiceDetailGeneralViewModel = (
),
].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 = {
taxRegimeLabel: issuedInvoice.taxRegimeDescription ?? issuedInvoice.taxRegimeCode,
fiscalClassification,
verifactuPolicy,
subtotalLabel: formatMoney(issuedInvoice, issuedInvoice.subtotalAmount),
itemsDiscountLabel,
globalDiscountLabel,

View File

@ -78,11 +78,6 @@ const buildDto = (): GetIssuedInvoiceByIdResponseDTO => ({
exemption_reason_code: 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"),
items_discount_amount: MoneyDTOHelper.fromNumber(5, "EUR"),
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?.dueRules[0]?.dueDays, "30");
assert.equal(invoice.fiscalClassification?.operationQualificationCode, "S1");
assert.equal(invoice.verifactuPolicy?.usesVerifactu, true);
assert.equal(invoice.globalDiscountPercentage, 10);
assert.equal(invoice.taxableAmount, 85);
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]?.unitAmount, 50);
assert.equal(invoice.items[0]?.retentionPercentage, 15);

View File

@ -1,6 +1,5 @@
import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core";
import type { GetIssuedInvoiceByIdResponseDTO } from "../../../../common";
import type { GetIssuedInvoiceByIdResult } from "../api";
import type {
IssuedInvoice,
@ -10,7 +9,6 @@ import type {
IssuedInvoiceRecipientDetail,
IssuedInvoiceStatus,
IssuedInvoiceTaxSummary,
IssuedInvoiceVerifactuPolicy,
IssuedInvoiceVerifactuRecord,
VerifactuRecordStatus,
} from "../entities";
@ -44,7 +42,6 @@ export const GetIssuedInvoiceByIdAdapter = {
taxRegimeCode: dto.tax_regime.code,
taxRegimeDescription: dto.tax_regime.description,
fiscalClassification: mapFiscalClassification(dto.fiscal_classification),
verifactuPolicy: mapVerifactuPolicy(dto.verifactu_policy),
subtotalAmount: MoneyDTOHelper.toNumber(dto.subtotal_amount),
itemsDiscountAmount: MoneyDTOHelper.toNumber(dto.items_discount_amount),
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 {
id: dto.recipient.id,
name: dto.recipient.name,
@ -77,7 +74,7 @@ const mapRecipient = (dto: GetIssuedInvoiceByIdResponseDTO): IssuedInvoiceRecipi
};
const mapTaxSummary = (
dto: GetIssuedInvoiceByIdResponseDTO["taxes"][number]
dto: GetIssuedInvoiceByIdResult["taxes"][number]
): IssuedInvoiceTaxSummary => {
return {
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 {
id: dto.id,
position: Number(dto.position),
@ -123,7 +120,7 @@ const mapItem = (dto: GetIssuedInvoiceByIdResponseDTO["items"][number]): IssuedI
};
const mapPaymentTerm = (
dto: GetIssuedInvoiceByIdResponseDTO["payment_term"]
dto: GetIssuedInvoiceByIdResult["payment_term"]
): IssuedInvoicePaymentTermDetail | null => {
if (dto == null) {
return null;
@ -144,7 +141,7 @@ const mapPaymentTerm = (
};
const mapVerifactu = (
dto: GetIssuedInvoiceByIdResponseDTO["verifactu"] | null
dto: GetIssuedInvoiceByIdResult["verifactu"]
): IssuedInvoiceVerifactuRecord | null => {
if (dto == null) {
return null;
@ -158,7 +155,7 @@ const mapVerifactu = (
};
const mapFiscalClassification = (
dto: GetIssuedInvoiceByIdResponseDTO["fiscal_classification"]
dto: GetIssuedInvoiceByIdResult["fiscal_classification"]
): IssuedInvoiceFiscalClassification | null => {
if (dto == null) {
return null;
@ -173,17 +170,3 @@ const mapFiscalClassification = (
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");
}
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;
}
export interface IssuedInvoiceVerifactuPolicy {
profileStatus:
| "not_configured"
| "exempt"
| "obligated_without_verifactu"
| "obligated_with_verifactu";
usesVerifactu: boolean;
effectiveAt: string | null;
}
export interface IssuedInvoicePaymentTermDueRule {
dueDays: string;
percentage: {
@ -123,7 +113,6 @@ export interface IssuedInvoice {
taxRegimeCode: string;
taxRegimeDescription: string | null;
fiscalClassification: IssuedInvoiceFiscalClassification | null;
verifactuPolicy: IssuedInvoiceVerifactuPolicy | null;
subtotalAmount: number;
itemsDiscountAmount: number;
globalDiscountPercentage: number;