.
This commit is contained in:
parent
0171f51c56
commit
7d1c441d34
@ -8,4 +8,5 @@ export * from "./quantity";
|
||||
export * from "./slug";
|
||||
export * from "./tin-number";
|
||||
export * from "./unique-id";
|
||||
export * from "./utc-date";
|
||||
export * from "./value-object";
|
||||
|
||||
41
apps/server/src/common/domain/value-objects/utc-date.spec.ts
Normal file
41
apps/server/src/common/domain/value-objects/utc-date.spec.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { UtcDate } from "./utc-date";
|
||||
|
||||
describe("UtcDate Value Object con Zod y Result", () => {
|
||||
test("Debe crear una instancia con fecha y hora en UTC", () => {
|
||||
const result = UtcDate.create("2025-01-06T19:36:18Z");
|
||||
expect(result.isSuccess).toBe(true);
|
||||
expect(result.data?.toISOString()).toBe("2025-01-06T19:36:18.000Z");
|
||||
expect(result.data?.toDateString()).toBe("2025-01-06");
|
||||
});
|
||||
|
||||
test("Debe crear una instancia con solo fecha (sin hora)", () => {
|
||||
const result = UtcDate.create("2020-11-12");
|
||||
expect(result.isSuccess).toBe(true);
|
||||
expect(result.data?.toISOString()).toBe("2020-11-12T00:00:00.000Z"); // Normalizado con 00:00 UTC
|
||||
expect(result.data?.toDateString()).toBe("2020-11-12");
|
||||
});
|
||||
|
||||
test("Debe devolver un Result.fail para formatos inválidos", () => {
|
||||
expect(UtcDate.create("2020-07-32").isFailure).toBe(true);
|
||||
expect(UtcDate.create("invalid-date").isFailure).toBe(true);
|
||||
expect(UtcDate.create("2020/11/12").isFailure).toBe(true);
|
||||
});
|
||||
|
||||
test("Debe comparar correctamente dos fechas idénticas", () => {
|
||||
const date1 = UtcDate.create("2023-12-31T23:59:59Z").data!;
|
||||
const date2 = UtcDate.create("2023-12-31T23:59:59Z").data!;
|
||||
expect(date1.equals(date2)).toBe(true);
|
||||
});
|
||||
|
||||
test("Debe comparar correctamente dos fechas diferentes", () => {
|
||||
const date1 = UtcDate.create("2023-12-31T23:59:59Z").data!;
|
||||
const date2 = UtcDate.create("2023-12-30T23:59:59Z").data!;
|
||||
expect(date1.equals(date2)).toBe(false);
|
||||
});
|
||||
|
||||
test("Debe manejar fechas sin hora correctamente en equals", () => {
|
||||
const date1 = UtcDate.create("2020-11-12").data!;
|
||||
const date2 = UtcDate.create("2020-11-12T00:00:00Z").data!;
|
||||
expect(date1.equals(date2)).toBe(true);
|
||||
});
|
||||
});
|
||||
67
apps/server/src/common/domain/value-objects/utc-date.ts
Normal file
67
apps/server/src/common/domain/value-objects/utc-date.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { Result } from "@common/helpers";
|
||||
import { z } from "zod";
|
||||
import { ValueObject } from "./value-object";
|
||||
|
||||
interface IUtcDateProps {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export class UtcDate extends ValueObject<IUtcDateProps> {
|
||||
private readonly date!: Date;
|
||||
|
||||
private constructor(props: IUtcDateProps) {
|
||||
super(props);
|
||||
const { value: dateString } = props;
|
||||
this.date = Object.freeze(new Date(dateString));
|
||||
}
|
||||
|
||||
static validate(dateString: string) {
|
||||
const dateStr = z.union([
|
||||
z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/, "Invalid ISO 8601 format"),
|
||||
z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Invalid YYYY-MM-DD format"),
|
||||
]);
|
||||
|
||||
const dateStrToDate = dateStr.pipe(z.coerce.date());
|
||||
|
||||
return dateStrToDate.safeParse(dateString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crea una instancia de UtcDate a partir de un string en formato UTC.
|
||||
* @param dateString Fecha en formato UTC (con o sin hora)
|
||||
* @returns UtcDate si es válida, Error en caso contrario.
|
||||
*/
|
||||
static create(dateString: string): Result<UtcDate, Error> {
|
||||
const dateIsValid = UtcDate.validate(dateString);
|
||||
if (!dateIsValid.success) {
|
||||
return Result.fail(new Error(`Invalid UTC date format: ${dateString}`));
|
||||
}
|
||||
|
||||
return Result.ok(new UtcDate({ value: dateString }));
|
||||
}
|
||||
|
||||
getValue(): string {
|
||||
return this.props.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Devuelve la fecha en formato UTC sin hora (YYYY-MM-DD).
|
||||
*/
|
||||
toDateString(): string {
|
||||
return this.date.toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Devuelve la fecha en formato UTC con hora (ISO 8601).
|
||||
*/
|
||||
toISOString(): string {
|
||||
return this.date.toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compara si dos instancias de UtcDate son iguales.
|
||||
*/
|
||||
equals(other: UtcDate): boolean {
|
||||
return this.toISOString() === other.toISOString();
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,32 @@
|
||||
import { AggregateRoot, UniqueID } from "@common/domain";
|
||||
import { AggregateRoot, UniqueID, UtcDate } from "@common/domain";
|
||||
import { Result } from "@common/helpers";
|
||||
import { CustomerInvoiceItem } from "../../entities";
|
||||
import { InvoiceStatus } from "../../value-objetcs";
|
||||
import { Customer } from "../customer/customer";
|
||||
|
||||
export interface ICustomerInvoiceProps {}
|
||||
export interface ICustomerInvoiceProps {
|
||||
status: InvoiceStatus;
|
||||
issueDate: UtcDate;
|
||||
invoiceNumber: string;
|
||||
invoiceType: string;
|
||||
|
||||
export interface ICustomerInvoice {}
|
||||
customer: Customer;
|
||||
items: CustomerInvoiceItem[];
|
||||
}
|
||||
|
||||
export interface ICustomerInvoice {
|
||||
id: UniqueID;
|
||||
status: InvoiceStatus;
|
||||
issueDate: UtcDate;
|
||||
invoiceNumber: string;
|
||||
invoiceType: string;
|
||||
|
||||
customer: Customer;
|
||||
items: CustomerInvoiceItem[];
|
||||
|
||||
//send();
|
||||
//accept();
|
||||
}
|
||||
|
||||
export class CustomerInvoice
|
||||
extends AggregateRoot<ICustomerInvoiceProps>
|
||||
@ -22,4 +45,28 @@ export class CustomerInvoice
|
||||
|
||||
return Result.ok(invoice);
|
||||
}
|
||||
|
||||
get status(): InvoiceStatus {
|
||||
return this.props.status;
|
||||
}
|
||||
|
||||
get issueDate(): UtcDate {
|
||||
return this.props.issueDate;
|
||||
}
|
||||
|
||||
get invoiceNumber(): string {
|
||||
return this.props.invoiceNumber;
|
||||
}
|
||||
|
||||
get invoiceType(): string {
|
||||
return this.props.invoiceType;
|
||||
}
|
||||
|
||||
get customer(): Customer {
|
||||
return this.props.customer;
|
||||
}
|
||||
|
||||
get items(): CustomerInvoiceItem[] {
|
||||
return this.props.items;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from "./invoice-status";
|
||||
@ -0,0 +1,56 @@
|
||||
import { InvoiceStatus } from "./invoice-status";
|
||||
|
||||
// Pruebas para InvoiceStatus
|
||||
describe("InvoiceStatus", () => {
|
||||
test("Debe crear estados válidos", () => {
|
||||
const result1 = InvoiceStatus.create("borrador");
|
||||
const result2 = InvoiceStatus.create("enviada");
|
||||
const result3 = InvoiceStatus.create("cerrada");
|
||||
|
||||
expect(result1.isSuccess).toBe(true);
|
||||
expect(result1.data.getValue()).toBe("borrador");
|
||||
expect(result1.data.toString()).toBe("borrador");
|
||||
|
||||
expect(result2.isSuccess).toBe(true);
|
||||
expect(result2.data.getValue()).toBe("enviada");
|
||||
expect(result2.data.toString()).toBe("enviada");
|
||||
|
||||
expect(result3.isSuccess).toBe(true);
|
||||
expect(result3.data.getValue()).toBe("cerrada");
|
||||
expect(result3.data.toString()).toBe("cerrada");
|
||||
});
|
||||
|
||||
test("Debe lanzar error al crear un estado inválido", () => {
|
||||
const result = InvoiceStatus.create("inexistente");
|
||||
expect(result.isFailure).toBe(true);
|
||||
});
|
||||
|
||||
test("Debe permitir transiciones válidas #1", () => {
|
||||
const result = InvoiceStatus.create("borrador");
|
||||
expect(result.isSuccess).toBe(true);
|
||||
expect(result.data.canTransitionTo("enviada")).toBe(true);
|
||||
});
|
||||
|
||||
test("Debe permitir transiciones válidas #2", () => {
|
||||
const result = InvoiceStatus.create("borrador");
|
||||
expect(result.isSuccess).toBe(true);
|
||||
|
||||
const new_state = result.data.transitionTo("enviada");
|
||||
expect(new_state.isSuccess).toBe(true);
|
||||
expect(new_state.data.toString()).toBe("enviada");
|
||||
});
|
||||
|
||||
test("Debe impedir transiciones inválidas", () => {
|
||||
const result = InvoiceStatus.create("cerrada");
|
||||
expect(result.isSuccess).toBe(true);
|
||||
expect(result.data.canTransitionTo("enviada")).toBe(false);
|
||||
});
|
||||
|
||||
test("Debe lanzar error al intentar transición inválida", () => {
|
||||
const result = InvoiceStatus.create("borrador");
|
||||
expect(result.isSuccess).toBe(true);
|
||||
const new_state = result.data.transitionTo("cerrada");
|
||||
expect(new_state.isSuccess).toBe(false);
|
||||
expect(new_state.error.message).toContain("Transición no permitida de borrador a cerrada");
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,56 @@
|
||||
import { ValueObject } from "@common/domain";
|
||||
import { Result } from "@common/helpers";
|
||||
|
||||
interface IInvoiceStatusProps {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export class InvoiceStatus extends ValueObject<IInvoiceStatusProps> {
|
||||
private static readonly ALLOWED_STATUSES = [
|
||||
"borrador",
|
||||
"enviada",
|
||||
"aceptada",
|
||||
"registrada",
|
||||
"rechazada",
|
||||
"cerrada",
|
||||
"error",
|
||||
];
|
||||
|
||||
private static readonly TRANSITIONS: Record<string, string[]> = {
|
||||
borrador: ["enviada"],
|
||||
enviada: ["aceptada", "registrada", "rechazada", "error"],
|
||||
aceptada: ["registrada"],
|
||||
registrada: ["cerrada"],
|
||||
rechazada: ["cerrada"],
|
||||
error: ["borrador"],
|
||||
cerrada: [],
|
||||
};
|
||||
|
||||
static create(value: string): Result<InvoiceStatus, Error> {
|
||||
if (!this.ALLOWED_STATUSES.includes(value)) {
|
||||
return Result.fail(new Error(`Estado de factura no válido: ${value}`));
|
||||
}
|
||||
return Result.ok(new InvoiceStatus({ value }));
|
||||
}
|
||||
|
||||
getValue(): string {
|
||||
return this.props.value;
|
||||
}
|
||||
|
||||
canTransitionTo(nextStatus: string): boolean {
|
||||
return InvoiceStatus.TRANSITIONS[this.props.value].includes(nextStatus);
|
||||
}
|
||||
|
||||
transitionTo(nextStatus: string): Result<InvoiceStatus, Error> {
|
||||
if (!this.canTransitionTo(nextStatus)) {
|
||||
return Result.fail(
|
||||
new Error(`Transición no permitida de ${this.props.value} a ${nextStatus}`)
|
||||
);
|
||||
}
|
||||
return InvoiceStatus.create(nextStatus);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.getValue();
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import { UniqueID, UtcDate } from "@common/domain";
|
||||
import { Result } from "@common/helpers";
|
||||
import {
|
||||
ISequelizeMapper,
|
||||
@ -5,6 +6,7 @@ import {
|
||||
SequelizeMapper,
|
||||
} from "@common/infrastructure/sequelize/sequelize-mapper";
|
||||
import { CustomerInvoice } from "@contexts/customer-billing/domain";
|
||||
import { InvoiceStatus } from "@contexts/customer-billing/domain/value-objetcs";
|
||||
import {
|
||||
CustomerInvoiceCreationAttributes,
|
||||
CustomerInvoiceModel,
|
||||
@ -25,8 +27,27 @@ export class CustomerInvoiceMapper
|
||||
source: CustomerInvoiceModel,
|
||||
params?: MapperParamsType
|
||||
): Result<CustomerInvoice, Error> {
|
||||
/*const idOrError = UniqueID.create(source.id);
|
||||
const tinOrError = TINNumber.create(source.tin);
|
||||
const idOrError = UniqueID.create(source.id);
|
||||
const statusOrError = InvoiceStatus.create(source.status);
|
||||
const issueDateOrError = UtcDate.create(source.issue_date);
|
||||
|
||||
const result = Result.combine([idOrError, statusOrError]);
|
||||
|
||||
if (result.isFailure) {
|
||||
return Result.fail(result.error);
|
||||
}
|
||||
|
||||
return CustomerInvoice.create(
|
||||
{
|
||||
status: statusOrError.data,
|
||||
issueDate: issueDateOrError.data,
|
||||
invoiceNumber: source.invoice_number,
|
||||
invoiceType: source.invoice_type,
|
||||
},
|
||||
idOrError.data
|
||||
);
|
||||
|
||||
/*const tinOrError = TINNumber.create(source.tin);
|
||||
const emailOrError = EmailAddress.create(source.email);
|
||||
const phoneOrError = PhoneNumber.create(source.phone);
|
||||
const faxOrError = PhoneNumber.createNullable(source.fax);
|
||||
@ -77,6 +98,10 @@ export class CustomerInvoiceMapper
|
||||
source: CustomerInvoice,
|
||||
params?: MapperParamsType
|
||||
): Result<CustomerInvoiceCreationAttributes, Error> {
|
||||
return Result.ok({
|
||||
id: source.id.toString(),
|
||||
status: source.status.toString(),
|
||||
});
|
||||
/*return Result.ok({
|
||||
id: source.id.toString(),
|
||||
reference: source.reference,
|
||||
|
||||
@ -48,7 +48,7 @@ export class CustomerInvoiceModel extends Model<
|
||||
|
||||
declare issue_date: string;
|
||||
declare invoice_number: string;
|
||||
declare invoide_type: string;
|
||||
declare invoice_type: string;
|
||||
|
||||
declare lang_code: string;
|
||||
declare currency_code: string;
|
||||
@ -110,7 +110,7 @@ export default (sequelize: Sequelize) => {
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
invoide_type: {
|
||||
invoice_type: {
|
||||
type: new DataTypes.STRING(),
|
||||
allowNull: false,
|
||||
},
|
||||
|
||||
Loading…
Reference in New Issue
Block a user