From 1f666a1a5d6bed3c76e817407d29387cc799bd24 Mon Sep 17 00:00:00 2001 From: david Date: Mon, 24 Feb 2025 20:00:28 +0100 Subject: [PATCH] . --- apps/server/.eslintrc.json | 6 +- apps/server/package.json | 2 + .../domain/value-objects/email-address.ts | 2 +- .../src/common/domain/value-objects/index.ts | 3 + .../domain/value-objects/money-value.spec.ts | 53 ++++ .../domain/value-objects/money-value.ts | 149 +++++++++++ .../common/domain/value-objects/name.spec.ts | 2 +- .../src/common/domain/value-objects/name.ts | 8 +- .../domain/value-objects/percentage.spec.ts | 51 ++++ .../common/domain/value-objects/percentage.ts | 83 ++++++ .../domain/value-objects/phone-number.ts | 2 +- .../domain/value-objects/postal-address.ts | 2 +- .../common/domain/value-objects/quantity.ts | 122 +++++++++ .../common/domain/value-objects/slug.spec.ts | 2 +- .../src/common/domain/value-objects/slug.ts | 2 +- .../common/domain/value-objects/tin-number.ts | 2 +- .../domain/value-objects/unique-id.spec.ts | 16 +- .../common/domain/value-objects/unique-id.ts | 2 +- .../value-objects/value-objects.spec.ts | 14 +- apps/server/src/common/helpers/maybe.spec.ts | 8 +- apps/server/src/common/helpers/maybe.ts | 6 +- .../auth/domain/services/user.service.ts | 6 +- .../listUsers/list-users.presenter.ts | 6 +- .../application/customer-invoices/index.ts | 0 .../application/customers/index.ts | 1 + .../customers/list-customers/index.ts | 1 + .../list-customers/list-customers.use-case.ts | 17 ++ .../customer-billing/application/index.ts | 2 + .../customer-invoice/customer-invoice.ts | 25 ++ .../aggregates/customer-invoice/index.ts | 1 + .../domain/aggregates/customer/customer.ts | 130 +++++++++ .../domain/aggregates/customer/index.ts | 1 + .../domain/aggregates/index.ts | 2 + .../domain/events/customer-invoice-item.ts | 61 +++++ .../contexts/customer-billing/domain/index.ts | 3 + .../customer-repository.interface.ts | 9 + .../domain/repositories/index.ts | 1 + .../services/customer-service.interface.ts | 8 + .../domain/services/customer.service.ts | 24 ++ .../customer-billing/domain/services/index.ts | 2 + .../infraestructure/index.ts | 0 .../mappers/customer.mapper.ts | 100 +++++++ .../infraestructure/mappers/index.ts | 1 + .../sequelize/customer-invoice-item.model.ts | 100 +++++++ .../sequelize/customer-invoice.model.ts | 251 ++++++++++++++++++ .../sequelize/customer.model.ts | 172 ++++++++++++ .../sequelize/customer.repository.ts | 81 ++++++ .../infraestructure/sequelize/index.ts | 9 + .../customer-invoices/list/index.ts | 2 + .../list/list-customer-invoices.controller.ts | 37 +++ .../list/list-customer-invoices.presenter.ts | 38 +++ .../controllers/customers/index.ts | 1 + .../controllers/customers/list/index.ts | 16 ++ .../list/list-customers.controller.ts | 37 +++ .../list/list-customers.presenter.ts | 38 +++ .../presentation/controllers/index.ts | 2 + .../customer-invoices.request.dto.ts | 1 + .../customer-invoices.response.dto.ts | 27 ++ .../customer-invoices.validation.dto.ts | 3 + .../dto/customer-invoices/index.ts | 3 + .../presentation/dto/customers.request.dto.ts | 1 + .../dto/customers.response.dto.ts | 27 ++ .../dto/customers.validation.dto.ts | 3 + .../presentation/dto/index.ts | 4 + .../customer-billing/presentation/index.ts | 2 + .../server/src/contexts/sales/domain/index.ts | 5 - .../src/contexts/sales/presentation/index.ts | 2 - apps/server/src/routes/auth.routes.ts | 12 +- apps/server/src/routes/companies.routes.ts | 6 +- .../src/routes/customer-invoices.routes.ts | 20 ++ apps/server/src/routes/customers.routes.ts | 17 +- apps/server/src/routes/users.routes.ts | 6 +- apps/server/src/routes/v1.routes.ts | 4 + doc/DsRegistroVeriFactu.xlsx | Bin 0 -> 59877 bytes packages/rdx-ddd/package.json | 12 + packages/rdx-verifactu/.eslintrc.json | 18 ++ packages/rdx-verifactu/.gitignore | 2 + packages/rdx-verifactu/package.json | 12 + packages/rdx-verifactu/tsconfig.json | 20 ++ pnpm-lock.yaml | 20 ++ 80 files changed, 1887 insertions(+), 62 deletions(-) create mode 100644 apps/server/src/common/domain/value-objects/money-value.spec.ts create mode 100644 apps/server/src/common/domain/value-objects/money-value.ts create mode 100644 apps/server/src/common/domain/value-objects/percentage.spec.ts create mode 100644 apps/server/src/common/domain/value-objects/percentage.ts create mode 100644 apps/server/src/common/domain/value-objects/quantity.ts create mode 100644 apps/server/src/contexts/customer-billing/application/customer-invoices/index.ts create mode 100644 apps/server/src/contexts/customer-billing/application/customers/index.ts create mode 100644 apps/server/src/contexts/customer-billing/application/customers/list-customers/index.ts create mode 100644 apps/server/src/contexts/customer-billing/application/customers/list-customers/list-customers.use-case.ts create mode 100644 apps/server/src/contexts/customer-billing/application/index.ts create mode 100644 apps/server/src/contexts/customer-billing/domain/aggregates/customer-invoice/customer-invoice.ts create mode 100644 apps/server/src/contexts/customer-billing/domain/aggregates/customer-invoice/index.ts create mode 100644 apps/server/src/contexts/customer-billing/domain/aggregates/customer/customer.ts create mode 100644 apps/server/src/contexts/customer-billing/domain/aggregates/customer/index.ts create mode 100644 apps/server/src/contexts/customer-billing/domain/aggregates/index.ts create mode 100644 apps/server/src/contexts/customer-billing/domain/events/customer-invoice-item.ts create mode 100644 apps/server/src/contexts/customer-billing/domain/index.ts create mode 100644 apps/server/src/contexts/customer-billing/domain/repositories/customer-repository.interface.ts create mode 100644 apps/server/src/contexts/customer-billing/domain/repositories/index.ts create mode 100644 apps/server/src/contexts/customer-billing/domain/services/customer-service.interface.ts create mode 100644 apps/server/src/contexts/customer-billing/domain/services/customer.service.ts create mode 100644 apps/server/src/contexts/customer-billing/domain/services/index.ts rename apps/server/src/contexts/{sales => customer-billing}/infraestructure/index.ts (100%) create mode 100644 apps/server/src/contexts/customer-billing/infraestructure/mappers/customer.mapper.ts create mode 100644 apps/server/src/contexts/customer-billing/infraestructure/mappers/index.ts create mode 100644 apps/server/src/contexts/customer-billing/infraestructure/sequelize/customer-invoice-item.model.ts create mode 100644 apps/server/src/contexts/customer-billing/infraestructure/sequelize/customer-invoice.model.ts create mode 100644 apps/server/src/contexts/customer-billing/infraestructure/sequelize/customer.model.ts create mode 100644 apps/server/src/contexts/customer-billing/infraestructure/sequelize/customer.repository.ts create mode 100644 apps/server/src/contexts/customer-billing/infraestructure/sequelize/index.ts create mode 100644 apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/list/index.ts create mode 100644 apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/list/list-customer-invoices.controller.ts create mode 100644 apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/list/list-customer-invoices.presenter.ts create mode 100644 apps/server/src/contexts/customer-billing/presentation/controllers/customers/index.ts create mode 100644 apps/server/src/contexts/customer-billing/presentation/controllers/customers/list/index.ts create mode 100644 apps/server/src/contexts/customer-billing/presentation/controllers/customers/list/list-customers.controller.ts create mode 100644 apps/server/src/contexts/customer-billing/presentation/controllers/customers/list/list-customers.presenter.ts create mode 100644 apps/server/src/contexts/customer-billing/presentation/controllers/index.ts create mode 100644 apps/server/src/contexts/customer-billing/presentation/dto/customer-invoices/customer-invoices.request.dto.ts create mode 100644 apps/server/src/contexts/customer-billing/presentation/dto/customer-invoices/customer-invoices.response.dto.ts create mode 100644 apps/server/src/contexts/customer-billing/presentation/dto/customer-invoices/customer-invoices.validation.dto.ts create mode 100644 apps/server/src/contexts/customer-billing/presentation/dto/customer-invoices/index.ts create mode 100644 apps/server/src/contexts/customer-billing/presentation/dto/customers.request.dto.ts create mode 100644 apps/server/src/contexts/customer-billing/presentation/dto/customers.response.dto.ts create mode 100644 apps/server/src/contexts/customer-billing/presentation/dto/customers.validation.dto.ts create mode 100644 apps/server/src/contexts/customer-billing/presentation/dto/index.ts create mode 100644 apps/server/src/contexts/customer-billing/presentation/index.ts delete mode 100644 apps/server/src/contexts/sales/domain/index.ts delete mode 100644 apps/server/src/contexts/sales/presentation/index.ts create mode 100644 apps/server/src/routes/customer-invoices.routes.ts create mode 100644 doc/DsRegistroVeriFactu.xlsx create mode 100644 packages/rdx-ddd/package.json create mode 100644 packages/rdx-verifactu/.eslintrc.json create mode 100644 packages/rdx-verifactu/.gitignore create mode 100644 packages/rdx-verifactu/package.json create mode 100644 packages/rdx-verifactu/tsconfig.json diff --git a/apps/server/.eslintrc.json b/apps/server/.eslintrc.json index f324a57e..039be3de 100644 --- a/apps/server/.eslintrc.json +++ b/apps/server/.eslintrc.json @@ -12,7 +12,11 @@ "prettier" ], "parser": "@typescript-eslint/parser", - "plugins": ["@typescript-eslint", "sort-class-members"], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": ["@typescript-eslint", "sort-class-members", "hexagonal-architecture"], "rules": { "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-inferrable-types": "off", diff --git a/apps/server/package.json b/apps/server/package.json index 19820492..2bfe9a72 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -48,9 +48,11 @@ "typescript": "^5.7.3" }, "dependencies": { + "@types/dinero.js": "^1.9.4", "bcrypt": "^5.1.1", "cls-rtracer": "^2.6.3", "cors": "^2.8.5", + "dinero.js": "^1.9.1", "dotenv": "^16.4.7", "esbuild": "^0.24.0", "express": "^4.18.2", diff --git a/apps/server/src/common/domain/value-objects/email-address.ts b/apps/server/src/common/domain/value-objects/email-address.ts index 5d898619..f5e37634 100644 --- a/apps/server/src/common/domain/value-objects/email-address.ts +++ b/apps/server/src/common/domain/value-objects/email-address.ts @@ -11,7 +11,7 @@ export class EmailAddress extends ValueObject { const valueIsValid = EmailAddress.validate(value); if (!valueIsValid.success) { - Result.fail(new Error(valueIsValid.error.errors[0].message)); + return Result.fail(new Error(valueIsValid.error.errors[0].message)); } return Result.ok(new EmailAddress({ value: valueIsValid.data! })); } diff --git a/apps/server/src/common/domain/value-objects/index.ts b/apps/server/src/common/domain/value-objects/index.ts index 9a566d27..d3dca809 100644 --- a/apps/server/src/common/domain/value-objects/index.ts +++ b/apps/server/src/common/domain/value-objects/index.ts @@ -1,7 +1,10 @@ export * from "./email-address"; +export * from "./money-value"; export * from "./name"; +export * from "./percentage"; export * from "./phone-number"; export * from "./postal-address"; +export * from "./quantity"; export * from "./slug"; export * from "./tin-number"; export * from "./unique-id"; diff --git a/apps/server/src/common/domain/value-objects/money-value.spec.ts b/apps/server/src/common/domain/value-objects/money-value.spec.ts new file mode 100644 index 00000000..74e9b62d --- /dev/null +++ b/apps/server/src/common/domain/value-objects/money-value.spec.ts @@ -0,0 +1,53 @@ +import { MoneyValue } from "./money-value"; + +describe("MoneyValue", () => { + test("should correctly instantiate with amount, scale, and currency", () => { + const money = new MoneyValue({ amount: 10000, scale: 2, currency_code: "EUR" }); + expect(money.amount).toBe(100); + expect(money.currency).toBe("EUR"); + expect(money.scale).toBe(2); + }); + + test("should add two MoneyValue instances with the same currency", () => { + const money1 = new MoneyValue({ amount: 10000, scale: 2, currency_code: "EUR" }); + const money2 = new MoneyValue({ amount: 5000, scale: 2, currency_code: "EUR" }); + const result = money1.add(money2); + expect(result.amount).toBe(150); + }); + + test("should subtract two MoneyValue instances with the same currency", () => { + const money1 = new MoneyValue({ amount: 20000, scale: 2, currency_code: "EUR" }); + const money2 = new MoneyValue({ amount: 5000, scale: 2, currency_code: "EUR" }); + const result = money1.subtract(money2); + expect(result.amount).toBe(150); + }); + + test("should throw an error when adding different currencies", () => { + const money1 = new MoneyValue({ amount: 10000, scale: 2, currency_code: "EUR" }); + const money2 = new MoneyValue({ amount: 5000, scale: 2, currency_code: "USD" }); + expect(() => money1.add(money2)).toThrow("Currency mismatch"); + }); + + test("should correctly convert scale", () => { + const money = new MoneyValue({ amount: 10000, scale: 2, currency_code: "EUR" }); + const converted = money.convertScale(4); + expect(converted.amount).toBe(100); + expect(converted.scale).toBe(4); + }); + + test("should format correctly according to locale", () => { + const money = new MoneyValue({ amount: 123456, scale: 2, currency_code: "EUR" }); + expect(money.format("es-ES")).toBe("1.234,56 €"); + expect(money.format("en-US")).toBe("€1,234.56"); + }); + + test("should compare MoneyValue instances correctly", () => { + const money1 = new MoneyValue({ amount: 10000, scale: 2, currency_code: "EUR" }); + const money2 = new MoneyValue({ amount: 10000, scale: 2, currency_code: "EUR" }); + const money3 = new MoneyValue({ amount: 5000, scale: 2, currency_code: "EUR" }); + + expect(money1.equalsTo(money2)).toBe(true); + expect(money1.greaterThan(money3)).toBe(true); + expect(money3.lessThan(money1)).toBe(true); + }); +}); diff --git a/apps/server/src/common/domain/value-objects/money-value.ts b/apps/server/src/common/domain/value-objects/money-value.ts new file mode 100644 index 00000000..c841787c --- /dev/null +++ b/apps/server/src/common/domain/value-objects/money-value.ts @@ -0,0 +1,149 @@ +import { Result } from "@common/helpers"; +import DineroFactory, { Currency, Dinero } from "dinero.js"; +import { ValueObject } from "./value-object"; + +const DEFAULT_SCALE = 2; + +type CurrencyData = Currency; + +interface IMoneyValueProps { + amount: number; + scale: number; + currency_code: string; +} + +interface IMoneyValue { + amount: number; + scale: number; + currency: Dinero.Currency; + + getValue(): IMoneyValueProps; + convertScale(newScale: number): MoneyValue; + add(addend: MoneyValue): MoneyValue; + subtract(subtrahend: MoneyValue): MoneyValue; + multiply(multiplier: number): MoneyValue; + divide(divisor: number): MoneyValue; + equalsTo(comparator: MoneyValue): boolean; + greaterThan(comparator: MoneyValue): boolean; + lessThan(comparator: MoneyValue): boolean; + isZero(): boolean; + isPositive(): boolean; + isNegative(): boolean; + format(locale: string): string; +} + +export class MoneyValue extends ValueObject implements IMoneyValue { + private readonly value: Dinero; + + static create(props: IMoneyValueProps) { + return Result.ok(new MoneyValue(props)); + } + + constructor(props: IMoneyValueProps) { + super(props); + const { amount, scale, currency_code } = props; + this.value = Object.freeze( + DineroFactory({ + amount, + precision: scale, + currency: currency_code as Currency, + }) + ); // 🔒 Garantiza inmutabilidad + } + + get amount(): number { + return this.value.getAmount() / Math.pow(10, this.value.getPrecision()); + } + + get currency(): CurrencyData { + return this.value.getCurrency(); + } + + get scale(): number { + return this.value.getPrecision(); + } + + getValue(): IMoneyValueProps { + return this.props; + } + + convertScale(newScale: number): MoneyValue { + const factor = Math.pow(10, newScale - this.scale); + const newAmount = Math.round(this.amount * factor); + return new MoneyValue({ + amount: newAmount, + scale: newScale, + currency_code: this.currency, + }); + } + + add(addend: MoneyValue): MoneyValue { + return new MoneyValue({ + amount: this.value.add(addend.value).getAmount(), + scale: this.scale, + currency_code: this.currency, + }); + } + + subtract(subtrahend: MoneyValue): MoneyValue { + return new MoneyValue({ + amount: this.value.subtract(subtrahend.value).getAmount(), + scale: this.scale, + currency_code: this.currency, + }); + } + + multiply(multiplier: number): MoneyValue { + return new MoneyValue({ + amount: this.value.multiply(multiplier).getAmount(), + scale: this.scale, + currency_code: this.currency, + }); + } + + divide(divisor: number): MoneyValue { + return new MoneyValue({ + amount: this.value.divide(divisor).getAmount(), + scale: this.scale, + currency_code: this.currency, + }); + } + + equalsTo(comparator: MoneyValue): boolean { + return this.value.equalsTo(comparator.value); + } + + greaterThan(comparator: MoneyValue): boolean { + return this.value.greaterThan(comparator.value); + } + + lessThan(comparator: MoneyValue): boolean { + return this.value.lessThan(comparator.value); + } + + isZero(): boolean { + return this.value.isZero(); + } + + isPositive(): boolean { + return this.amount > 0; + } + + isNegative(): boolean { + return this.amount < 0; + } + + format(locale: string): string { + const amount = this.amount; + const currency = this.currency; + const scale = this.scale; + + return new Intl.NumberFormat(locale, { + style: "currency", + currency: currency, + minimumFractionDigits: scale, + maximumFractionDigits: scale, + useGrouping: true, + }).format(amount); + } +} diff --git a/apps/server/src/common/domain/value-objects/name.spec.ts b/apps/server/src/common/domain/value-objects/name.spec.ts index 846b23bb..0557f757 100644 --- a/apps/server/src/common/domain/value-objects/name.spec.ts +++ b/apps/server/src/common/domain/value-objects/name.spec.ts @@ -24,7 +24,7 @@ describe("Name Value Object", () => { const nullableNameResult = Name.createNullable("Alice"); expect(nullableNameResult.isSuccess).toBe(true); expect(nullableNameResult.data.isSome()).toBe(true); - expect(nullableNameResult.data.getValue()).toBe("Alice"); + expect(nullableNameResult.data.getOrUndefined()?.toString()).toBe("Alice"); }); test("Debe generar acrónimos correctamente", () => { diff --git a/apps/server/src/common/domain/value-objects/name.ts b/apps/server/src/common/domain/value-objects/name.ts index 1e78c8e3..8fdf0171 100644 --- a/apps/server/src/common/domain/value-objects/name.ts +++ b/apps/server/src/common/domain/value-objects/name.ts @@ -2,11 +2,11 @@ import { Maybe, Result } from "@common/helpers"; import { z } from "zod"; import { ValueObject } from "./value-object"; -interface NameProps { +interface INameProps { value: string; } -export class Name extends ValueObject { +export class Name extends ValueObject { private static readonly MAX_LENGTH = 255; protected static validate(value: string) { @@ -21,9 +21,9 @@ export class Name extends ValueObject { const valueIsValid = Name.validate(value); if (!valueIsValid.success) { - Result.fail(new Error(valueIsValid.error.errors[0].message)); + return Result.fail(new Error(valueIsValid.error.errors[0].message)); } - return Result.ok(new Name({ value: valueIsValid.data! })); + return Result.ok(new Name({ value })); } static createNullable(value?: string): Result, Error> { diff --git a/apps/server/src/common/domain/value-objects/percentage.spec.ts b/apps/server/src/common/domain/value-objects/percentage.spec.ts new file mode 100644 index 00000000..8b89ec03 --- /dev/null +++ b/apps/server/src/common/domain/value-objects/percentage.spec.ts @@ -0,0 +1,51 @@ +import { Percentage } from "./percentage"; // Ajusta la ruta según sea necesario + +describe("Percentage Value Object", () => { + test("Debe crear un porcentaje válido con escala por defecto", () => { + const result = Percentage.create({ amount: 200 }); // 2.00% + expect(result.isSuccess).toBe(true); + expect(result.data?.toString()).toBe("2.00%"); + }); + + test("Debe crear un porcentaje válido con escala definida", () => { + const result = Percentage.create({ amount: 2150, scale: 2 }); // 21.50% + expect(result.isSuccess).toBe(true); + expect(result.data?.toString()).toBe("21.50%"); + }); + + test("Debe devolver error si la cantidad supera el 100%", () => { + const result = Percentage.create({ amount: 48732000, scale: 4 }); + expect(result.isSuccess).toBe(false); + expect(result.error.message).toBe("La escala debe estar entre 0 y 2."); + }); + + test("Debe devolver error si la cantidad es negativa", () => { + const result = Percentage.create({ amount: -100, scale: 2 }); + expect(result.isSuccess).toBe(false); + expect(result.error.message).toContain("La cantidad no puede ser negativa."); + }); + + test("Debe devolver error si la escala es menor a 0", () => { + const result = Percentage.create({ amount: 100, scale: -1 }); + expect(result.isSuccess).toBe(false); + expect(result.error.message).toContain("Number must be greater than or equal to 0"); + }); + + test("Debe devolver error si la escala es mayor a 10", () => { + const result = Percentage.create({ amount: 100, scale: 11 }); + expect(result.isSuccess).toBe(false); + expect(result.error.message).toContain("La escala debe estar entre 0 y 2."); + }); + + test("Debe representar correctamente el valor como string", () => { + const result = Percentage.create({ amount: 750, scale: 2 }); // 7.50% + expect(result.isSuccess).toBe(true); + expect(result.data?.toString()).toBe("7.50%"); + }); + + test("Debe manejar correctamente el caso de 0%", () => { + const result = Percentage.create({ amount: 0, scale: 2 }); + expect(result.isSuccess).toBe(true); + expect(result.data?.toString()).toBe("0.00%"); + }); +}); diff --git a/apps/server/src/common/domain/value-objects/percentage.ts b/apps/server/src/common/domain/value-objects/percentage.ts new file mode 100644 index 00000000..94f3bb43 --- /dev/null +++ b/apps/server/src/common/domain/value-objects/percentage.ts @@ -0,0 +1,83 @@ +import { Result } from "@common/helpers"; +import { z } from "zod"; +import { ValueObject } from "./value-object"; + +const DEFAULT_SCALE = 2; + +interface IPercentageProps { + amount: number; + scale: number; +} + +interface IPercentage { + amount: number; + scale: number; + + getValue(): IPercentageProps; + toNumber(): number; + toString(): string; +} + +export class Percentage extends ValueObject implements IPercentage { + public static readonly DEFAULT_SCALE = DEFAULT_SCALE; + public static readonly MIN_VALUE = 0; + public static readonly MAX_VALUE = 100; + + public static readonly MIN_SCALE = 0; + public static readonly MAX_SCALE = 2; + + protected static validate(values: IPercentageProps) { + const schema = z.object({ + amount: z.number().int().min(Percentage.MIN_VALUE, "La cantidad no puede ser negativa."), + scale: z + .number() + .int() + .min(Percentage.MIN_SCALE) + .max( + Percentage.MAX_SCALE, + `La escala debe estar entre ${Percentage.MIN_SCALE} y ${Percentage.MAX_SCALE}.` + ), + }); + + return schema.safeParse(values); + } + + static create(props: { amount: number; scale?: number }): Result { + const { amount, scale = Percentage.DEFAULT_SCALE } = props; + + const validationResult = Percentage.validate({ amount, scale }); + if (!validationResult.success) { + return Result.fail(new Error(validationResult.error.errors.map((e) => e.message).join(", "))); + } + + // Cálculo del valor real del porcentaje + const realValue = amount / Math.pow(10, scale); + + // Validación de rango + if (realValue > Percentage.MAX_VALUE) { + return Result.fail(new Error("El porcentaje no puede ser mayor a 100%.")); + } + + return Result.ok(new Percentage({ amount, scale })); + } + + get amount(): number { + return this.props.amount; + } + + get scale(): number { + return this.props.scale; + } + + getValue(): IPercentageProps { + return this.props; + } + + toNumber(): number { + return this.amount / Math.pow(10, this.scale); + } + + toString(): string { + return `${this.toNumber().toFixed(this.scale)}%`; + } +} diff --git a/apps/server/src/common/domain/value-objects/phone-number.ts b/apps/server/src/common/domain/value-objects/phone-number.ts index 3c593fd1..ec6fa8af 100644 --- a/apps/server/src/common/domain/value-objects/phone-number.ts +++ b/apps/server/src/common/domain/value-objects/phone-number.ts @@ -13,7 +13,7 @@ export class PhoneNumber extends ValueObject { const valueIsValid = PhoneNumber.validate(value); if (!valueIsValid.success) { - Result.fail(new Error(valueIsValid.error.errors[0].message)); + return Result.fail(new Error(valueIsValid.error.errors[0].message)); } return Result.ok(new PhoneNumber({ value: valueIsValid.data! })); } diff --git a/apps/server/src/common/domain/value-objects/postal-address.ts b/apps/server/src/common/domain/value-objects/postal-address.ts index 999e5a7b..5aeba48f 100644 --- a/apps/server/src/common/domain/value-objects/postal-address.ts +++ b/apps/server/src/common/domain/value-objects/postal-address.ts @@ -52,7 +52,7 @@ export class PostalAddress extends ValueObject { const valueIsValid = PostalAddress.validate(values); if (!valueIsValid.success) { - Result.fail(new Error(valueIsValid.error.errors[0].message)); + return Result.fail(new Error(valueIsValid.error.errors[0].message)); } return Result.ok(new PostalAddress(valueIsValid.data!)); } diff --git a/apps/server/src/common/domain/value-objects/quantity.ts b/apps/server/src/common/domain/value-objects/quantity.ts new file mode 100644 index 00000000..8b6babcf --- /dev/null +++ b/apps/server/src/common/domain/value-objects/quantity.ts @@ -0,0 +1,122 @@ +import { Result } from "@common/helpers"; +import { z } from "zod"; +import { ValueObject } from "./value-object"; + +const DEFAULT_SCALE = 2; + +interface IQuantityProps { + amount: number; + scale: number; +} + +interface IQuantity { + amount: number; + scale: number; + + getValue(): IQuantityProps; + toNumber(): number; + toString(): string; + + increment(anotherQuantity?: Quantity): Result; + decrement(anotherQuantity?: Quantity): Result; + hasSameScale(otherQuantity: Quantity): boolean; + convertScale(newScale: number): Result; +} + +export class Quantity extends ValueObject implements IQuantity { + protected static validate(values: IQuantityProps) { + const schema = z.object({ + amount: z.number().int(), + scale: z.number().int().min(0), + }); + + return schema.safeParse(values); + } + + public static readonly DEFAULT_SCALE = DEFAULT_SCALE; + public static readonly MIN_SCALE = 0; + public static readonly MAX_SCALE = 2; + + static create(props: IQuantityProps) { + const checkProps = Quantity.validate(props); + + if (!checkProps.success) { + Result.fail(new Error(checkProps.error.errors[0].message)); + } + return Result.ok(new Quantity({ ...checkProps.data! })); + } + + get amount(): number { + return this.props.amount; + } + + get scale(): number { + return this.props.scale; + } + + getValue(): IQuantityProps { + return this.props; + } + + toNumber(): number { + return this.amount / Math.pow(10, this.scale); + } + + toString(): string { + return this.toNumber().toFixed(this.scale); + } + + increment(anotherQuantity?: Quantity): Result { + if (!anotherQuantity) { + return Quantity.create({ + amount: Number(this.amount) + 1, + scale: this.scale, + }); + } + + if (!this.hasSameScale(anotherQuantity)) { + return Result.fail(Error("No se pueden sumar cantidades con diferentes escalas.")); + } + + return Quantity.create({ + amount: Number(this.amount) + Number(anotherQuantity.amount), + scale: this.scale, + }); + } + + decrement(anotherQuantity?: Quantity): Result { + if (!anotherQuantity) { + return Quantity.create({ + amount: Number(this.amount) - 1, + scale: this.scale, + }); + } + + if (!this.hasSameScale(anotherQuantity)) { + return Result.fail(Error("No se pueden restar cantidades con diferentes escalas.")); + } + + return Quantity.create({ + amount: Number(this.amount) - Number(anotherQuantity.amount), + scale: this.scale, + }); + } + + hasSameScale(otherQuantity: Quantity): boolean { + return this.scale === otherQuantity.scale; + } + + convertScale(newScale: number): Result { + if (newScale < Quantity.MIN_SCALE || newScale > Quantity.MAX_SCALE) { + return Result.fail(new Error(`Scale out of range: ${newScale}`)); + } + + const oldFactor = Math.pow(10, this.scale); + const value = Number(this.amount) / oldFactor; + + const newFactor = Math.pow(10, newScale); + const newValue = Math.round(value * newFactor); + + return Quantity.create({ amount: newValue, scale: newScale }); + } +} diff --git a/apps/server/src/common/domain/value-objects/slug.spec.ts b/apps/server/src/common/domain/value-objects/slug.spec.ts index e8a5d5e9..9172ddf7 100644 --- a/apps/server/src/common/domain/value-objects/slug.spec.ts +++ b/apps/server/src/common/domain/value-objects/slug.spec.ts @@ -36,6 +36,6 @@ describe("Slug Value Object", () => { const nullableSlugResult = Slug.createNullable("my-slug"); expect(nullableSlugResult.isSuccess).toBe(true); expect(nullableSlugResult.data.isSome()).toBe(true); - expect(nullableSlugResult.data.getValue()).toBe("my-slug"); + expect(nullableSlugResult.data.getOrUndefined()?.toString()).toBe("my-slug"); }); }); diff --git a/apps/server/src/common/domain/value-objects/slug.ts b/apps/server/src/common/domain/value-objects/slug.ts index 844bea57..42d74dbb 100644 --- a/apps/server/src/common/domain/value-objects/slug.ts +++ b/apps/server/src/common/domain/value-objects/slug.ts @@ -26,7 +26,7 @@ export class Slug extends ValueObject { const valueIsValid = Slug.validate(value); if (!valueIsValid.success) { - Result.fail(new Error(valueIsValid.error.errors[0].message)); + return Result.fail(new Error(valueIsValid.error.errors[0].message)); } return Result.ok(new Slug({ value: valueIsValid.data! })); } diff --git a/apps/server/src/common/domain/value-objects/tin-number.ts b/apps/server/src/common/domain/value-objects/tin-number.ts index 4804a9d9..695fa2f6 100644 --- a/apps/server/src/common/domain/value-objects/tin-number.ts +++ b/apps/server/src/common/domain/value-objects/tin-number.ts @@ -28,7 +28,7 @@ export class TINNumber extends ValueObject { const valueIsValid = TINNumber.validate(value); if (!valueIsValid.success) { - Result.fail(new Error(valueIsValid.error.errors[0].message)); + return Result.fail(new Error(valueIsValid.error.errors[0].message)); } return Result.ok(new TINNumber({ value: valueIsValid.data! })); } diff --git a/apps/server/src/common/domain/value-objects/unique-id.spec.ts b/apps/server/src/common/domain/value-objects/unique-id.spec.ts index 96393e7f..f85f793b 100644 --- a/apps/server/src/common/domain/value-objects/unique-id.spec.ts +++ b/apps/server/src/common/domain/value-objects/unique-id.spec.ts @@ -9,7 +9,7 @@ describe("UniqueID", () => { const result = UniqueID.create(id); expect(result.isSuccess).toBe(true); - expect(result.data?.isDefined()).toBe(true); + expect(result.data.toString()).toBe(id); }); test("should fail to create UniqueID with an invalid UUID", () => { @@ -18,18 +18,17 @@ describe("UniqueID", () => { expect(result.isFailure).toBe(true); }); - test("should create an undefined UniqueID when id is undefined and generateOnEmpty is false", () => { + test("should fail when id is undefined and generateOnEmpty is false", () => { const result = UniqueID.create(undefined, false); - expect(result.isSuccess).toBe(true); - expect(result.data?.isDefined()).toBe(false); + expect(result.isFailure).toBe(true); }); test("should generate a new UUID when id is undefined and generateOnEmpty is true", () => { const result = UniqueID.create(undefined, true); expect(result.isSuccess).toBe(true); - expect(result.data?.isDefined()).toBe(true); + expect(result.data?.toString()).toBeTruthy(); }); test("should fail when id is null", () => { @@ -42,13 +41,12 @@ describe("UniqueID", () => { const result = UniqueID.create(" ", true); expect(result.isSuccess).toBe(true); - expect(result.data?.isDefined()).toBe(true); + expect(result.data?.toString()).toBeTruthy(); }); - test("should create an undefined UniqueID when id is an empty string and generateOnEmpty is false", () => { + test("should fail when id is an empty string and generateOnEmpty is false", () => { const result = UniqueID.create(" ", false); - expect(result.isSuccess).toBe(true); - expect(result.data?.isDefined()).toBe(false); + expect(result.isFailure).toBe(true); }); }); diff --git a/apps/server/src/common/domain/value-objects/unique-id.ts b/apps/server/src/common/domain/value-objects/unique-id.ts index 609f4dd8..34c24b13 100644 --- a/apps/server/src/common/domain/value-objects/unique-id.ts +++ b/apps/server/src/common/domain/value-objects/unique-id.ts @@ -9,7 +9,7 @@ export class UniqueID extends ValueObject { if (!generateOnEmpty) { return Result.fail(new Error("ID cannot be undefined or null")); } - UniqueID.generateNewID(); + return UniqueID.generateNewID(); } const result = UniqueID.validate(id!); diff --git a/apps/server/src/common/domain/value-objects/value-objects.spec.ts b/apps/server/src/common/domain/value-objects/value-objects.spec.ts index e877ef3d..6762c11a 100644 --- a/apps/server/src/common/domain/value-objects/value-objects.spec.ts +++ b/apps/server/src/common/domain/value-objects/value-objects.spec.ts @@ -1,8 +1,16 @@ import { ValueObject } from "./value-object"; -class TestValueObject extends ValueObject<{ prop: string }> { - constructor(prop: string) { - super({ prop }); +interface ITestValueProps { + value: string; +} + +class TestValueObject extends ValueObject { + constructor(value: string) { + super({ value }); + } + + getValue() { + return this.props; } } diff --git a/apps/server/src/common/helpers/maybe.spec.ts b/apps/server/src/common/helpers/maybe.spec.ts index 667e2725..b6a145a6 100644 --- a/apps/server/src/common/helpers/maybe.spec.ts +++ b/apps/server/src/common/helpers/maybe.spec.ts @@ -4,13 +4,13 @@ describe("Maybe", () => { test("debe contener un valor cuando se usa Some", () => { const maybeNumber = Maybe.Some(42); expect(maybeNumber.isSome()).toBe(true); - expect(maybeNumber.getValue()).toBe(42); + expect(maybeNumber.getOrUndefined()).toBe(42); }); test("debe estar vacío cuando se usa None", () => { const maybeEmpty = Maybe.None(); expect(maybeEmpty.isSome()).toBe(false); - expect(maybeEmpty.getValue()).toBeUndefined(); + expect(maybeEmpty.getOrUndefined()).toBeUndefined(); }); test("map debe transformar el valor si existe", () => { @@ -18,7 +18,7 @@ describe("Maybe", () => { const maybeDoubled = maybeNumber.map((n) => n * 2); expect(maybeDoubled.isSome()).toBe(true); - expect(maybeDoubled.getValue()).toBe(20); + expect(maybeDoubled.getOrUndefined()).toBe(20); }); test("map debe retornar None si el valor no existe", () => { @@ -26,6 +26,6 @@ describe("Maybe", () => { const maybeTransformed = maybeEmpty.map((n) => n * 2); expect(maybeTransformed.isSome()).toBe(false); - expect(maybeTransformed.getValue()).toBeUndefined(); + expect(maybeTransformed.getOrUndefined()).toBeUndefined(); }); }); diff --git a/apps/server/src/common/helpers/maybe.ts b/apps/server/src/common/helpers/maybe.ts index 52eb9c6e..eae6aa39 100644 --- a/apps/server/src/common/helpers/maybe.ts +++ b/apps/server/src/common/helpers/maybe.ts @@ -24,10 +24,14 @@ export class Maybe { return this.value !== undefined; } - getValue(): T | undefined { + unwrap(): T | undefined { return this.value; } + getOrUndefined(): T | undefined { + return this.unwrap(); + } + map(fn: (value: T) => U): Maybe { return this.isSome() ? Maybe.Some(fn(this.value as T)) : Maybe.None(); } diff --git a/apps/server/src/contexts/auth/domain/services/user.service.ts b/apps/server/src/contexts/auth/domain/services/user.service.ts index f3827f6f..75596501 100644 --- a/apps/server/src/contexts/auth/domain/services/user.service.ts +++ b/apps/server/src/contexts/auth/domain/services/user.service.ts @@ -1,12 +1,12 @@ import { UniqueID } from "@common/domain"; -import { Result } from "@common/helpers"; +import { Collection, Result } from "@common/helpers"; import { IUserRepository, User } from ".."; import { IUserService } from "./user-service.interface"; export class UserService implements IUserService { constructor(private readonly userRepository: IUserRepository) {} - async findUsers(transaction?: any): Promise> { + async findUsers(transaction?: any): Promise, Error>> { const usersOrError = await this.userRepository.findAll(transaction); if (usersOrError.isFailure) { return Result.fail(usersOrError.error); @@ -14,7 +14,7 @@ export class UserService implements IUserService { // Solo devolver usuarios activos const activeUsers = usersOrError.data.filter((user) => user.isActive); - return Result.ok(activeUsers); + return Result.ok(new Collection(activeUsers)); } async findUserById(userId: UniqueID, transaction?: any): Promise> { diff --git a/apps/server/src/contexts/auth/presentation/controllers/listUsers/list-users.presenter.ts b/apps/server/src/contexts/auth/presentation/controllers/listUsers/list-users.presenter.ts index ee8d20cf..38b094cc 100644 --- a/apps/server/src/contexts/auth/presentation/controllers/listUsers/list-users.presenter.ts +++ b/apps/server/src/contexts/auth/presentation/controllers/listUsers/list-users.presenter.ts @@ -1,13 +1,13 @@ -import { ensureString } from "@common/helpers"; +import { Collection, ensureString } from "@common/helpers"; import { User } from "@contexts/auth/domain"; import { IListUsersResponseDTO } from "../../dto"; export interface IListUsersPresenter { - toDTO: (users: User[]) => IListUsersResponseDTO[]; + toDTO: (users: Collection) => IListUsersResponseDTO[]; } export const listUsersPresenter: IListUsersPresenter = { - toDTO: (users: User[]): IListUsersResponseDTO[] => + toDTO: (users: Collection): IListUsersResponseDTO[] => users.map((user) => ({ id: ensureString(user.id.toString()), email: ensureString(user.email.toString()), diff --git a/apps/server/src/contexts/customer-billing/application/customer-invoices/index.ts b/apps/server/src/contexts/customer-billing/application/customer-invoices/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/apps/server/src/contexts/customer-billing/application/customers/index.ts b/apps/server/src/contexts/customer-billing/application/customers/index.ts new file mode 100644 index 00000000..1bcf6e04 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/application/customers/index.ts @@ -0,0 +1 @@ +export * from "./list-customers"; diff --git a/apps/server/src/contexts/customer-billing/application/customers/list-customers/index.ts b/apps/server/src/contexts/customer-billing/application/customers/list-customers/index.ts new file mode 100644 index 00000000..052600c9 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/application/customers/list-customers/index.ts @@ -0,0 +1 @@ +export * from "./list-customers.use-case"; diff --git a/apps/server/src/contexts/customer-billing/application/customers/list-customers/list-customers.use-case.ts b/apps/server/src/contexts/customer-billing/application/customers/list-customers/list-customers.use-case.ts new file mode 100644 index 00000000..66fec103 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/application/customers/list-customers/list-customers.use-case.ts @@ -0,0 +1,17 @@ +import { Collection, Result } from "@common/helpers"; +import { ITransactionManager } from "@common/infrastructure/database"; +import { Customer } from "@contexts/customer-billing/domain/aggregates"; +import { ICustomerService } from "@contexts/customer-billing/domain/services"; + +export class ListCustomersUseCase { + constructor( + private readonly customerService: ICustomerService, + private readonly transactionManager: ITransactionManager + ) {} + + public execute(): Promise, Error>> { + return this.transactionManager.complete((transaction) => { + return this.customerService.findCustomers(transaction); + }); + } +} diff --git a/apps/server/src/contexts/customer-billing/application/index.ts b/apps/server/src/contexts/customer-billing/application/index.ts new file mode 100644 index 00000000..e48a5e9f --- /dev/null +++ b/apps/server/src/contexts/customer-billing/application/index.ts @@ -0,0 +1,2 @@ +export * from "./customer-invoices"; +export * from "./customers"; diff --git a/apps/server/src/contexts/customer-billing/domain/aggregates/customer-invoice/customer-invoice.ts b/apps/server/src/contexts/customer-billing/domain/aggregates/customer-invoice/customer-invoice.ts new file mode 100644 index 00000000..9266cfd3 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/domain/aggregates/customer-invoice/customer-invoice.ts @@ -0,0 +1,25 @@ +import { AggregateRoot, UniqueID } from "@common/domain"; +import { Result } from "@common/helpers"; + +export interface ICustomerInvoiceProps {} + +export interface ICustomerInvoice {} + +export class CustomerInvoice + extends AggregateRoot + implements ICustomerInvoice +{ + static create(props: ICustomerInvoiceProps, id?: UniqueID): Result { + const invoice = new CustomerInvoice(props, id); + + // Reglas de negocio / validaciones + // ... + // ... + + // 🔹 Disparar evento de dominio "CustomerAuthenticatedEvent" + //const { customer } = props; + //user.addDomainEvent(new CustomerAuthenticatedEvent(id, customer.toString())); + + return Result.ok(invoice); + } +} diff --git a/apps/server/src/contexts/customer-billing/domain/aggregates/customer-invoice/index.ts b/apps/server/src/contexts/customer-billing/domain/aggregates/customer-invoice/index.ts new file mode 100644 index 00000000..8fdd6983 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/domain/aggregates/customer-invoice/index.ts @@ -0,0 +1 @@ +export * from "./customer-invoice"; diff --git a/apps/server/src/contexts/customer-billing/domain/aggregates/customer/customer.ts b/apps/server/src/contexts/customer-billing/domain/aggregates/customer/customer.ts new file mode 100644 index 00000000..805c2350 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/domain/aggregates/customer/customer.ts @@ -0,0 +1,130 @@ +import { + AggregateRoot, + EmailAddress, + PhoneNumber, + PostalAddress, + TINNumber, + UniqueID, +} from "@common/domain"; +import { Maybe, Result } from "@common/helpers"; + +export interface ICustomerProps { + reference: string; + isFreelancer: boolean; + name: string; + tin: TINNumber; + address: PostalAddress; + email: EmailAddress; + phone: PhoneNumber; + legalRecord: string; + defaultTax: number; + status: string; + langCode: string; + currencyCode: string; + + tradeName: Maybe; + website: Maybe; + fax: Maybe; +} + +export interface ICustomer { + id: UniqueID; + reference: string; + name: string; + tin: TINNumber; + address: PostalAddress; + email: EmailAddress; + phone: PhoneNumber; + legalRecord: string; + defaultTax: number; + langCode: string; + currencyCode: string; + + tradeName: Maybe; + fax: Maybe; + website: Maybe; + + isCustomer: boolean; + isFreelancer: boolean; + isActive: boolean; +} + +export class Customer extends AggregateRoot implements ICustomer { + static create(props: ICustomerProps, id?: UniqueID): Result { + const customer = new Customer(props, id); + + // Reglas de negocio / validaciones + // ... + // ... + + // 🔹 Disparar evento de dominio "CustomerAuthenticatedEvent" + //const { customer } = props; + //user.addDomainEvent(new CustomerAuthenticatedEvent(id, customer.toString())); + + return Result.ok(customer); + } + + get reference() { + return this.props.reference; + } + + get name() { + return this.props.name; + } + + get tradeName() { + return this.props.tradeName; + } + + get tin(): TINNumber { + return this.props.tin; + } + + get address(): PostalAddress { + return this.props.address; + } + + get email(): EmailAddress { + return this.props.email; + } + + get phone(): PhoneNumber { + return this.props.phone; + } + + get fax(): Maybe { + return this.props.fax; + } + + get website() { + return this.props.website; + } + + get legalRecord() { + return this.props.legalRecord; + } + + get defaultTax() { + return this.props.defaultTax; + } + + get langCode() { + return this.props.langCode; + } + + get currencyCode() { + return this.props.currencyCode; + } + + get isCustomer(): boolean { + return !this.props.isFreelancer; + } + + get isFreelancer(): boolean { + return this.props.isFreelancer; + } + + get isActive(): boolean { + return this.props.status === "active"; + } +} diff --git a/apps/server/src/contexts/customer-billing/domain/aggregates/customer/index.ts b/apps/server/src/contexts/customer-billing/domain/aggregates/customer/index.ts new file mode 100644 index 00000000..2b295031 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/domain/aggregates/customer/index.ts @@ -0,0 +1 @@ +export * from "./customer"; diff --git a/apps/server/src/contexts/customer-billing/domain/aggregates/index.ts b/apps/server/src/contexts/customer-billing/domain/aggregates/index.ts new file mode 100644 index 00000000..b3a2d541 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/domain/aggregates/index.ts @@ -0,0 +1,2 @@ +export * from "./customer"; +export * from "./customer-invoice"; diff --git a/apps/server/src/contexts/customer-billing/domain/events/customer-invoice-item.ts b/apps/server/src/contexts/customer-billing/domain/events/customer-invoice-item.ts new file mode 100644 index 00000000..753a5c29 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/domain/events/customer-invoice-item.ts @@ -0,0 +1,61 @@ +import { DomainEntity, MoneyValue, Percentage, UniqueID } from "@common/domain"; +import { Quantity } from "@common/domain/value-objects/quantity"; +import { Maybe, Result } from "@common/helpers"; + +export interface ICustomerInvoiceItemProps { + description: Maybe; // Descripción del artículo o servicio + quantity: Quantity; // Cantidad de unidades + unitPrice: MoneyValue; // Precio unitario en la moneda de la factura + // subtotalPrice: MoneyValue; // Precio unitario * Cantidad + discount: Percentage; // % descuento + // totalPrice: MoneyValue; +} + +export interface ICustomerInvoiceItem { + description: Maybe; + quantity: Quantity; + unitPrice: MoneyValue; + subtotalPrice: MoneyValue; + discount: Percentage; + totalPrice: MoneyValue; +} + +export class CustomerInvoiceItem + extends DomainEntity + implements ICustomerInvoiceItem +{ + public static create( + props: ICustomerInvoiceItemProps, + id?: UniqueID + ): Result { + return Result.ok(new CustomerInvoiceItem(props, id)); + } + + get description(): Maybe { + return this.props.description; + } + + get quantity(): Quantity { + return this.props.quantity; + } + + get unitPrice(): MoneyValue { + return this.props.unitPrice; + } + + get subtotalPrice(): MoneyValue { + return this.quantity.isNull() || this.unitPrice.isNull() + ? MoneyValue.create({ amount: null, scale: 2 }).object + : this.unitPrice.multiply(this.quantity.toNumber()); + } + + get discount(): Percentage { + return this.props.discount; + } + + get totalPrice(): MoneyValue { + return this.subtotalPrice.isNull() + ? MoneyValue.create({ amount: null, scale: 2 }).object + : this.subtotalPrice.subtract(this.subtotalPrice.percentage(this.discount.toNumber())); + } +} diff --git a/apps/server/src/contexts/customer-billing/domain/index.ts b/apps/server/src/contexts/customer-billing/domain/index.ts new file mode 100644 index 00000000..ef023faa --- /dev/null +++ b/apps/server/src/contexts/customer-billing/domain/index.ts @@ -0,0 +1,3 @@ +export * from "./aggregates"; +export * from "./repositories"; +export * from "./services"; diff --git a/apps/server/src/contexts/customer-billing/domain/repositories/customer-repository.interface.ts b/apps/server/src/contexts/customer-billing/domain/repositories/customer-repository.interface.ts new file mode 100644 index 00000000..c2189ec0 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/domain/repositories/customer-repository.interface.ts @@ -0,0 +1,9 @@ +import { EmailAddress, UniqueID } from "@common/domain"; +import { Collection, Result } from "@common/helpers"; +import { Customer } from "../aggregates"; + +export interface ICustomerRepository { + findAll(transaction?: any): Promise, Error>>; + findById(id: UniqueID, transaction?: any): Promise>; + findByEmail(email: EmailAddress, transaction?: any): Promise>; +} diff --git a/apps/server/src/contexts/customer-billing/domain/repositories/index.ts b/apps/server/src/contexts/customer-billing/domain/repositories/index.ts new file mode 100644 index 00000000..2ae98271 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/domain/repositories/index.ts @@ -0,0 +1 @@ +export * from "./customer-repository.interface"; diff --git a/apps/server/src/contexts/customer-billing/domain/services/customer-service.interface.ts b/apps/server/src/contexts/customer-billing/domain/services/customer-service.interface.ts new file mode 100644 index 00000000..a47da4f6 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/domain/services/customer-service.interface.ts @@ -0,0 +1,8 @@ +import { UniqueID } from "@common/domain"; +import { Collection, Result } from "@common/helpers"; +import { Customer } from "../aggregates"; + +export interface ICustomerService { + findCustomers(transaction?: any): Promise, Error>>; + findCustomerById(userId: UniqueID, transaction?: any): Promise>; +} diff --git a/apps/server/src/contexts/customer-billing/domain/services/customer.service.ts b/apps/server/src/contexts/customer-billing/domain/services/customer.service.ts new file mode 100644 index 00000000..6beb6bd7 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/domain/services/customer.service.ts @@ -0,0 +1,24 @@ +import { UniqueID } from "@common/domain"; +import { Collection, Result } from "@common/helpers"; +import { Customer } from "../aggregates"; +import { ICustomerRepository } from "../repositories"; +import { ICustomerService } from "./customer-service.interface"; + +export class CustomerService implements ICustomerService { + constructor(private readonly customerRepository: ICustomerRepository) {} + + async findCustomers(transaction?: any): Promise, Error>> { + const customersOrError = await this.customerRepository.findAll(transaction); + if (customersOrError.isFailure) { + return Result.fail(customersOrError.error); + } + + // Solo devolver usuarios activos + const activeCustomers = customersOrError.data.filter((customer) => customer.isActive); + return Result.ok(new Collection(activeCustomers)); + } + + async findCustomerById(customerId: UniqueID, transaction?: any): Promise> { + return await this.customerRepository.findById(customerId, transaction); + } +} diff --git a/apps/server/src/contexts/customer-billing/domain/services/index.ts b/apps/server/src/contexts/customer-billing/domain/services/index.ts new file mode 100644 index 00000000..fd8abcbb --- /dev/null +++ b/apps/server/src/contexts/customer-billing/domain/services/index.ts @@ -0,0 +1,2 @@ +export * from "./customer-service.interface"; +export * from "./customer.service"; diff --git a/apps/server/src/contexts/sales/infraestructure/index.ts b/apps/server/src/contexts/customer-billing/infraestructure/index.ts similarity index 100% rename from apps/server/src/contexts/sales/infraestructure/index.ts rename to apps/server/src/contexts/customer-billing/infraestructure/index.ts diff --git a/apps/server/src/contexts/customer-billing/infraestructure/mappers/customer.mapper.ts b/apps/server/src/contexts/customer-billing/infraestructure/mappers/customer.mapper.ts new file mode 100644 index 00000000..f94d33c9 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/infraestructure/mappers/customer.mapper.ts @@ -0,0 +1,100 @@ +import { EmailAddress, PhoneNumber, PostalAddress, TINNumber, UniqueID } from "@common/domain"; +import { Maybe, Result } from "@common/helpers"; +import { + ISequelizeMapper, + MapperParamsType, + SequelizeMapper, +} from "@common/infrastructure/sequelize/sequelize-mapper"; +import { Customer } from "@contexts/customer-billing/domain"; +import { CustomerCreationAttributes, CustomerModel } from "../sequelize/customer.model"; + +export interface ICustomerMapper + extends ISequelizeMapper {} + +export class CustomerMapper + extends SequelizeMapper + implements ICustomerMapper +{ + public mapToDomain(source: CustomerModel, params?: MapperParamsType): Result { + const idOrError = UniqueID.create(source.id); + const tinOrError = TINNumber.create(source.tin); + const emailOrError = EmailAddress.create(source.email); + const phoneOrError = PhoneNumber.create(source.phone); + const faxOrError = PhoneNumber.createNullable(source.fax); + const postalAddressOrError = PostalAddress.create({ + street: source.street, + city: source.city, + state: source.state, + postalCode: source.postal_code, + country: source.country, + }); + + const result = Result.combine([ + idOrError, + tinOrError, + emailOrError, + phoneOrError, + faxOrError, + postalAddressOrError, + ]); + + if (result.isFailure) { + return Result.fail(result.error); + } + + return Customer.create( + { + isFreelancer: source.is_freelancer, + reference: source.reference, + name: source.name, + tradeName: source.trade_name ? Maybe.Some(source.trade_name) : Maybe.None(), + tin: tinOrError.data, + address: postalAddressOrError.data, + email: emailOrError.data, + phone: phoneOrError.data, + fax: faxOrError.data, + website: source.website ? Maybe.Some(source.website) : Maybe.None(), + legalRecord: source.legal_record, + defaultTax: source.default_tax, + status: source.status, + langCode: source.lang_code, + currencyCode: source.currency_code, + }, + idOrError.data + ); + } + + public mapToPersistence( + source: Customer, + params?: MapperParamsType + ): Result { + return Result.ok({ + id: source.id.toString(), + reference: source.reference, + is_freelancer: source.isFreelancer, + name: source.name, + trade_name: source.tradeName.isSome() ? source.tradeName.getValue() : undefined, + tin: source.tin.toString(), + + street: source.address.street, + city: source.address.city, + state: source.address.state, + postal_code: source.address.postalCode, + country: source.address.country, + + email: source.email.toString(), + phone: source.phone.toString(), + fax: source.fax.isSome() ? source.fax.getValue()?.toString() : undefined, + website: source.website.isSome() ? source.website.getValue() : undefined, + + legal_record: source.legalRecord, + default_tax: source.defaultTax, + status: source.isActive ? "active" : "inactive", + lang_code: source.langCode, + currency_code: source.currencyCode, + }); + } +} + +const customerMapper: CustomerMapper = new CustomerMapper(); +export { customerMapper }; diff --git a/apps/server/src/contexts/customer-billing/infraestructure/mappers/index.ts b/apps/server/src/contexts/customer-billing/infraestructure/mappers/index.ts new file mode 100644 index 00000000..7f5fae75 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/infraestructure/mappers/index.ts @@ -0,0 +1 @@ +export * from "./customer.mapper"; diff --git a/apps/server/src/contexts/customer-billing/infraestructure/sequelize/customer-invoice-item.model.ts b/apps/server/src/contexts/customer-billing/infraestructure/sequelize/customer-invoice-item.model.ts new file mode 100644 index 00000000..91919de0 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/infraestructure/sequelize/customer-invoice-item.model.ts @@ -0,0 +1,100 @@ +import { + CreationOptional, + DataTypes, + InferAttributes, + InferCreationAttributes, + Model, + NonAttribute, + Sequelize, +} from "sequelize"; +import { CustomerInvoiceModel } from "./customer-invoice.model"; + +export type CustomerInvoiceItemCreationAttributes = InferCreationAttributes< + CustomerInvoiceItemModel, + { omit: "invoice" } +>; + +export class CustomerInvoiceItemModel extends Model< + InferAttributes, + InferCreationAttributes +> { + static associate(connection: Sequelize) { + const { CustomerInvoiceModel, CustomerInvoiceItemModel } = connection.models; + + CustomerInvoiceItemModel.belongsTo(CustomerInvoiceModel, { + as: "invoice", + foreignKey: "invoice_id", + onDelete: "CASCADE", + }); + } + + declare invoice_id: string; + declare item_id: string; + declare id_article: CreationOptional; + declare position: number; + declare description: CreationOptional; + declare quantity: CreationOptional; + declare unit_price: CreationOptional; + declare subtotal_price: CreationOptional; + declare discount: CreationOptional; + declare total_price: CreationOptional; + + declare invoice: NonAttribute; +} + +export default (sequelize: Sequelize) => { + CustomerInvoiceItemModel.init( + { + item_id: { + type: new DataTypes.UUID(), + primaryKey: true, + }, + invoice_id: { + type: new DataTypes.UUID(), + primaryKey: true, + }, + id_article: { + type: DataTypes.BIGINT().UNSIGNED, + allowNull: true, + }, + position: { + type: new DataTypes.MEDIUMINT(), + autoIncrement: false, + allowNull: false, + }, + description: { + type: new DataTypes.TEXT(), + allowNull: true, + }, + quantity: { + type: DataTypes.BIGINT(), + allowNull: true, + }, + unit_price: { + type: new DataTypes.BIGINT(), + allowNull: true, + }, + subtotal_price: { + type: new DataTypes.BIGINT(), + allowNull: true, + }, + discount: { + type: new DataTypes.SMALLINT(), + allowNull: true, + }, + total_price: { + type: new DataTypes.BIGINT(), + allowNull: true, + }, + }, + { + sequelize, + tableName: "invoice_items", + timestamps: false, + + indexes: [], + } + ); + + return CustomerInvoiceItemModel; +}; diff --git a/apps/server/src/contexts/customer-billing/infraestructure/sequelize/customer-invoice.model.ts b/apps/server/src/contexts/customer-billing/infraestructure/sequelize/customer-invoice.model.ts new file mode 100644 index 00000000..7e0f7f2a --- /dev/null +++ b/apps/server/src/contexts/customer-billing/infraestructure/sequelize/customer-invoice.model.ts @@ -0,0 +1,251 @@ +import { + CreationOptional, + DataTypes, + InferAttributes, + InferCreationAttributes, + Model, + NonAttribute, + Sequelize, +} from "sequelize"; +import { CustomerInvoiceItemModel } from "./customer-invoice-item.model"; +import { CustomerModel } from "./customer.model"; + +export type CustomerInvoiceCreationAttributes = InferCreationAttributes< + CustomerInvoiceModel, + { omit: "items" | "customer" } +> & { + //items: CustomerInvoiceItemCreationAttributes[]; + customer_id: string; +}; + +export class CustomerInvoiceModel extends Model< + InferAttributes, + InferCreationAttributes +> { + // To avoid table creation + /*static async sync(): Promise { + return Promise.resolve(); + }*/ + + static associate(connection: Sequelize) { + const { CustomerInvoiceModel, CustomerInvoiceItemModel, CustomerModel } = connection.models; + + CustomerInvoiceModel.hasMany(CustomerInvoiceItemModel, { + as: "items", + foreignKey: "quote_id", + onDelete: "CASCADE", + }); + + CustomerInvoiceModel.belongsTo(CustomerModel, { + foreignKey: "dealer_id", + as: "dealer", + onDelete: "RESTRICT", + }); + } + + declare id: string; + declare status: string; + + declare issue_date: string; + declare invoice_number: string; + declare invoide_type: string; + + declare lang_code: string; + declare currency_code: string; + + declare customer_id: string; + declare customer_tin: string; + declare customer_name: string; + + declare customer_reference: CreationOptional; + + declare customer_street: string; + declare customer_city: string; + declare customer_state: string; + declare customer_postal_code: string; + declare customer_country: string; + + declare subtotal_price: CreationOptional; + + declare discount: CreationOptional; + declare discount_price: CreationOptional; + + declare before_tax_price: CreationOptional; + + declare tax: CreationOptional; + declare tax_price: CreationOptional; + + declare total_price: CreationOptional; + + declare notes: CreationOptional; + + declare items: NonAttribute; + declare customer: NonAttribute; + + declare integrity_hash: CreationOptional; + declare previous_invoice_id: CreationOptional; + declare signed_at: CreationOptional; +} + +export default (sequelize: Sequelize) => { + CustomerInvoiceModel.init( + { + id: { + type: DataTypes.UUID, + primaryKey: true, + }, + + status: { + type: new DataTypes.STRING(), + allowNull: false, + }, + + issue_date: { + type: new DataTypes.DATEONLY(), + allowNull: false, + }, + + invoice_number: { + type: DataTypes.STRING(), + allowNull: false, + }, + + invoide_type: { + type: new DataTypes.STRING(), + allowNull: false, + }, + + lang_code: { + type: DataTypes.STRING(2), + allowNull: false, + defaultValue: "es", + }, + + currency_code: { + type: new DataTypes.STRING(3), + allowNull: false, + defaultValue: "EUR", + }, + + customer_id: { + type: new DataTypes.UUID(), + primaryKey: true, + }, + + customer_name: { + type: DataTypes.STRING, + allowNull: false, + }, + + customer_tin: { + type: DataTypes.STRING, + allowNull: false, + }, + + customer_reference: { + type: new DataTypes.STRING(), + }, + + customer_street: { + type: DataTypes.STRING, + allowNull: false, + }, + customer_city: { + type: DataTypes.STRING, + allowNull: false, + }, + customer_state: { + type: DataTypes.STRING, + allowNull: false, + }, + customer_postal_code: { + type: DataTypes.STRING, + allowNull: false, + }, + customer_country: { + type: DataTypes.STRING, + allowNull: false, + }, + + subtotal_price: { + type: new DataTypes.BIGINT(), + allowNull: true, + }, + + discount: { + type: new DataTypes.SMALLINT(), + allowNull: true, + }, + + discount_price: { + type: new DataTypes.BIGINT(), + allowNull: true, + }, + + before_tax_price: { + type: new DataTypes.BIGINT(), + allowNull: true, + }, + + tax: { + type: new DataTypes.SMALLINT(), + allowNull: true, + }, + + tax_price: { + type: new DataTypes.BIGINT(), + allowNull: true, + }, + + total_price: { + type: new DataTypes.BIGINT(), + allowNull: true, + }, + + notes: { + type: DataTypes.TEXT, + }, + + integrity_hash: { + type: DataTypes.STRING, + allowNull: false, + comment: "Hash criptográfico para asegurar integridad", + }, + previous_invoice_id: { + type: DataTypes.UUID, + allowNull: true, + comment: "Referencia a la factura anterior (si aplica)", + }, + signed_at: { + type: DataTypes.DATE, + allowNull: false, + comment: "Fecha en que la factura fue firmada digitalmente", + }, + }, + { + sequelize, + tableName: "customer_invoices", + + paranoid: true, // softs deletes + timestamps: true, + + createdAt: "created_at", + updatedAt: "updated_at", + deletedAt: "deleted_at", + + indexes: [ + { name: "status_idx", fields: ["status"] }, + { name: "reference_idx", fields: ["reference"] }, + { name: "deleted_at_idx", fields: ["deleted_at"] }, + { name: "signed_at_idx", fields: ["signed_at"] }, + ], + + whereMergeStrategy: "and", // <- cómo tratar el merge de un scope + + defaultScope: {}, + + scopes: {}, + } + ); + return CustomerInvoiceModel; +}; diff --git a/apps/server/src/contexts/customer-billing/infraestructure/sequelize/customer.model.ts b/apps/server/src/contexts/customer-billing/infraestructure/sequelize/customer.model.ts new file mode 100644 index 00000000..bc63a03b --- /dev/null +++ b/apps/server/src/contexts/customer-billing/infraestructure/sequelize/customer.model.ts @@ -0,0 +1,172 @@ +import { + CreationOptional, + DataTypes, + InferAttributes, + InferCreationAttributes, + Model, + Sequelize, +} from "sequelize"; + +export type CustomerCreationAttributes = InferCreationAttributes & {}; + +export class CustomerModel extends Model< + InferAttributes, + InferCreationAttributes +> { + // To avoid table creation + /*static async sync(): Promise { + return Promise.resolve(); + }*/ + + declare id: string; + declare reference: CreationOptional; + + declare is_freelancer: boolean; + declare name: string; + declare trade_name: CreationOptional; + declare tin: string; + + declare street: string; + declare city: string; + declare state: string; + declare postal_code: string; + declare country: string; + + declare email: string; + declare phone: string; + declare fax: CreationOptional; + declare website: CreationOptional; + + declare legal_record: string; + + declare default_tax: number; + declare status: string; + declare lang_code: string; + declare currency_code: string; +} + +export default (sequelize: Sequelize) => { + CustomerModel.init( + { + id: { + type: DataTypes.UUID, + primaryKey: true, + }, + reference: { + type: DataTypes.STRING, + allowNull: false, + }, + is_freelancer: { + type: DataTypes.BOOLEAN, + allowNull: false, + }, + name: { + type: DataTypes.STRING, + allowNull: false, + }, + trade_name: { + type: DataTypes.STRING, + allowNull: true, + }, + tin: { + type: DataTypes.STRING, + allowNull: false, + }, + + street: { + type: DataTypes.STRING, + allowNull: false, + }, + city: { + type: DataTypes.STRING, + allowNull: false, + }, + state: { + type: DataTypes.STRING, + allowNull: false, + }, + postal_code: { + type: DataTypes.STRING, + allowNull: false, + }, + country: { + type: DataTypes.STRING, + allowNull: false, + }, + + email: { + type: DataTypes.STRING, + allowNull: false, + validate: { + isEmail: true, + }, + }, + phone: { + type: DataTypes.STRING, + allowNull: false, + }, + fax: { + type: DataTypes.STRING, + allowNull: true, + }, + website: { + type: DataTypes.STRING, + allowNull: true, + validate: { + isUrl: true, + }, + }, + legal_record: { + type: DataTypes.TEXT, + allowNull: false, + }, + + default_tax: { + type: new DataTypes.SMALLINT(), + allowNull: false, + defaultValue: 2100, + }, + + lang_code: { + type: DataTypes.STRING(2), + allowNull: false, + defaultValue: "es", + }, + + currency_code: { + type: new DataTypes.STRING(3), + allowNull: false, + defaultValue: "EUR", + }, + + status: { + type: DataTypes.STRING, + allowNull: false, + defaultValue: "active", + }, + }, + { + sequelize, + tableName: "customers", + + paranoid: true, // softs deletes + timestamps: true, + + createdAt: "created_at", + updatedAt: "updated_at", + deletedAt: "deleted_at", + + indexes: [ + { name: "email_idx", fields: ["email"], unique: true }, + { name: "reference_idx", fields: ["reference"], unique: true }, + ], + + whereMergeStrategy: "and", // <- cómo tratar el merge de un scope + + defaultScope: {}, + + scopes: {}, + } + ); + return CustomerModel; +}; diff --git a/apps/server/src/contexts/customer-billing/infraestructure/sequelize/customer.repository.ts b/apps/server/src/contexts/customer-billing/infraestructure/sequelize/customer.repository.ts new file mode 100644 index 00000000..db511f73 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/infraestructure/sequelize/customer.repository.ts @@ -0,0 +1,81 @@ +import { EmailAddress, UniqueID } from "@common/domain"; +import { Collection, Result } from "@common/helpers"; +import { SequelizeRepository } from "@common/infrastructure"; +import { Customer, ICustomerRepository } from "@contexts/customer-billing/domain"; +import { Transaction } from "sequelize"; +import { customerMapper, ICustomerMapper } from "../mappers"; +import { CustomerModel } from "./customer.model"; + +class CustomerRepository extends SequelizeRepository implements ICustomerRepository { + private readonly _mapper!: ICustomerMapper; + + /** + * 🔹 Función personalizada para mapear errores de unicidad en autenticación + */ + private _customErrorMapper(error: Error): string | null { + if (error.name === "SequelizeUniqueConstraintError") { + return "Customer with this email already exists"; + } + + return null; + } + + constructor(mapper: ICustomerMapper) { + super(); + this._mapper = mapper; + } + + async findAll(transaction?: Transaction): Promise, Error>> { + try { + const rawCustomers: any = await this._findAll(CustomerModel, {}, transaction); + + if (!rawCustomers === true) { + return Result.fail(new Error("Customer with email not exists")); + } + + return this._mapper.mapArrayToDomain(rawCustomers); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } + + async findById(id: UniqueID, transaction?: Transaction): Promise> { + try { + const rawCustomer: any = await this._getById(CustomerModel, id, {}, transaction); + + if (!rawCustomer === true) { + return Result.fail(new Error(`Customer with id ${id.toString()} not exists`)); + } + + return this._mapper.mapToDomain(rawCustomer); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } + + async findByEmail( + email: EmailAddress, + transaction?: Transaction + ): Promise> { + try { + const rawCustomer: any = await this._getBy( + CustomerModel, + "email", + email.toString(), + {}, + transaction + ); + + if (!rawCustomer === true) { + return Result.fail(new Error(`Customer with email ${email.toString()} not exists`)); + } + + return this._mapper.mapToDomain(rawCustomer); + } catch (error: any) { + return this._handleDatabaseError(error, this._customErrorMapper); + } + } +} + +const customerRepository = new CustomerRepository(customerMapper); +export { customerRepository }; diff --git a/apps/server/src/contexts/customer-billing/infraestructure/sequelize/index.ts b/apps/server/src/contexts/customer-billing/infraestructure/sequelize/index.ts new file mode 100644 index 00000000..38e74d82 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/infraestructure/sequelize/index.ts @@ -0,0 +1,9 @@ +import { ICustomerRepository } from "@contexts/customer-billing/domain"; +import { customerRepository } from "./customer.repository"; + +export * from "./customer.model"; +export * from "./customer.repository"; + +export const createCustomerRepository = (): ICustomerRepository => { + return customerRepository; +}; diff --git a/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/list/index.ts b/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/list/index.ts new file mode 100644 index 00000000..1758ef67 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/list/index.ts @@ -0,0 +1,2 @@ +export * from "./list-customer-invoices.controller"; +export * from "./list-customer-invoices.presenter"; diff --git a/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/list/list-customer-invoices.controller.ts b/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/list/list-customer-invoices.controller.ts new file mode 100644 index 00000000..f45280f1 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/list/list-customer-invoices.controller.ts @@ -0,0 +1,37 @@ +import { ExpressController } from "@common/presentation"; +import { ListCustomerInvoicesUseCase } from "@contexts/customer-billing/application"; +import { IListCustomerInvoicesPresenter } from "./list-customer-invoices.presenter"; + +export class ListCustomerInvoicesController extends ExpressController { + public constructor( + private readonly listCustomerInvoices: ListCustomerInvoicesUseCase, + private readonly presenter: IListCustomerInvoicesPresenter + ) { + super(); + } + + protected async executeImpl() { + const customersOrError = await this.listCustomerInvoices.execute(); + + if (customersOrError.isFailure) { + return this.handleError(customersOrError.error); + } + + return this.ok(this.presenter.toDTO(customersOrError.data)); + } + + private handleError(error: Error) { + const message = error.message; + + if ( + message.includes("Database connection lost") || + message.includes("Database request timed out") + ) { + return this.unavailableError( + "Database service is currently unavailable. Please try again later." + ); + } + + return this.conflictError(message); + } +} diff --git a/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/list/list-customer-invoices.presenter.ts b/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/list/list-customer-invoices.presenter.ts new file mode 100644 index 00000000..fa95e929 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/presentation/controllers/customer-invoices/list/list-customer-invoices.presenter.ts @@ -0,0 +1,38 @@ +import { Collection, ensureBoolean, ensureNumber, ensureString } from "@common/helpers"; + +import { IListCustomerInvoicesResponseDTO } from "../../../dto"; + +export interface IListCustomerInvoicesPresenter { + toDTO: (customers: Collection) => IListCustomerInvoicesResponseDTO[]; +} + +export const listCustomerInvoicesPresenter: IListCustomerInvoicesPresenter = { + toDTO: (customers: Collection): IListCustomerInvoicesResponseDTO[] => + customers.map((customer) => ({ + id: ensureString(customer.id.toString()), + reference: ensureString(customer.reference), + + is_freelancer: ensureBoolean(customer.isFreelancer), + name: ensureString(customer.name), + trade_name: ensureString(customer.tradeName.getValue()), + tin: ensureString(customer.tin.toString()), + + street: ensureString(customer.address.street), + city: ensureString(customer.address.city), + state: ensureString(customer.address.state), + postal_code: ensureString(customer.address.postalCode), + country: ensureString(customer.address.country), + + email: ensureString(customer.email.toString()), + phone: ensureString(customer.phone.toString()), + fax: ensureString(customer.fax.getValue()?.toString()), + website: ensureString(customer.website.getValue()), + + legal_record: ensureString(customer.legalRecord), + + default_tax: ensureNumber(customer.defaultTax), + status: ensureString(customer.isActive ? "active" : "inactive"), + lang_code: ensureString(customer.langCode), + currency_code: ensureString(customer.currencyCode), + })), +}; diff --git a/apps/server/src/contexts/customer-billing/presentation/controllers/customers/index.ts b/apps/server/src/contexts/customer-billing/presentation/controllers/customers/index.ts new file mode 100644 index 00000000..491ccf0c --- /dev/null +++ b/apps/server/src/contexts/customer-billing/presentation/controllers/customers/index.ts @@ -0,0 +1 @@ +export * from "./list"; diff --git a/apps/server/src/contexts/customer-billing/presentation/controllers/customers/list/index.ts b/apps/server/src/contexts/customer-billing/presentation/controllers/customers/list/index.ts new file mode 100644 index 00000000..33c7e19f --- /dev/null +++ b/apps/server/src/contexts/customer-billing/presentation/controllers/customers/list/index.ts @@ -0,0 +1,16 @@ +import { SequelizeTransactionManager } from "@common/infrastructure"; +import { ListCustomersUseCase } from "@contexts/customer-billing/application/customers/list-customers"; +import { CustomerService } from "@contexts/customer-billing/domain"; +import { customerRepository } from "@contexts/customer-billing/infraestructure"; +import { ListCustomersController } from "./list-customers.controller"; +import { listCustomersPresenter } from "./list-customers.presenter"; + +export const listCustomersController = () => { + const transactionManager = new SequelizeTransactionManager(); + const customerService = new CustomerService(customerRepository); + + const useCase = new ListCustomersUseCase(customerService, transactionManager); + const presenter = listCustomersPresenter; + + return new ListCustomersController(useCase, presenter); +}; diff --git a/apps/server/src/contexts/customer-billing/presentation/controllers/customers/list/list-customers.controller.ts b/apps/server/src/contexts/customer-billing/presentation/controllers/customers/list/list-customers.controller.ts new file mode 100644 index 00000000..8dc424fd --- /dev/null +++ b/apps/server/src/contexts/customer-billing/presentation/controllers/customers/list/list-customers.controller.ts @@ -0,0 +1,37 @@ +import { ExpressController } from "@common/presentation"; +import { ListCustomersUseCase } from "@contexts/customer-billing/application"; +import { IListCustomersPresenter } from "./list-customers.presenter"; + +export class ListCustomersController extends ExpressController { + public constructor( + private readonly listCustomers: ListCustomersUseCase, + private readonly presenter: IListCustomersPresenter + ) { + super(); + } + + protected async executeImpl() { + const customersOrError = await this.listCustomers.execute(); + + if (customersOrError.isFailure) { + return this.handleError(customersOrError.error); + } + + return this.ok(this.presenter.toDTO(customersOrError.data)); + } + + private handleError(error: Error) { + const message = error.message; + + if ( + message.includes("Database connection lost") || + message.includes("Database request timed out") + ) { + return this.unavailableError( + "Database service is currently unavailable. Please try again later." + ); + } + + return this.conflictError(message); + } +} diff --git a/apps/server/src/contexts/customer-billing/presentation/controllers/customers/list/list-customers.presenter.ts b/apps/server/src/contexts/customer-billing/presentation/controllers/customers/list/list-customers.presenter.ts new file mode 100644 index 00000000..dc3e7934 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/presentation/controllers/customers/list/list-customers.presenter.ts @@ -0,0 +1,38 @@ +import { Collection, ensureBoolean, ensureNumber, ensureString } from "@common/helpers"; +import { Customer } from "@contexts/customer-billing/domain"; +import { IListCustomersResponseDTO } from "../../../dto"; + +export interface IListCustomersPresenter { + toDTO: (customers: Collection) => IListCustomersResponseDTO[]; +} + +export const listCustomersPresenter: IListCustomersPresenter = { + toDTO: (customers: Collection): IListCustomersResponseDTO[] => + customers.map((customer) => ({ + id: ensureString(customer.id.toString()), + reference: ensureString(customer.reference), + + is_freelancer: ensureBoolean(customer.isFreelancer), + name: ensureString(customer.name), + trade_name: ensureString(customer.tradeName.getValue()), + tin: ensureString(customer.tin.toString()), + + street: ensureString(customer.address.street), + city: ensureString(customer.address.city), + state: ensureString(customer.address.state), + postal_code: ensureString(customer.address.postalCode), + country: ensureString(customer.address.country), + + email: ensureString(customer.email.toString()), + phone: ensureString(customer.phone.toString()), + fax: ensureString(customer.fax.getValue()?.toString()), + website: ensureString(customer.website.getValue()), + + legal_record: ensureString(customer.legalRecord), + + default_tax: ensureNumber(customer.defaultTax), + status: ensureString(customer.isActive ? "active" : "inactive"), + lang_code: ensureString(customer.langCode), + currency_code: ensureString(customer.currencyCode), + })), +}; diff --git a/apps/server/src/contexts/customer-billing/presentation/controllers/index.ts b/apps/server/src/contexts/customer-billing/presentation/controllers/index.ts new file mode 100644 index 00000000..e48a5e9f --- /dev/null +++ b/apps/server/src/contexts/customer-billing/presentation/controllers/index.ts @@ -0,0 +1,2 @@ +export * from "./customer-invoices"; +export * from "./customers"; diff --git a/apps/server/src/contexts/customer-billing/presentation/dto/customer-invoices/customer-invoices.request.dto.ts b/apps/server/src/contexts/customer-billing/presentation/dto/customer-invoices/customer-invoices.request.dto.ts new file mode 100644 index 00000000..a5c9a563 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/presentation/dto/customer-invoices/customer-invoices.request.dto.ts @@ -0,0 +1 @@ +export interface IListCustomerInvoicesRequestDTO {} diff --git a/apps/server/src/contexts/customer-billing/presentation/dto/customer-invoices/customer-invoices.response.dto.ts b/apps/server/src/contexts/customer-billing/presentation/dto/customer-invoices/customer-invoices.response.dto.ts new file mode 100644 index 00000000..c19599c1 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/presentation/dto/customer-invoices/customer-invoices.response.dto.ts @@ -0,0 +1,27 @@ +export interface IListCustomerInvoicesResponseDTO { + id: string; + reference: string; + + is_freelancer: boolean; + name: string; + trade_name: string; + tin: string; + + street: string; + city: string; + state: string; + postal_code: string; + country: string; + + email: string; + phone: string; + fax: string; + website: string; + + legal_record: string; + + default_tax: number; + status: string; + lang_code: string; + currency_code: string; +} diff --git a/apps/server/src/contexts/customer-billing/presentation/dto/customer-invoices/customer-invoices.validation.dto.ts b/apps/server/src/contexts/customer-billing/presentation/dto/customer-invoices/customer-invoices.validation.dto.ts new file mode 100644 index 00000000..a4bf02d4 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/presentation/dto/customer-invoices/customer-invoices.validation.dto.ts @@ -0,0 +1,3 @@ +import { z } from "zod"; + +export const ListCustomerInvoicesSchema = z.object({}); diff --git a/apps/server/src/contexts/customer-billing/presentation/dto/customer-invoices/index.ts b/apps/server/src/contexts/customer-billing/presentation/dto/customer-invoices/index.ts new file mode 100644 index 00000000..08586dd7 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/presentation/dto/customer-invoices/index.ts @@ -0,0 +1,3 @@ +export * from "./customer-invoices.request.dto"; +export * from "./customer-invoices.response.dto"; +export * from "./customer-invoices.validation.dto"; diff --git a/apps/server/src/contexts/customer-billing/presentation/dto/customers.request.dto.ts b/apps/server/src/contexts/customer-billing/presentation/dto/customers.request.dto.ts new file mode 100644 index 00000000..06640216 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/presentation/dto/customers.request.dto.ts @@ -0,0 +1 @@ +export interface IListCustomersRequestDTO {} diff --git a/apps/server/src/contexts/customer-billing/presentation/dto/customers.response.dto.ts b/apps/server/src/contexts/customer-billing/presentation/dto/customers.response.dto.ts new file mode 100644 index 00000000..4ba91903 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/presentation/dto/customers.response.dto.ts @@ -0,0 +1,27 @@ +export interface IListCustomersResponseDTO { + id: string; + reference: string; + + is_freelancer: boolean; + name: string; + trade_name: string; + tin: string; + + street: string; + city: string; + state: string; + postal_code: string; + country: string; + + email: string; + phone: string; + fax: string; + website: string; + + legal_record: string; + + default_tax: number; + status: string; + lang_code: string; + currency_code: string; +} diff --git a/apps/server/src/contexts/customer-billing/presentation/dto/customers.validation.dto.ts b/apps/server/src/contexts/customer-billing/presentation/dto/customers.validation.dto.ts new file mode 100644 index 00000000..ba8d4eec --- /dev/null +++ b/apps/server/src/contexts/customer-billing/presentation/dto/customers.validation.dto.ts @@ -0,0 +1,3 @@ +import { z } from "zod"; + +export const ListCustomersSchema = z.object({}); diff --git a/apps/server/src/contexts/customer-billing/presentation/dto/index.ts b/apps/server/src/contexts/customer-billing/presentation/dto/index.ts new file mode 100644 index 00000000..9ae48163 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/presentation/dto/index.ts @@ -0,0 +1,4 @@ +export * from "./customer-invoices"; +export * from "./customers.request.dto"; +export * from "./customers.response.dto"; +export * from "./customers.validation.dto"; diff --git a/apps/server/src/contexts/customer-billing/presentation/index.ts b/apps/server/src/contexts/customer-billing/presentation/index.ts new file mode 100644 index 00000000..77c53837 --- /dev/null +++ b/apps/server/src/contexts/customer-billing/presentation/index.ts @@ -0,0 +1,2 @@ +export * from "./controllers/customers"; +export * from "./dto"; diff --git a/apps/server/src/contexts/sales/domain/index.ts b/apps/server/src/contexts/sales/domain/index.ts deleted file mode 100644 index 4a16e729..00000000 --- a/apps/server/src/contexts/sales/domain/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./aggregates"; -export * from "./entities"; -export * from "./events"; -export * from "./repositories"; -export * from "./value-objects"; diff --git a/apps/server/src/contexts/sales/presentation/index.ts b/apps/server/src/contexts/sales/presentation/index.ts deleted file mode 100644 index a123289d..00000000 --- a/apps/server/src/contexts/sales/presentation/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./controllers"; -export * from "./dto"; diff --git a/apps/server/src/routes/auth.routes.ts b/apps/server/src/routes/auth.routes.ts index cc417fb8..fbb50677 100644 --- a/apps/server/src/routes/auth.routes.ts +++ b/apps/server/src/routes/auth.routes.ts @@ -14,7 +14,7 @@ import { import { NextFunction, Request, Response, Router } from "express"; export const authRouter = (appRouter: Router) => { - const authRoutes: Router = Router({ mergeParams: true }); + const routes: Router = Router({ mergeParams: true }); /** * @api {post} /api/auth/register Register a new user * @apiName RegisterUser @@ -29,7 +29,7 @@ export const authRouter = (appRouter: Router) => { * * @apiError (400) {String} message Error message. */ - authRoutes.post("/register", validateRequestDTO(RegisterUserSchema), (req, res, next) => { + routes.post("/register", validateRequestDTO(RegisterUserSchema), (req, res, next) => { registerController().execute(req, res, next); }); @@ -48,7 +48,7 @@ export const authRouter = (appRouter: Router) => { * * @apiError (401) {String} message Invalid email or password. */ - authRoutes.post( + routes.post( "/login", validateRequestDTO(LoginUserSchema), checkTabContext, @@ -68,7 +68,7 @@ export const authRouter = (appRouter: Router) => { * * @apiSuccess (200) {String} message Success message. */ - authRoutes.post( + routes.post( "/logout", checkTabContext, checkUser, @@ -77,7 +77,7 @@ export const authRouter = (appRouter: Router) => { } ); - authRoutes.post( + routes.post( "/refresh", validateRequestDTO(RefreshTokenSchema), checkTabContext, @@ -86,5 +86,5 @@ export const authRouter = (appRouter: Router) => { } ); - appRouter.use("/auth", authRoutes); + appRouter.use("/auth", routes); }; diff --git a/apps/server/src/routes/companies.routes.ts b/apps/server/src/routes/companies.routes.ts index a31fd194..6d8149e0 100644 --- a/apps/server/src/routes/companies.routes.ts +++ b/apps/server/src/routes/companies.routes.ts @@ -4,9 +4,9 @@ import { listCompaniesController, ListCompaniesSchema } from "@contexts/companie import { NextFunction, Request, Response, Router } from "express"; export const companiesRouter = (appRouter: Router) => { - const companiesRoutes: Router = Router({ mergeParams: true }); + const routes: Router = Router({ mergeParams: true }); - companiesRoutes.get( + routes.get( "/", validateRequestDTO(ListCompaniesSchema), checkTabContext, @@ -16,5 +16,5 @@ export const companiesRouter = (appRouter: Router) => { } ); - appRouter.use("/companies", companiesRoutes); + appRouter.use("/companies", routes); }; diff --git a/apps/server/src/routes/customer-invoices.routes.ts b/apps/server/src/routes/customer-invoices.routes.ts new file mode 100644 index 00000000..3690e005 --- /dev/null +++ b/apps/server/src/routes/customer-invoices.routes.ts @@ -0,0 +1,20 @@ +import { validateRequestDTO } from "@common/presentation"; +import { checkTabContext, checkUser } from "@contexts/auth/infraestructure"; +import { ListCustomerInvoicesSchema } from "@contexts/customer-billing/presentation"; +import { NextFunction, Request, Response, Router } from "express"; + +export const customerInvoicesRouter = (appRouter: Router) => { + const routes: Router = Router({ mergeParams: true }); + + routes.get( + "/", + validateRequestDTO(ListCustomerInvoicesSchema), + checkTabContext, + checkUser, + (req: Request, res: Response, next: NextFunction) => { + listCustomerInvoicesController().execute(req, res, next); + } + ); + + appRouter.use("/customer-invoices", routes); +}; diff --git a/apps/server/src/routes/customers.routes.ts b/apps/server/src/routes/customers.routes.ts index ab2b0561..6407c543 100644 --- a/apps/server/src/routes/customers.routes.ts +++ b/apps/server/src/routes/customers.routes.ts @@ -1,20 +1,23 @@ import { validateRequestDTO } from "@common/presentation"; import { checkTabContext } from "@contexts/auth/infraestructure"; -import { listUsersController, ListUsersSchema } from "@contexts/auth/presentation"; +import { + listCustomersController, + ListCustomersSchema, +} from "@contexts/customer-billing/presentation"; import { NextFunction, Request, Response, Router } from "express"; -export const userRouter = (appRouter: Router) => { - const authRoutes: Router = Router({ mergeParams: true }); +export const customersRouter = (appRouter: Router) => { + const routes: Router = Router({ mergeParams: true }); - authRoutes.get( + routes.get( "/", - validateRequestDTO(ListUsersSchema), + validateRequestDTO(ListCustomersSchema), checkTabContext, //checkUserIsAdmin, (req: Request, res: Response, next: NextFunction) => { - listUsersController().execute(req, res, next); + listCustomersController().execute(req, res, next); } ); - appRouter.use("/customers", authRoutes); + appRouter.use("/customers", routes); }; diff --git a/apps/server/src/routes/users.routes.ts b/apps/server/src/routes/users.routes.ts index 95795e50..ecdce4bf 100644 --- a/apps/server/src/routes/users.routes.ts +++ b/apps/server/src/routes/users.routes.ts @@ -4,9 +4,9 @@ import { listUsersController, ListUsersSchema } from "@contexts/auth/presentatio import { NextFunction, Request, Response, Router } from "express"; export const usersRouter = (appRouter: Router) => { - const usersRoutes: Router = Router({ mergeParams: true }); + const routes: Router = Router({ mergeParams: true }); - usersRoutes.get( + routes.get( "/", validateRequestDTO(ListUsersSchema), checkTabContext, @@ -16,5 +16,5 @@ export const usersRouter = (appRouter: Router) => { } ); - appRouter.use("/users", usersRoutes); + appRouter.use("/users", routes); }; diff --git a/apps/server/src/routes/v1.routes.ts b/apps/server/src/routes/v1.routes.ts index 0cded0fd..2677ebc6 100644 --- a/apps/server/src/routes/v1.routes.ts +++ b/apps/server/src/routes/v1.routes.ts @@ -1,6 +1,7 @@ import { Router } from "express"; import { authRouter } from "./auth.routes"; import { companiesRouter } from "./companies.routes"; +import { customersRouter } from "./customers.routes"; import { usersRouter } from "./users.routes"; export const v1Routes = () => { @@ -14,5 +15,8 @@ export const v1Routes = () => { usersRouter(routes); companiesRouter(routes); + // Sales + customersRouter(routes); + return routes; }; diff --git a/doc/DsRegistroVeriFactu.xlsx b/doc/DsRegistroVeriFactu.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..b1a2e453ca4f5bb92b5fd10b339bf2569832c5db GIT binary patch literal 59877 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Π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

nOP?t18}S7OJB?s7Of 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 literal 0 HcmV?d00001 diff --git a/packages/rdx-ddd/package.json b/packages/rdx-ddd/package.json new file mode 100644 index 00000000..83f346c1 --- /dev/null +++ b/packages/rdx-ddd/package.json @@ -0,0 +1,12 @@ +{ + "name": "rdx-ddd", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/packages/rdx-verifactu/.eslintrc.json b/packages/rdx-verifactu/.eslintrc.json new file mode 100644 index 00000000..f4e1b0c2 --- /dev/null +++ b/packages/rdx-verifactu/.eslintrc.json @@ -0,0 +1,18 @@ +{ + "extends": ["eslint-config-codely/typescript"], + "overrides": [ + { + "files": ["*.ts"], + "parserOptions": { + "project": [ + "./tsconfig.json", + "./packages/criteria/tsconfig.json", + "./packages/criteria-mysql/tsconfig.json" + ] + }, + "rules": { + "@typescript-eslint/no-floating-promises": ["off"] + } + } + ] +} diff --git a/packages/rdx-verifactu/.gitignore b/packages/rdx-verifactu/.gitignore new file mode 100644 index 00000000..76add878 --- /dev/null +++ b/packages/rdx-verifactu/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist \ No newline at end of file diff --git a/packages/rdx-verifactu/package.json b/packages/rdx-verifactu/package.json new file mode 100644 index 00000000..89f23c44 --- /dev/null +++ b/packages/rdx-verifactu/package.json @@ -0,0 +1,12 @@ +{ + "name": "rdx-verifactu", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} diff --git a/packages/rdx-verifactu/tsconfig.json b/packages/rdx-verifactu/tsconfig.json new file mode 100644 index 00000000..5f65a300 --- /dev/null +++ b/packages/rdx-verifactu/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "allowJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "noEmit": false, + "skipLibCheck": true, + "strict": true, + "incremental": false, + "declaration": true, + "exactOptionalPropertyTypes": true, + "target": "es2020" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c163635..6fde587c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: apps/server: dependencies: + '@types/dinero.js': + specifier: ^1.9.4 + version: 1.9.4 bcrypt: specifier: ^5.1.1 version: 5.1.1 @@ -29,6 +32,9 @@ importers: cors: specifier: ^2.8.5 version: 2.8.5 + dinero.js: + specifier: ^1.9.1 + version: 1.9.1 dotenv: specifier: ^16.4.7 version: 16.4.7 @@ -296,6 +302,10 @@ importers: specifier: ^5.7.3 version: 5.7.3 + packages/rdx-ddd: {} + + packages/rdx-verifactu: {} + packages/shared: {} packages/typescript-config: {} @@ -1267,6 +1277,9 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/dinero.js@1.9.4': + resolution: {integrity: sha512-mtJnan4ajy9MqvoJGVXu0tC9EAAzFjeoKc3d+8AW+H/Od9+8IiC59ymjrZF+JdTToyDvkLReacTsc50Z8eYr6Q==} + '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} @@ -2011,6 +2024,9 @@ packages: resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} engines: {node: '>=0.3.1'} + dinero.js@1.9.1: + resolution: {integrity: sha512-1HXiF2vv3ZeRQ23yr+9lFxj/PbZqutuYWJnE0qfCB9xYBPnuaJ8lXtli1cJM0TvUXW1JTOaePldmqN5JVNxKSA==} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -5504,6 +5520,8 @@ snapshots: dependencies: '@types/ms': 2.1.0 + '@types/dinero.js@1.9.4': {} + '@types/estree@1.0.6': {} '@types/express-serve-static-core@4.19.6': @@ -6430,6 +6448,8 @@ snapshots: diff@4.0.2: {} + dinero.js@1.9.1: {} + dir-glob@3.0.1: dependencies: path-type: 4.0.0