Facturas de cliente
This commit is contained in:
parent
d40c2279bd
commit
d17a22dc9f
@ -1 +1,2 @@
|
|||||||
export * from "./errors";
|
export * from "./errors";
|
||||||
|
export * from "./value-objects";
|
||||||
|
|||||||
296
modules/core/src/api/domain/value-objects/__tests__/tax.test.ts
Normal file
296
modules/core/src/api/domain/value-objects/__tests__/tax.test.ts
Normal file
@ -0,0 +1,296 @@
|
|||||||
|
import { Tax } from "../tax";
|
||||||
|
|
||||||
|
describe("Tax Value Object", () => {
|
||||||
|
describe("Creación", () => {
|
||||||
|
test("debe crear una tasa de impuesto válida", () => {
|
||||||
|
const result = Tax.create({
|
||||||
|
value: 2100, // 21.00%
|
||||||
|
scale: 2,
|
||||||
|
name: "IVA General",
|
||||||
|
code: "IVA_GENERAL",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.isSuccess).toBe(true);
|
||||||
|
if (result.isSuccess) {
|
||||||
|
const tax = result.value;
|
||||||
|
expect(tax.value).toBe(2100);
|
||||||
|
expect(tax.scale).toBe(2);
|
||||||
|
expect(tax.name).toBe("IVA General");
|
||||||
|
expect(tax.code).toBe("IVA_GENERAL");
|
||||||
|
expect(tax.toNumber()).toBe(21.0);
|
||||||
|
expect(tax.toString()).toBe("21.00%");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("debe usar escala por defecto cuando no se proporciona", () => {
|
||||||
|
const result = Tax.create({
|
||||||
|
value: 21, // 21%
|
||||||
|
name: "IVA General",
|
||||||
|
code: "IVA_GENERAL",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.isSuccess).toBe(true);
|
||||||
|
if (result.isSuccess) {
|
||||||
|
const tax = result.value;
|
||||||
|
expect(tax.scale).toBe(Tax.DEFAULT_SCALE);
|
||||||
|
expect(tax.toNumber()).toBe(21.0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("debe fallar con valor negativo", () => {
|
||||||
|
const result = Tax.create({
|
||||||
|
value: -100,
|
||||||
|
name: "IVA General",
|
||||||
|
code: "IVA_GENERAL",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.isFailure).toBe(true);
|
||||||
|
if (result.isFailure) {
|
||||||
|
expect(result.error.message).toContain("no puede ser negativa");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("debe fallar con tasa mayor a 100%", () => {
|
||||||
|
const result = Tax.create({
|
||||||
|
value: 10100, // 101.00%
|
||||||
|
scale: 2,
|
||||||
|
name: "IVA General",
|
||||||
|
code: "IVA_GENERAL",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.isFailure).toBe(true);
|
||||||
|
if (result.isFailure) {
|
||||||
|
expect(result.error.message).toContain("no puede ser mayor a 100%");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("debe fallar con nombre vacío", () => {
|
||||||
|
const result = Tax.create({
|
||||||
|
value: 2100,
|
||||||
|
name: "",
|
||||||
|
code: "IVA_GENERAL",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.isFailure).toBe(true);
|
||||||
|
if (result.isFailure) {
|
||||||
|
expect(result.error.message).toContain("El nombre del impuesto es obligatorio");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("debe fallar con código vacío", () => {
|
||||||
|
const result = Tax.create({
|
||||||
|
value: 2100,
|
||||||
|
name: "IVA General",
|
||||||
|
code: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.isFailure).toBe(true);
|
||||||
|
if (result.isFailure) {
|
||||||
|
expect(result.error.message).toContain("El código del impuesto es obligatorio");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("debe fallar con código con caracteres inválidos", () => {
|
||||||
|
const result = Tax.create({
|
||||||
|
value: 2100,
|
||||||
|
name: "IVA General",
|
||||||
|
code: "iva-general", // minúsculas no permitidas
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.isFailure).toBe(true);
|
||||||
|
if (result.isFailure) {
|
||||||
|
expect(result.error.message).toContain("solo letras mayúsculas");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("debe fallar con escala fuera de rango", () => {
|
||||||
|
const result = Tax.create({
|
||||||
|
value: 2100,
|
||||||
|
scale: 5, // Máximo permitido es 4
|
||||||
|
name: "IVA General",
|
||||||
|
code: "IVA_GENERAL",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.isFailure).toBe(true);
|
||||||
|
if (result.isFailure) {
|
||||||
|
expect(result.error.message).toContain("La escala debe estar entre");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Métodos de cálculo", () => {
|
||||||
|
let tax: Tax;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const result = Tax.create({
|
||||||
|
value: 2100, // 21.00%
|
||||||
|
scale: 2,
|
||||||
|
name: "IVA General",
|
||||||
|
code: "IVA_GENERAL",
|
||||||
|
});
|
||||||
|
expect(result.isSuccess).toBe(true);
|
||||||
|
if (result.isSuccess) {
|
||||||
|
tax = result.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("debe calcular correctamente el importe del impuesto", () => {
|
||||||
|
const baseAmount = 1000;
|
||||||
|
const taxAmount = tax.calculateAmount(baseAmount);
|
||||||
|
expect(taxAmount).toBe(210); // 21% de 1000
|
||||||
|
});
|
||||||
|
|
||||||
|
test("debe calcular correctamente con decimales", () => {
|
||||||
|
const baseAmount = 123.45;
|
||||||
|
const taxAmount = tax.calculateAmount(baseAmount);
|
||||||
|
expect(taxAmount).toBeCloseTo(25.9245, 4); // 21% de 123.45
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Métodos de comparación", () => {
|
||||||
|
let tax1: Tax;
|
||||||
|
let tax2: Tax;
|
||||||
|
let tax3: Tax;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const result1 = Tax.create({
|
||||||
|
value: 2100, // 21.00%
|
||||||
|
scale: 2,
|
||||||
|
name: "IVA General",
|
||||||
|
code: "IVA_GENERAL",
|
||||||
|
});
|
||||||
|
const result2 = Tax.create({
|
||||||
|
value: 1000, // 10.00%
|
||||||
|
scale: 2,
|
||||||
|
name: "IVA Reducido",
|
||||||
|
code: "IVA_REDUCIDO",
|
||||||
|
});
|
||||||
|
const result3 = Tax.create({
|
||||||
|
value: 2100, // 21.00%
|
||||||
|
scale: 2,
|
||||||
|
name: "IVA General",
|
||||||
|
code: "IVA_GENERAL",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result1.isSuccess && result2.isSuccess && result3.isSuccess).toBe(true);
|
||||||
|
if (result1.isSuccess && result2.isSuccess && result3.isSuccess) {
|
||||||
|
tax1 = result1.value;
|
||||||
|
tax2 = result2.value;
|
||||||
|
tax3 = result3.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("debe considerar dos tasas con los mismos valores como iguales", () => {
|
||||||
|
expect(tax1.equalsTo(tax3)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("debe considerar dos tasas con valores diferentes como distintas", () => {
|
||||||
|
expect(tax1.equalsTo(tax2)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("debe comparar correctamente tasas mayores", () => {
|
||||||
|
expect(tax1.greaterThan(tax2)).toBe(true);
|
||||||
|
expect(tax2.greaterThan(tax1)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("debe comparar correctamente tasas menores", () => {
|
||||||
|
expect(tax2.lessThan(tax1)).toBe(true);
|
||||||
|
expect(tax1.lessThan(tax2)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Métodos de verificación", () => {
|
||||||
|
test("debe identificar correctamente una tasa cero", () => {
|
||||||
|
const result = Tax.create({
|
||||||
|
value: 0,
|
||||||
|
name: "Sin IVA",
|
||||||
|
code: "SIN_IVA",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.isSuccess).toBe(true);
|
||||||
|
if (result.isSuccess) {
|
||||||
|
const tax = result.value;
|
||||||
|
expect(tax.isZero()).toBe(true);
|
||||||
|
expect(tax.isPositive()).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("debe identificar correctamente una tasa positiva", () => {
|
||||||
|
const result = Tax.create({
|
||||||
|
value: 2100,
|
||||||
|
name: "IVA General",
|
||||||
|
code: "IVA_GENERAL",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.isSuccess).toBe(true);
|
||||||
|
if (result.isSuccess) {
|
||||||
|
const tax = result.value;
|
||||||
|
expect(tax.isZero()).toBe(false);
|
||||||
|
expect(tax.isPositive()).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Representación", () => {
|
||||||
|
test("debe devolver la representación JSON correcta", () => {
|
||||||
|
const result = Tax.create({
|
||||||
|
value: 2100,
|
||||||
|
scale: 2,
|
||||||
|
name: "IVA General",
|
||||||
|
code: "IVA_GENERAL",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.isSuccess).toBe(true);
|
||||||
|
if (result.isSuccess) {
|
||||||
|
const tax = result.value;
|
||||||
|
const json = tax.toJSON();
|
||||||
|
|
||||||
|
expect(json).toEqual({
|
||||||
|
value: 2100,
|
||||||
|
scale: 2,
|
||||||
|
name: "IVA General",
|
||||||
|
code: "IVA_GENERAL",
|
||||||
|
percentage: 21.0,
|
||||||
|
formatted: "21.00%",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("debe formatear correctamente diferentes escalas", () => {
|
||||||
|
const result = Tax.create({
|
||||||
|
value: 21000, // 21.000%
|
||||||
|
scale: 3,
|
||||||
|
name: "IVA General",
|
||||||
|
code: "IVA_GENERAL",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.isSuccess).toBe(true);
|
||||||
|
if (result.isSuccess) {
|
||||||
|
const tax = result.value;
|
||||||
|
expect(tax.toString()).toBe("21.000%");
|
||||||
|
expect(tax.toNumber()).toBe(21.0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Inmutabilidad", () => {
|
||||||
|
test("debe ser inmutable", () => {
|
||||||
|
const result = Tax.create({
|
||||||
|
value: 2100,
|
||||||
|
name: "IVA General",
|
||||||
|
code: "IVA_GENERAL",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.isSuccess).toBe(true);
|
||||||
|
if (result.isSuccess) {
|
||||||
|
const tax = result.value;
|
||||||
|
const props = tax.getProps();
|
||||||
|
|
||||||
|
// Intentar modificar las propiedades debe fallar
|
||||||
|
expect(() => {
|
||||||
|
(props as any).value = 3000;
|
||||||
|
}).toThrow();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
2
modules/core/src/api/domain/value-objects/index.ts
Normal file
2
modules/core/src/api/domain/value-objects/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./tax";
|
||||||
|
export * from "./taxes";
|
||||||
162
modules/core/src/api/domain/value-objects/tax.ts
Normal file
162
modules/core/src/api/domain/value-objects/tax.ts
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import { TaxCatalogProvider } from "@erp/core";
|
||||||
|
import { ValueObject } from "@repo/rdx-ddd";
|
||||||
|
import { Result } from "@repo/rdx-utils";
|
||||||
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
|
const DEFAULT_SCALE = 2;
|
||||||
|
const DEFAULT_MIN_VALUE = 0;
|
||||||
|
const DEFAULT_MAX_VALUE = 100;
|
||||||
|
|
||||||
|
const DEFAULT_MIN_SCALE = 0;
|
||||||
|
const DEFAULT_MAX_SCALE = 4;
|
||||||
|
|
||||||
|
export interface TaxProps {
|
||||||
|
value: number;
|
||||||
|
scale: number;
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Tax extends ValueObject<TaxProps> {
|
||||||
|
static DEFAULT_SCALE = DEFAULT_SCALE;
|
||||||
|
static MIN_VALUE = DEFAULT_MIN_VALUE;
|
||||||
|
static MAX_VALUE = DEFAULT_MAX_VALUE;
|
||||||
|
static MIN_SCALE = DEFAULT_MIN_SCALE;
|
||||||
|
static MAX_SCALE = DEFAULT_MAX_SCALE;
|
||||||
|
|
||||||
|
private static CODE_REGEX = /^[a-z0-9_:-]+$/;
|
||||||
|
|
||||||
|
protected static validate(values: TaxProps) {
|
||||||
|
const schema = z.object({
|
||||||
|
value: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(Tax.MIN_VALUE, "La tasa de impuesto no puede ser negativa.")
|
||||||
|
.max(Tax.MAX_VALUE * 10 ** Tax.MAX_SCALE, "La tasa de impuesto es demasiado alta."),
|
||||||
|
scale: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(Tax.MIN_SCALE)
|
||||||
|
.max(Tax.MAX_SCALE, `La escala debe estar entre ${Tax.MIN_SCALE} y ${Tax.MAX_SCALE}.`),
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.min(1, "El nombre del impuesto es obligatorio.")
|
||||||
|
.max(100, "El nombre del impuesto no puede exceder 100 caracteres."),
|
||||||
|
code: z
|
||||||
|
.string()
|
||||||
|
.min(1, "El código del impuesto es obligatorio.")
|
||||||
|
.max(40, "El código del impuesto no puede exceder 40 caracteres.")
|
||||||
|
.regex(Tax.CODE_REGEX, "El código contiene caracteres no permitidos."),
|
||||||
|
});
|
||||||
|
|
||||||
|
return schema.safeParse(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(props: TaxProps): Result<Tax> {
|
||||||
|
const { value, scale = Tax.DEFAULT_SCALE, name, code } = props;
|
||||||
|
|
||||||
|
const validationResult = Tax.validate({ value, scale, name, code });
|
||||||
|
if (!validationResult.success) {
|
||||||
|
return Result.fail(new Error(validationResult.error.issues.map((e) => e.message).join(", ")));
|
||||||
|
}
|
||||||
|
|
||||||
|
const realValue = value / 10 ** scale;
|
||||||
|
if (realValue > Tax.MAX_VALUE) {
|
||||||
|
return Result.fail(new Error("La tasa de impuesto no puede ser mayor a 100%."));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(new Tax({ value, scale, name, code }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crea un Tax usando solo el 'code', resolviendo el resto de datos desde el catálogo.
|
||||||
|
* @param code Código del impuesto (p.ej. "iva_21")
|
||||||
|
* @param provider Proveedor del catálogo de impuestos
|
||||||
|
*/
|
||||||
|
static createFromCode(code: string, provider: TaxCatalogProvider): Result<Tax, Error> {
|
||||||
|
const normalized = (code ?? "").trim().toLowerCase();
|
||||||
|
if (!normalized || !Tax.CODE_REGEX.test(normalized)) {
|
||||||
|
return Result.fail(new Error(`Código de impuesto inválido: "${code}"`));
|
||||||
|
}
|
||||||
|
|
||||||
|
const maybeItem = provider.findByCode(normalized);
|
||||||
|
if (maybeItem.isNone()) {
|
||||||
|
return Result.fail(
|
||||||
|
new Error(`Código de impuesto no encontrado en el catálogo: "${normalized}"`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = maybeItem.unwrap()!;
|
||||||
|
// Delegamos en create para reusar validación y límites
|
||||||
|
return Tax.create({
|
||||||
|
value: item.value,
|
||||||
|
scale: item.scale ?? Tax.DEFAULT_SCALE,
|
||||||
|
name: item.name,
|
||||||
|
code: item.code, // guardamos el code tal cual viene del catálogo
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get value(): number {
|
||||||
|
return this.props.value;
|
||||||
|
}
|
||||||
|
get scale(): number {
|
||||||
|
return this.props.scale;
|
||||||
|
}
|
||||||
|
get name(): string {
|
||||||
|
return this.props.name;
|
||||||
|
}
|
||||||
|
get code(): string {
|
||||||
|
return this.props.code;
|
||||||
|
}
|
||||||
|
|
||||||
|
getProps(): TaxProps {
|
||||||
|
return this.props;
|
||||||
|
}
|
||||||
|
|
||||||
|
toPrimitive() {
|
||||||
|
return this.getProps();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Devuelve el valor real de la tasa como número decimal (ej: 21.00) */
|
||||||
|
toNumber(): number {
|
||||||
|
return this.value / 10 ** this.scale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Devuelve la tasa formateada como porcentaje (ej: "21.00%") */
|
||||||
|
toString(): string {
|
||||||
|
return `${this.toNumber().toFixed(this.scale)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Calcula el importe del impuesto sobre una base imponible */
|
||||||
|
calculateAmount(baseAmount: number): number {
|
||||||
|
return (baseAmount * this.toNumber()) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
isZero(): boolean {
|
||||||
|
return this.toNumber() === 0;
|
||||||
|
}
|
||||||
|
isPositive(): boolean {
|
||||||
|
return this.toNumber() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
equalsTo(other: Tax): boolean {
|
||||||
|
return this.equals(other);
|
||||||
|
}
|
||||||
|
greaterThan(other: Tax): boolean {
|
||||||
|
return this.toNumber() > other.toNumber();
|
||||||
|
}
|
||||||
|
lessThan(other: Tax): boolean {
|
||||||
|
return this.toNumber() < other.toNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
value: this.value,
|
||||||
|
scale: this.scale,
|
||||||
|
name: this.name,
|
||||||
|
code: this.code,
|
||||||
|
percentage: this.toNumber(),
|
||||||
|
formatted: this.toString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
17
modules/core/src/api/domain/value-objects/taxes.ts
Normal file
17
modules/core/src/api/domain/value-objects/taxes.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { Collection } from "@repo/rdx-utils";
|
||||||
|
import { Tax } from "./tax";
|
||||||
|
|
||||||
|
export interface TaxesProps {
|
||||||
|
items?: Tax[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Taxes extends Collection<Tax> {
|
||||||
|
constructor(props: TaxesProps) {
|
||||||
|
const { items } = props;
|
||||||
|
super(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static create(props: TaxesProps): Taxes {
|
||||||
|
return new Taxes(props);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
modules/core/src/common/catalogs/index.ts
Normal file
1
modules/core/src/common/catalogs/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./taxes";
|
||||||
3
modules/core/src/common/catalogs/taxes/index.ts
Normal file
3
modules/core/src/common/catalogs/taxes/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from "./spain-tax-catalog-provider";
|
||||||
|
export * from "./tax-catalog-provider";
|
||||||
|
export * from "./tax-catalog-types";
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
// --- Adaptador que carga el catálogo JSON en memoria e indexa por code ---
|
||||||
|
|
||||||
|
import { Maybe } from "@repo/rdx-utils";
|
||||||
|
import { TaxCatalogProvider } from "./tax-catalog-provider";
|
||||||
|
import { TaxCatalogType, TaxItemType } from "./tax-catalog-types";
|
||||||
|
|
||||||
|
// Si quieres habilitar la carga desde fichero en Node:
|
||||||
|
// import * as fs from "node:fs";
|
||||||
|
// import * as path from "node:path";
|
||||||
|
|
||||||
|
export class JsonTaxCatalogProvider implements TaxCatalogProvider {
|
||||||
|
// Índice por código normalizado
|
||||||
|
private readonly index: Map<string, TaxItemType>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param catalog Catálogo ya parseado (p.ej. import JSON o fetch)
|
||||||
|
*/
|
||||||
|
constructor(catalog: TaxCatalogType) {
|
||||||
|
this.index = new Map<string, TaxItemType>();
|
||||||
|
// Normalizamos códigos a minúsculas y sin espacios
|
||||||
|
for (const item of catalog) {
|
||||||
|
const normalized = JsonTaxCatalogProvider.normalizeCode(item.code);
|
||||||
|
// En caso de duplicados, el último gana (o lanza error si prefieres)
|
||||||
|
this.index.set(normalized, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static normalizeCode(code: string): string {
|
||||||
|
return (code ?? "").trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
findByCode(code: string): Maybe<TaxItemType> {
|
||||||
|
const normalized = JsonTaxCatalogProvider.normalizeCode(code);
|
||||||
|
const found = this.index.get(normalized);
|
||||||
|
return found ? Maybe.some(found) : Maybe.none<TaxItemType>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opcional: carga desde fichero JSON en Node (descomenta si lo necesitas)
|
||||||
|
// static fromJsonFile(filePath: string): JsonTaxCatalogProvider {
|
||||||
|
// const full = path.resolve(filePath);
|
||||||
|
// const raw = fs.readFileSync(full, "utf-8");
|
||||||
|
// const data = JSON.parse(raw) as TaxCatalogDto;
|
||||||
|
// return new JsonTaxCatalogProvider(data);
|
||||||
|
// }
|
||||||
|
}
|
||||||
@ -0,0 +1,4 @@
|
|||||||
|
import { JsonTaxCatalogProvider } from "./json-tax-catalog-provider";
|
||||||
|
import spainTaxCatalog from "./spain-tax-catalog.json";
|
||||||
|
|
||||||
|
export const spainTaxCatalogProvider = new JsonTaxCatalogProvider(spainTaxCatalog);
|
||||||
357
modules/core/src/common/catalogs/taxes/spain-tax-catalog.json
Normal file
357
modules/core/src/common/catalogs/taxes/spain-tax-catalog.json
Normal file
@ -0,0 +1,357 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "IVA 21%",
|
||||||
|
"code": "iva_21",
|
||||||
|
"value": 2100,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IVA",
|
||||||
|
"description": "IVA general. Tipo estándar nacional.",
|
||||||
|
"aeat_code": "01"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IVA 10%",
|
||||||
|
"code": "iva_10",
|
||||||
|
"value": 1000,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IVA",
|
||||||
|
"description": "IVA reducido para bienes y servicios específicos.",
|
||||||
|
"aeat_code": "03"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IVA 7,5%",
|
||||||
|
"code": "iva_7_5",
|
||||||
|
"value": 750,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IVA",
|
||||||
|
"description": "Tipo especial de IVA.",
|
||||||
|
"aeat_code": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IVA 5%",
|
||||||
|
"code": "iva_5",
|
||||||
|
"value": 500,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IVA",
|
||||||
|
"description": "Tipo reducido especial.",
|
||||||
|
"aeat_code": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IVA 4%",
|
||||||
|
"code": "iva_4",
|
||||||
|
"value": 400,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IVA",
|
||||||
|
"description": "IVA superreducido para bienes de primera necesidad.",
|
||||||
|
"aeat_code": "02"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IVA 2%",
|
||||||
|
"code": "iva_2",
|
||||||
|
"value": 200,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IVA",
|
||||||
|
"description": "Tipo especial de IVA.",
|
||||||
|
"aeat_code": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IVA 0%",
|
||||||
|
"code": "iva_0",
|
||||||
|
"value": 0,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IVA",
|
||||||
|
"description": "Operaciones sujetas pero tipo cero.",
|
||||||
|
"aeat_code": "05"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exenta",
|
||||||
|
"code": "iva_exenta",
|
||||||
|
"value": 0,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IVA",
|
||||||
|
"description": "Operación exenta de IVA.",
|
||||||
|
"aeat_code": "04"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "No sujeto",
|
||||||
|
"code": "iva_no_sujeto",
|
||||||
|
"value": 0,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IVA",
|
||||||
|
"description": "Operación no sujeta a IVA.",
|
||||||
|
"aeat_code": "06"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IVA Intracomunitario Bienes",
|
||||||
|
"code": "iva_intracomunitario_bienes",
|
||||||
|
"value": 0,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IVA",
|
||||||
|
"description": "Entrega intracomunitaria de bienes, exenta de IVA.",
|
||||||
|
"aeat_code": "E5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IVA Intracomunitario Servicio",
|
||||||
|
"code": "iva_intracomunitario_servicio",
|
||||||
|
"value": 0,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IVA",
|
||||||
|
"description": "Prestación intracomunitaria de servicios, exenta.",
|
||||||
|
"aeat_code": "E6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Exportación",
|
||||||
|
"code": "iva_exportacion",
|
||||||
|
"value": 0,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IVA",
|
||||||
|
"description": "Exportaciones exentas de IVA.",
|
||||||
|
"aeat_code": "E2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Inv. Suj. Pasivo",
|
||||||
|
"code": "iva_inversion_sujeto_pasivo",
|
||||||
|
"value": 0,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IVA",
|
||||||
|
"description": "Inversión del sujeto pasivo.",
|
||||||
|
"aeat_code": "09"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Retención 35%",
|
||||||
|
"code": "retencion_35",
|
||||||
|
"value": 3500,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "Retención",
|
||||||
|
"description": "Retención profesional o fiscal tipo máximo.",
|
||||||
|
"aeat_code": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Retención 19%",
|
||||||
|
"code": "retencion_19",
|
||||||
|
"value": 1900,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "Retención",
|
||||||
|
"description": "Retención IRPF general.",
|
||||||
|
"aeat_code": "R1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Retención 15%",
|
||||||
|
"code": "retencion_15",
|
||||||
|
"value": 1500,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "Retención",
|
||||||
|
"description": "Retención para autónomos y profesionales.",
|
||||||
|
"aeat_code": "R2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Retención 7%",
|
||||||
|
"code": "retencion_7",
|
||||||
|
"value": 700,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "Retención",
|
||||||
|
"description": "Retención para nuevos autónomos.",
|
||||||
|
"aeat_code": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Retención 2%",
|
||||||
|
"code": "retencion_2",
|
||||||
|
"value": 200,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "Retención",
|
||||||
|
"description": "Retención sobre arrendamientos de inmuebles urbanos.",
|
||||||
|
"aeat_code": "R3"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "REC 5,2%",
|
||||||
|
"code": "rec_5_2",
|
||||||
|
"value": 520,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "Recargo de equivalencia",
|
||||||
|
"description": "Recargo general para IVA 21%.",
|
||||||
|
"aeat_code": "51"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "REC 1,75%",
|
||||||
|
"code": "rec_1_75",
|
||||||
|
"value": 175,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "Recargo de equivalencia",
|
||||||
|
"description": "Recargo para IVA 10%.",
|
||||||
|
"aeat_code": "52"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "REC 1,4%",
|
||||||
|
"code": "rec_1_4",
|
||||||
|
"value": 140,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "Recargo de equivalencia",
|
||||||
|
"description": "Recargo para IVA 5%.",
|
||||||
|
"aeat_code": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "REC 1%",
|
||||||
|
"code": "rec_1",
|
||||||
|
"value": 100,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "Recargo de equivalencia",
|
||||||
|
"description": "Recargo especial.",
|
||||||
|
"aeat_code": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "REC 0,62%",
|
||||||
|
"code": "rec_0_62",
|
||||||
|
"value": 62,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "Recargo de equivalencia",
|
||||||
|
"description": "Recargo para IVA reducido especial.",
|
||||||
|
"aeat_code": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "REC 0,5%",
|
||||||
|
"code": "rec_0_5",
|
||||||
|
"value": 50,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "Recargo de equivalencia",
|
||||||
|
"description": "Recargo especial.",
|
||||||
|
"aeat_code": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "REC 0,26%",
|
||||||
|
"code": "rec_0_26",
|
||||||
|
"value": 26,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "Recargo de equivalencia",
|
||||||
|
"description": "Recargo mínimo.",
|
||||||
|
"aeat_code": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "REC 0%",
|
||||||
|
"code": "rec_0",
|
||||||
|
"value": 0,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "Recargo de equivalencia",
|
||||||
|
"description": "Sin recargo.",
|
||||||
|
"aeat_code": null
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "IGIC 7%",
|
||||||
|
"code": "igic_7",
|
||||||
|
"value": 700,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IGIC",
|
||||||
|
"description": "Tipo general IGIC Canarias.",
|
||||||
|
"aeat_code": "10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IGIC 3%",
|
||||||
|
"code": "igic_3",
|
||||||
|
"value": 300,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IGIC",
|
||||||
|
"description": "Tipo reducido IGIC Canarias.",
|
||||||
|
"aeat_code": "11"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IGIC 0%",
|
||||||
|
"code": "igic_0",
|
||||||
|
"value": 0,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IGIC",
|
||||||
|
"description": "Operación exenta IGIC.",
|
||||||
|
"aeat_code": "12"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IGIC 9,5%",
|
||||||
|
"code": "igic_9_5",
|
||||||
|
"value": 950,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IGIC",
|
||||||
|
"description": "Tipo incrementado IGIC.",
|
||||||
|
"aeat_code": "13"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IGIC 13,5%",
|
||||||
|
"code": "igic_13_5",
|
||||||
|
"value": 1350,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IGIC",
|
||||||
|
"description": "Tipo incrementado especial IGIC.",
|
||||||
|
"aeat_code": "14"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IGIC 20%",
|
||||||
|
"code": "igic_20",
|
||||||
|
"value": 2000,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IGIC",
|
||||||
|
"description": "Tipo incrementado IGIC.",
|
||||||
|
"aeat_code": "15"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IGIC 1%",
|
||||||
|
"code": "igic_1",
|
||||||
|
"value": 100,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IGIC",
|
||||||
|
"description": "Tipo reducido IGIC.",
|
||||||
|
"aeat_code": "16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IGIC 2,75%",
|
||||||
|
"code": "igic_2_75",
|
||||||
|
"value": 275,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IGIC",
|
||||||
|
"description": "Tipo especial IGIC.",
|
||||||
|
"aeat_code": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IGIC Exento",
|
||||||
|
"code": "igic_exento",
|
||||||
|
"value": 0,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IGIC",
|
||||||
|
"description": "Operación exenta de IGIC.",
|
||||||
|
"aeat_code": "12"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "IPSI 10%",
|
||||||
|
"code": "ipsi_10",
|
||||||
|
"value": 1000,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IPSI",
|
||||||
|
"description": "Tipo general IPSI Ceuta/Melilla.",
|
||||||
|
"aeat_code": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IPSI 4%",
|
||||||
|
"code": "ipsi_4",
|
||||||
|
"value": 400,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IPSI",
|
||||||
|
"description": "Tipo reducido IPSI.",
|
||||||
|
"aeat_code": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IPSI 0,5%",
|
||||||
|
"code": "ipsi_0_5",
|
||||||
|
"value": 50,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IPSI",
|
||||||
|
"description": "Tipo superreducido IPSI.",
|
||||||
|
"aeat_code": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IPSI Exento",
|
||||||
|
"code": "ipsi_exento",
|
||||||
|
"value": 0,
|
||||||
|
"scale": 2,
|
||||||
|
"group": "IPSI",
|
||||||
|
"description": "Operación exenta de IPSI.",
|
||||||
|
"aeat_code": null
|
||||||
|
}
|
||||||
|
]
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
// --- Puerto (interfaz) para resolver tasas desde un catálogo ---
|
||||||
|
|
||||||
|
import { Maybe } from "@repo/rdx-utils"; // Usa tu implementación real de Maybe
|
||||||
|
import { TaxItemType } from "./tax-catalog-types";
|
||||||
|
|
||||||
|
export interface TaxCatalogProvider {
|
||||||
|
/**
|
||||||
|
* Busca un item del catálogo por su código.
|
||||||
|
* Debe considerar normalización del 'code' según las reglas del catálogo.
|
||||||
|
*/
|
||||||
|
findByCode(code: string): Maybe<TaxItemType>;
|
||||||
|
}
|
||||||
13
modules/core/src/common/catalogs/taxes/tax-catalog-types.ts
Normal file
13
modules/core/src/common/catalogs/taxes/tax-catalog-types.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// --- DTOs del catálogo (comparten contrato entre frontend/backend) ---
|
||||||
|
|
||||||
|
export type TaxItemType = {
|
||||||
|
name: string; // p.ej. "IVA 21%"
|
||||||
|
code: string; // p.ej. "iva_21"
|
||||||
|
value: number; // porcentaje * 10^scale (21% => 2100)
|
||||||
|
scale: number; // decimales de 'value' (normalmente 2)
|
||||||
|
group: string; // p.ej. "IVA", "IGIC", "IPSI", "Retención"
|
||||||
|
description?: string; // opcional
|
||||||
|
aeat_code?: string | null; // opcional
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TaxCatalogType = TaxItemType[];
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
export * from "./catalogs";
|
||||||
export * from "./dto";
|
export * from "./dto";
|
||||||
export * from "./schemas";
|
export * from "./schemas";
|
||||||
export * from "./types";
|
export * from "./types";
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
|
import { spainTaxCatalogProvider } from "@erp/core";
|
||||||
import {
|
import {
|
||||||
DomainError,
|
DomainError,
|
||||||
|
Tax,
|
||||||
|
Taxes,
|
||||||
ValidationErrorCollection,
|
ValidationErrorCollection,
|
||||||
ValidationErrorDetail,
|
ValidationErrorDetail,
|
||||||
extractOrPushError,
|
extractOrPushError,
|
||||||
@ -14,17 +17,22 @@ import {
|
|||||||
maybeFromNullableVO,
|
maybeFromNullableVO,
|
||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
import { Result } from "@repo/rdx-utils";
|
import { Result } from "@repo/rdx-utils";
|
||||||
import { CreateCustomerInvoiceRequestDTO } from "../../../common/dto";
|
|
||||||
import {
|
import {
|
||||||
|
CreateCustomerInvoiceItemRequestDTO,
|
||||||
|
CreateCustomerInvoiceRequestDTO,
|
||||||
|
} from "../../../common/dto";
|
||||||
|
import {
|
||||||
|
CustomerInvoiceItem,
|
||||||
CustomerInvoiceItemDescription,
|
CustomerInvoiceItemDescription,
|
||||||
CustomerInvoiceItemProps,
|
CustomerInvoiceItemProps,
|
||||||
CustomerInvoiceItemQuantity,
|
CustomerInvoiceItems,
|
||||||
CustomerInvoiceNumber,
|
CustomerInvoiceNumber,
|
||||||
CustomerInvoiceProps,
|
CustomerInvoiceProps,
|
||||||
CustomerInvoiceSerie,
|
CustomerInvoiceSerie,
|
||||||
CustomerInvoiceStatus,
|
CustomerInvoiceStatus,
|
||||||
ItemAmount,
|
ItemAmount,
|
||||||
ItemDiscount,
|
ItemDiscount,
|
||||||
|
ItemQuantity,
|
||||||
} from "../../domain";
|
} from "../../domain";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -96,7 +104,7 @@ export function mapDTOToCreateCustomerInvoiceProps(dto: CreateCustomerInvoiceReq
|
|||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
const items = mapDTOToCreateCustomerInvoiceItemsProps(dto, errors);
|
const items = _mapDTOtoInvoiceItems(dto.items, languageCode!, currencyCode!, errors);
|
||||||
|
|
||||||
if (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
return Result.fail(
|
return Result.fail(
|
||||||
@ -120,6 +128,9 @@ export function mapDTOToCreateCustomerInvoiceProps(dto: CreateCustomerInvoiceReq
|
|||||||
currencyCode: currencyCode!,
|
currencyCode: currencyCode!,
|
||||||
|
|
||||||
discountPercentage: discountPercentage!,
|
discountPercentage: discountPercentage!,
|
||||||
|
|
||||||
|
taxes: Taxes.create({ items: [] }),
|
||||||
|
items: items,
|
||||||
};
|
};
|
||||||
|
|
||||||
return Result.ok({ id: customerId!, props: invoiceProps });
|
return Result.ok({ id: customerId!, props: invoiceProps });
|
||||||
@ -128,25 +139,19 @@ export function mapDTOToCreateCustomerInvoiceProps(dto: CreateCustomerInvoiceReq
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDTOToCreateCustomerInvoiceItemsProps(
|
function _mapDTOtoInvoiceItems(
|
||||||
dto: CreateCustomerInvoiceRequestDTO,
|
items: CreateCustomerInvoiceItemRequestDTO[],
|
||||||
|
languageCode: LanguageCode,
|
||||||
|
currencyCode: CurrencyCode,
|
||||||
errors: ValidationErrorDetail[]
|
errors: ValidationErrorDetail[]
|
||||||
): CustomerInvoiceItemProps[] | undefined {
|
) {
|
||||||
const items: CustomerInvoiceItemProps[] = [];
|
const invoiceItems = CustomerInvoiceItems.create({
|
||||||
|
currencyCode,
|
||||||
|
languageCode,
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
|
||||||
const languageCode = extractOrPushError(
|
items.forEach((item, index) => {
|
||||||
LanguageCode.create(dto.language_code),
|
|
||||||
"language_code",
|
|
||||||
errors
|
|
||||||
);
|
|
||||||
|
|
||||||
const currencyCode = extractOrPushError(
|
|
||||||
CurrencyCode.create(dto.currency_code),
|
|
||||||
"currency_code",
|
|
||||||
errors
|
|
||||||
);
|
|
||||||
|
|
||||||
dto.items.forEach((item, index) => {
|
|
||||||
const description = extractOrPushError(
|
const description = extractOrPushError(
|
||||||
maybeFromNullableVO(item.description, (value) =>
|
maybeFromNullableVO(item.description, (value) =>
|
||||||
CustomerInvoiceItemDescription.create(value)
|
CustomerInvoiceItemDescription.create(value)
|
||||||
@ -156,7 +161,7 @@ function mapDTOToCreateCustomerInvoiceItemsProps(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const quantity = extractOrPushError(
|
const quantity = extractOrPushError(
|
||||||
maybeFromNullableVO(item.quantity, (value) => CustomerInvoiceItemQuantity.create(value)),
|
maybeFromNullableVO(item.quantity, (value) => ItemQuantity.create(value)),
|
||||||
"quantity",
|
"quantity",
|
||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
@ -173,16 +178,48 @@ function mapDTOToCreateCustomerInvoiceItemsProps(
|
|||||||
errors
|
errors
|
||||||
);
|
);
|
||||||
|
|
||||||
items.push({
|
const taxes = _mapDTOtoTaxes(item, index, errors);
|
||||||
|
|
||||||
|
const itemProps: CustomerInvoiceItemProps = {
|
||||||
currencyCode: currencyCode!,
|
currencyCode: currencyCode!,
|
||||||
languageCode: languageCode!,
|
languageCode: languageCode!,
|
||||||
|
|
||||||
description: description!,
|
description: description!,
|
||||||
quantity: quantity!,
|
quantity: quantity!,
|
||||||
unitAmount: unitAmount!,
|
unitAmount: unitAmount!,
|
||||||
discountPercentage: discountPercentage!,
|
discountPercentage: discountPercentage!,
|
||||||
});
|
taxes,
|
||||||
});
|
};
|
||||||
|
|
||||||
return items;
|
const itemResult = CustomerInvoiceItem.create(itemProps);
|
||||||
|
if (itemResult.isSuccess) {
|
||||||
|
invoiceItems.add(itemResult.data);
|
||||||
|
} else {
|
||||||
|
errors.push({
|
||||||
|
path: `items[${index}]`,
|
||||||
|
message: itemResult.error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return invoiceItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _mapDTOtoTaxes(
|
||||||
|
item: CreateCustomerInvoiceItemRequestDTO,
|
||||||
|
itemIndex: number,
|
||||||
|
errors: ValidationErrorDetail[]
|
||||||
|
) {
|
||||||
|
const taxes = Taxes.create({ items: [] });
|
||||||
|
|
||||||
|
item.taxes.split(",").every((tax_code, taxIndex) => {
|
||||||
|
const taxResult = Tax.createFromCode(tax_code, spainTaxCatalogProvider);
|
||||||
|
if (taxResult.isSuccess) {
|
||||||
|
taxes.add(taxResult.data);
|
||||||
|
} else {
|
||||||
|
errors.push({
|
||||||
|
path: `items[${itemIndex}].taxes[${taxIndex}]`,
|
||||||
|
message: taxResult.error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return taxes;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { Taxes } from "@erp/core/api";
|
||||||
import {
|
import {
|
||||||
AggregateRoot,
|
AggregateRoot,
|
||||||
CurrencyCode,
|
CurrencyCode,
|
||||||
@ -47,8 +48,7 @@ export interface CustomerInvoiceProps {
|
|||||||
discountPercentage: Percentage;
|
discountPercentage: Percentage;
|
||||||
//discountAmount: MoneyValue;
|
//discountAmount: MoneyValue;
|
||||||
|
|
||||||
//taxableAmount: MoneyValue;
|
taxes: Taxes;
|
||||||
//taxAmount: MoneyValue;
|
|
||||||
|
|
||||||
//totalAmount: MoneyValue;
|
//totalAmount: MoneyValue;
|
||||||
|
|
||||||
@ -60,7 +60,6 @@ export type CustomerInvoicePatchProps = Partial<Omit<CustomerInvoiceProps, "comp
|
|||||||
|
|
||||||
export class CustomerInvoice extends AggregateRoot<CustomerInvoiceProps> {
|
export class CustomerInvoice extends AggregateRoot<CustomerInvoiceProps> {
|
||||||
private _items!: CustomerInvoiceItems;
|
private _items!: CustomerInvoiceItems;
|
||||||
//protected _status: CustomerInvoiceStatus;
|
|
||||||
|
|
||||||
protected constructor(props: CustomerInvoiceProps, id?: UniqueID) {
|
protected constructor(props: CustomerInvoiceProps, id?: UniqueID) {
|
||||||
super(props, id);
|
super(props, id);
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import {
|
|||||||
LanguageCode,
|
LanguageCode,
|
||||||
MoneyValue,
|
MoneyValue,
|
||||||
Percentage,
|
Percentage,
|
||||||
|
Taxes,
|
||||||
UniqueID,
|
UniqueID,
|
||||||
} from "@repo/rdx-ddd";
|
} from "@repo/rdx-ddd";
|
||||||
import { Maybe, Result } from "@repo/rdx-utils";
|
import { Maybe, Result } from "@repo/rdx-utils";
|
||||||
@ -20,6 +21,8 @@ export interface CustomerInvoiceItemProps {
|
|||||||
unitAmount: Maybe<ItemAmount>; // Precio unitario en la moneda de la factura
|
unitAmount: Maybe<ItemAmount>; // Precio unitario en la moneda de la factura
|
||||||
discountPercentage: Maybe<ItemDiscount>; // % descuento
|
discountPercentage: Maybe<ItemDiscount>; // % descuento
|
||||||
|
|
||||||
|
taxes: Taxes;
|
||||||
|
|
||||||
languageCode: LanguageCode;
|
languageCode: LanguageCode;
|
||||||
currencyCode: CurrencyCode;
|
currencyCode: CurrencyCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
|
|||||||
private _currencyCode!: CurrencyCode;
|
private _currencyCode!: CurrencyCode;
|
||||||
|
|
||||||
constructor(props: CustomerInvoiceItemsProps) {
|
constructor(props: CustomerInvoiceItemsProps) {
|
||||||
const { items, languageCode, currencyCode } = props;
|
const { items = [], languageCode, currencyCode } = props;
|
||||||
super(items);
|
super(items);
|
||||||
this._languageCode = languageCode;
|
this._languageCode = languageCode;
|
||||||
this._currencyCode = currencyCode;
|
this._currencyCode = currencyCode;
|
||||||
@ -22,4 +22,15 @@ export class CustomerInvoiceItems extends Collection<CustomerInvoiceItem> {
|
|||||||
public static create(props: CustomerInvoiceItemsProps): CustomerInvoiceItems {
|
public static create(props: CustomerInvoiceItemsProps): CustomerInvoiceItems {
|
||||||
return new CustomerInvoiceItems(props);
|
return new CustomerInvoiceItems(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
add(item: CustomerInvoiceItem): boolean {
|
||||||
|
// Antes de añadir un nuevo item, debo comprobar que el item a añadir
|
||||||
|
// tiene el mismo "currencyCode" y "languageCode" que la colección de items.
|
||||||
|
if (!this._languageCode.equals(item.languageCode) || !this._currencyCode.equals(item.currencyCode)) {
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
return super.add(item)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,7 +93,7 @@ export default (database: Sequelize) => {
|
|||||||
|
|
||||||
underscored: true,
|
underscored: true,
|
||||||
|
|
||||||
indexes: [{ name: "tax_code_idx", fields: ["tax_code"], unique: false }],
|
indexes: [],
|
||||||
|
|
||||||
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
||||||
|
|
||||||
|
|||||||
@ -93,7 +93,7 @@ export default (database: Sequelize) => {
|
|||||||
|
|
||||||
underscored: true,
|
underscored: true,
|
||||||
|
|
||||||
indexes: [{ name: "tax_code_idx", fields: ["tax_code"], unique: false }],
|
indexes: [],
|
||||||
|
|
||||||
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,16 @@
|
|||||||
import { NumericStringSchema, PercentageSchema } from "@erp/core";
|
import { NumericStringSchema, PercentageSchema } from "@erp/core";
|
||||||
import * as z from "zod/v4";
|
import * as z from "zod/v4";
|
||||||
|
|
||||||
|
export const CreateCustomerInvoiceItemRequestSchema = z.object({
|
||||||
|
id: z.uuid(),
|
||||||
|
position: z.string(),
|
||||||
|
description: z.string().default(""),
|
||||||
|
quantity: NumericStringSchema.default(""),
|
||||||
|
unit_amount: NumericStringSchema.default(""),
|
||||||
|
discount_percentage: NumericStringSchema.default(""),
|
||||||
|
taxes: z.string().default(""),
|
||||||
|
});
|
||||||
|
|
||||||
export const CreateCustomerInvoiceRequestSchema = z.object({
|
export const CreateCustomerInvoiceRequestSchema = z.object({
|
||||||
id: z.uuid(),
|
id: z.uuid(),
|
||||||
company_id: z.uuid(),
|
company_id: z.uuid(),
|
||||||
@ -22,18 +32,10 @@ export const CreateCustomerInvoiceRequestSchema = z.object({
|
|||||||
scale: "2",
|
scale: "2",
|
||||||
}),
|
}),
|
||||||
|
|
||||||
items: z
|
items: z.array(CreateCustomerInvoiceItemRequestSchema).default([]),
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
id: z.uuid(),
|
|
||||||
position: z.string(),
|
|
||||||
description: z.string().default(""),
|
|
||||||
quantity: NumericStringSchema.default(""),
|
|
||||||
unit_amount: NumericStringSchema.default(""),
|
|
||||||
discount_percentage: NumericStringSchema.default(""),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.default([]),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type CreateCustomerInvoiceItemRequestDTO = z.infer<
|
||||||
|
typeof CreateCustomerInvoiceItemRequestSchema
|
||||||
|
>;
|
||||||
export type CreateCustomerInvoiceRequestDTO = z.infer<typeof CreateCustomerInvoiceRequestSchema>;
|
export type CreateCustomerInvoiceRequestDTO = z.infer<typeof CreateCustomerInvoiceRequestSchema>;
|
||||||
|
|||||||
@ -21,7 +21,9 @@ export const GetCustomerInvoiceByIdResponseSchema = z.object({
|
|||||||
discount_percentage: PercentageSchema,
|
discount_percentage: PercentageSchema,
|
||||||
discount_amount: AmountSchema,
|
discount_amount: AmountSchema,
|
||||||
taxable_amount: AmountSchema,
|
taxable_amount: AmountSchema,
|
||||||
tax_amount: AmountSchema,
|
|
||||||
|
taxes: z.string(),
|
||||||
|
|
||||||
total_amount: AmountSchema,
|
total_amount: AmountSchema,
|
||||||
|
|
||||||
items: z.array(
|
items: z.array(
|
||||||
@ -32,6 +34,7 @@ export const GetCustomerInvoiceByIdResponseSchema = z.object({
|
|||||||
quantity: QuantitySchema,
|
quantity: QuantitySchema,
|
||||||
unit_amount: AmountSchema,
|
unit_amount: AmountSchema,
|
||||||
discount_percentage: PercentageSchema,
|
discount_percentage: PercentageSchema,
|
||||||
|
taxes: z.string(),
|
||||||
total_amount: AmountSchema,
|
total_amount: AmountSchema,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
|||||||
@ -28,11 +28,12 @@ export class Collection<T> {
|
|||||||
* Agrega un nuevo elemento a la colección.
|
* Agrega un nuevo elemento a la colección.
|
||||||
* @param item - Elemento a agregar.
|
* @param item - Elemento a agregar.
|
||||||
*/
|
*/
|
||||||
add(item: T): void {
|
add(item: T): boolean {
|
||||||
this.items.push(item);
|
this.items.push(item);
|
||||||
if (this.totalItems !== null) {
|
if (this.totalItems !== null) {
|
||||||
this.totalItems++;
|
this.totalItems++;
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user