Repaso de Verifactu
This commit is contained in:
parent
bd630b29dd
commit
538c081d58
@ -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'
|
||||||
|
-- );
|
||||||
@ -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;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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");
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
});
|
});
|
||||||
@ -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,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -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(),
|
||||||
|
|
||||||
|
|||||||
@ -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 };
|
||||||
|
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -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);
|
||||||
|
});
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@ -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()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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, []);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
|
});
|
||||||
@ -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();
|
||||||
|
|||||||
@ -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),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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,
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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
|
|
||||||
>;
|
|
||||||
@ -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"
|
||||||
|
|||||||
@ -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 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|||||||
@ -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 });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user