.
This commit is contained in:
parent
c8d4f4fa52
commit
1f666a1a5d
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -11,7 +11,7 @@ export class EmailAddress extends ValueObject<EmailAddressProps> {
|
||||
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! }));
|
||||
}
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
149
apps/server/src/common/domain/value-objects/money-value.ts
Normal file
149
apps/server/src/common/domain/value-objects/money-value.ts
Normal file
@ -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<IMoneyValueProps> 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);
|
||||
}
|
||||
}
|
||||
@ -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", () => {
|
||||
|
||||
@ -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<NameProps> {
|
||||
export class Name extends ValueObject<INameProps> {
|
||||
private static readonly MAX_LENGTH = 255;
|
||||
|
||||
protected static validate(value: string) {
|
||||
@ -21,9 +21,9 @@ export class Name extends ValueObject<NameProps> {
|
||||
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<Maybe<Name>, Error> {
|
||||
|
||||
@ -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%");
|
||||
});
|
||||
});
|
||||
83
apps/server/src/common/domain/value-objects/percentage.ts
Normal file
83
apps/server/src/common/domain/value-objects/percentage.ts
Normal file
@ -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<IPercentageProps> 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<Percentage> {
|
||||
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)}%`;
|
||||
}
|
||||
}
|
||||
@ -13,7 +13,7 @@ export class PhoneNumber extends ValueObject<PhoneNumberProps> {
|
||||
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! }));
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ export class PostalAddress extends ValueObject<IPostalAddressProps> {
|
||||
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!));
|
||||
}
|
||||
|
||||
122
apps/server/src/common/domain/value-objects/quantity.ts
Normal file
122
apps/server/src/common/domain/value-objects/quantity.ts
Normal file
@ -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<Quantity, Error>;
|
||||
decrement(anotherQuantity?: Quantity): Result<Quantity, Error>;
|
||||
hasSameScale(otherQuantity: Quantity): boolean;
|
||||
convertScale(newScale: number): Result<Quantity, Error>;
|
||||
}
|
||||
|
||||
export class Quantity extends ValueObject<IQuantityProps> 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<Quantity, Error> {
|
||||
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<Quantity, Error> {
|
||||
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<Quantity, Error> {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@ -26,7 +26,7 @@ export class Slug extends ValueObject<SlugProps> {
|
||||
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! }));
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@ export class TINNumber extends ValueObject<TINNumberProps> {
|
||||
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! }));
|
||||
}
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -9,7 +9,7 @@ export class UniqueID extends ValueObject<string> {
|
||||
if (!generateOnEmpty) {
|
||||
return Result.fail(new Error("ID cannot be undefined or null"));
|
||||
}
|
||||
UniqueID.generateNewID();
|
||||
return UniqueID.generateNewID();
|
||||
}
|
||||
|
||||
const result = UniqueID.validate(id!);
|
||||
|
||||
@ -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<ITestValueProps> {
|
||||
constructor(value: string) {
|
||||
super({ value });
|
||||
}
|
||||
|
||||
getValue() {
|
||||
return this.props;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<number>();
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -24,10 +24,14 @@ export class Maybe<T> {
|
||||
return this.value !== undefined;
|
||||
}
|
||||
|
||||
getValue(): T | undefined {
|
||||
unwrap(): T | undefined {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
getOrUndefined(): T | undefined {
|
||||
return this.unwrap();
|
||||
}
|
||||
|
||||
map<U>(fn: (value: T) => U): Maybe<U> {
|
||||
return this.isSome() ? Maybe.Some(fn(this.value as T)) : Maybe.None();
|
||||
}
|
||||
|
||||
@ -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<Result<User[], Error>> {
|
||||
async findUsers(transaction?: any): Promise<Result<Collection<User>, 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<Result<User>> {
|
||||
|
||||
@ -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<User>) => IListUsersResponseDTO[];
|
||||
}
|
||||
|
||||
export const listUsersPresenter: IListUsersPresenter = {
|
||||
toDTO: (users: User[]): IListUsersResponseDTO[] =>
|
||||
toDTO: (users: Collection<User>): IListUsersResponseDTO[] =>
|
||||
users.map((user) => ({
|
||||
id: ensureString(user.id.toString()),
|
||||
email: ensureString(user.email.toString()),
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from "./list-customers";
|
||||
@ -0,0 +1 @@
|
||||
export * from "./list-customers.use-case";
|
||||
@ -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<Result<Collection<Customer>, Error>> {
|
||||
return this.transactionManager.complete((transaction) => {
|
||||
return this.customerService.findCustomers(transaction);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./customer-invoices";
|
||||
export * from "./customers";
|
||||
@ -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<ICustomerInvoiceProps>
|
||||
implements ICustomerInvoice
|
||||
{
|
||||
static create(props: ICustomerInvoiceProps, id?: UniqueID): Result<CustomerInvoice, Error> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from "./customer-invoice";
|
||||
@ -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<string>;
|
||||
website: Maybe<string>;
|
||||
fax: Maybe<PhoneNumber>;
|
||||
}
|
||||
|
||||
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<string>;
|
||||
fax: Maybe<PhoneNumber>;
|
||||
website: Maybe<string>;
|
||||
|
||||
isCustomer: boolean;
|
||||
isFreelancer: boolean;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export class Customer extends AggregateRoot<ICustomerProps> implements ICustomer {
|
||||
static create(props: ICustomerProps, id?: UniqueID): Result<Customer, Error> {
|
||||
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<PhoneNumber> {
|
||||
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";
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from "./customer";
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./customer";
|
||||
export * from "./customer-invoice";
|
||||
@ -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<string>; // 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<string>;
|
||||
quantity: Quantity;
|
||||
unitPrice: MoneyValue;
|
||||
subtotalPrice: MoneyValue;
|
||||
discount: Percentage;
|
||||
totalPrice: MoneyValue;
|
||||
}
|
||||
|
||||
export class CustomerInvoiceItem
|
||||
extends DomainEntity<ICustomerInvoiceItemProps>
|
||||
implements ICustomerInvoiceItem
|
||||
{
|
||||
public static create(
|
||||
props: ICustomerInvoiceItemProps,
|
||||
id?: UniqueID
|
||||
): Result<CustomerInvoiceItem, Error> {
|
||||
return Result.ok(new CustomerInvoiceItem(props, id));
|
||||
}
|
||||
|
||||
get description(): Maybe<string> {
|
||||
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()));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
export * from "./aggregates";
|
||||
export * from "./repositories";
|
||||
export * from "./services";
|
||||
@ -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<Result<Collection<Customer>, Error>>;
|
||||
findById(id: UniqueID, transaction?: any): Promise<Result<Customer, Error>>;
|
||||
findByEmail(email: EmailAddress, transaction?: any): Promise<Result<Customer, Error>>;
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from "./customer-repository.interface";
|
||||
@ -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<Result<Collection<Customer>, Error>>;
|
||||
findCustomerById(userId: UniqueID, transaction?: any): Promise<Result<Customer>>;
|
||||
}
|
||||
@ -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<Result<Collection<Customer>, 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<Result<Customer>> {
|
||||
return await this.customerRepository.findById(customerId, transaction);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./customer-service.interface";
|
||||
export * from "./customer.service";
|
||||
@ -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<CustomerModel, CustomerCreationAttributes, Customer> {}
|
||||
|
||||
export class CustomerMapper
|
||||
extends SequelizeMapper<CustomerModel, CustomerCreationAttributes, Customer>
|
||||
implements ICustomerMapper
|
||||
{
|
||||
public mapToDomain(source: CustomerModel, params?: MapperParamsType): Result<Customer, Error> {
|
||||
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<CustomerCreationAttributes, Error> {
|
||||
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 };
|
||||
@ -0,0 +1 @@
|
||||
export * from "./customer.mapper";
|
||||
@ -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<CustomerInvoiceItemModel, { omit: "invoice" }>,
|
||||
InferCreationAttributes<CustomerInvoiceItemModel, { omit: "invoice" }>
|
||||
> {
|
||||
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<number | null>;
|
||||
declare position: number;
|
||||
declare description: CreationOptional<string | null>;
|
||||
declare quantity: CreationOptional<number | null>;
|
||||
declare unit_price: CreationOptional<number | null>;
|
||||
declare subtotal_price: CreationOptional<number | null>;
|
||||
declare discount: CreationOptional<number | null>;
|
||||
declare total_price: CreationOptional<number | null>;
|
||||
|
||||
declare invoice: NonAttribute<CustomerInvoiceModel>;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
@ -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<CustomerInvoiceModel, { omit: "items" | "customer" }>,
|
||||
InferCreationAttributes<CustomerInvoiceModel, { omit: "items" | "customer" }>
|
||||
> {
|
||||
// To avoid table creation
|
||||
/*static async sync(): Promise<any> {
|
||||
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<string>;
|
||||
|
||||
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<number | null>;
|
||||
|
||||
declare discount: CreationOptional<number | null>;
|
||||
declare discount_price: CreationOptional<number | null>;
|
||||
|
||||
declare before_tax_price: CreationOptional<number | null>;
|
||||
|
||||
declare tax: CreationOptional<number | null>;
|
||||
declare tax_price: CreationOptional<number | null>;
|
||||
|
||||
declare total_price: CreationOptional<number | null>;
|
||||
|
||||
declare notes: CreationOptional<string>;
|
||||
|
||||
declare items: NonAttribute<CustomerInvoiceItemModel[]>;
|
||||
declare customer: NonAttribute<CustomerModel>;
|
||||
|
||||
declare integrity_hash: CreationOptional<string>;
|
||||
declare previous_invoice_id: CreationOptional<string>;
|
||||
declare signed_at: CreationOptional<string>;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
@ -0,0 +1,172 @@
|
||||
import {
|
||||
CreationOptional,
|
||||
DataTypes,
|
||||
InferAttributes,
|
||||
InferCreationAttributes,
|
||||
Model,
|
||||
Sequelize,
|
||||
} from "sequelize";
|
||||
|
||||
export type CustomerCreationAttributes = InferCreationAttributes<CustomerModel, {}> & {};
|
||||
|
||||
export class CustomerModel extends Model<
|
||||
InferAttributes<CustomerModel>,
|
||||
InferCreationAttributes<CustomerModel>
|
||||
> {
|
||||
// To avoid table creation
|
||||
/*static async sync(): Promise<any> {
|
||||
return Promise.resolve();
|
||||
}*/
|
||||
|
||||
declare id: string;
|
||||
declare reference: CreationOptional<string>;
|
||||
|
||||
declare is_freelancer: boolean;
|
||||
declare name: string;
|
||||
declare trade_name: CreationOptional<string>;
|
||||
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<string>;
|
||||
declare website: CreationOptional<string>;
|
||||
|
||||
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;
|
||||
};
|
||||
@ -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<Customer> 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<Result<Collection<Customer>, 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<Result<Customer, Error>> {
|
||||
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<Result<Customer, Error>> {
|
||||
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 };
|
||||
@ -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;
|
||||
};
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./list-customer-invoices.controller";
|
||||
export * from "./list-customer-invoices.presenter";
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
import { Collection, ensureBoolean, ensureNumber, ensureString } from "@common/helpers";
|
||||
|
||||
import { IListCustomerInvoicesResponseDTO } from "../../../dto";
|
||||
|
||||
export interface IListCustomerInvoicesPresenter {
|
||||
toDTO: (customers: Collection<CustomerInvoice>) => IListCustomerInvoicesResponseDTO[];
|
||||
}
|
||||
|
||||
export const listCustomerInvoicesPresenter: IListCustomerInvoicesPresenter = {
|
||||
toDTO: (customers: Collection<CustomerInvoice>): 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),
|
||||
})),
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export * from "./list";
|
||||
@ -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);
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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<Customer>) => IListCustomersResponseDTO[];
|
||||
}
|
||||
|
||||
export const listCustomersPresenter: IListCustomersPresenter = {
|
||||
toDTO: (customers: Collection<Customer>): 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),
|
||||
})),
|
||||
};
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./customer-invoices";
|
||||
export * from "./customers";
|
||||
@ -0,0 +1 @@
|
||||
export interface IListCustomerInvoicesRequestDTO {}
|
||||
@ -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;
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ListCustomerInvoicesSchema = z.object({});
|
||||
@ -0,0 +1,3 @@
|
||||
export * from "./customer-invoices.request.dto";
|
||||
export * from "./customer-invoices.response.dto";
|
||||
export * from "./customer-invoices.validation.dto";
|
||||
@ -0,0 +1 @@
|
||||
export interface IListCustomersRequestDTO {}
|
||||
@ -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;
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ListCustomersSchema = z.object({});
|
||||
@ -0,0 +1,4 @@
|
||||
export * from "./customer-invoices";
|
||||
export * from "./customers.request.dto";
|
||||
export * from "./customers.response.dto";
|
||||
export * from "./customers.validation.dto";
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./controllers/customers";
|
||||
export * from "./dto";
|
||||
@ -1,5 +0,0 @@
|
||||
export * from "./aggregates";
|
||||
export * from "./entities";
|
||||
export * from "./events";
|
||||
export * from "./repositories";
|
||||
export * from "./value-objects";
|
||||
@ -1,2 +0,0 @@
|
||||
export * from "./controllers";
|
||||
export * from "./dto";
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
20
apps/server/src/routes/customer-invoices.routes.ts
Normal file
20
apps/server/src/routes/customer-invoices.routes.ts
Normal file
@ -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);
|
||||
};
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
BIN
doc/DsRegistroVeriFactu.xlsx
Normal file
BIN
doc/DsRegistroVeriFactu.xlsx
Normal file
Binary file not shown.
12
packages/rdx-ddd/package.json
Normal file
12
packages/rdx-ddd/package.json
Normal file
@ -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"
|
||||
}
|
||||
18
packages/rdx-verifactu/.eslintrc.json
Normal file
18
packages/rdx-verifactu/.eslintrc.json
Normal file
@ -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"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
2
packages/rdx-verifactu/.gitignore
vendored
Normal file
2
packages/rdx-verifactu/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
12
packages/rdx-verifactu/package.json
Normal file
12
packages/rdx-verifactu/package.json
Normal file
@ -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"
|
||||
}
|
||||
20
packages/rdx-verifactu/tsconfig.json
Normal file
20
packages/rdx-verifactu/tsconfig.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user