This commit is contained in:
David Arranz 2025-09-13 14:02:37 +02:00
parent af7d3dcf28
commit c7b42fa8ba
182 changed files with 0 additions and 7861 deletions

View File

@ -1,18 +0,0 @@
import { ICustomerInvoiceService } from "@/contexts/customer-billing/domain";
import { CustomerInvoice } from "@/contexts/customer-billing/domain/aggregates";
import { UniqueID } from "@/core/common/domain";
import { ITransactionManager } from "@/core/common/infrastructure/database";
import { Result } from "@repo/rdx-utils";
export class GetCustomerInvoiceUseCase {
constructor(
private readonly invoiceService: ICustomerInvoiceService,
private readonly transactionManager: ITransactionManager
) {}
public execute(invoiceId: UniqueID): Promise<Result<CustomerInvoice, Error>> {
return this.transactionManager.complete((transaction) => {
return this.invoiceService.findCustomerInvoiceById(invoiceId, transaction);
});
}
}

View File

@ -1,2 +0,0 @@
export * from "./get-customer-invoice.use-case";
export * from "./list-customer-invoices-use-case";

View File

@ -1,16 +0,0 @@
import { ITransactionManager } from "@/core/common/infrastructure/database";
import { Collection, Result } from "@repo/rdx-utils";
import { CustomerInvoice, ICustomerInvoiceService } from "../domain";
export class ListCustomerInvoicesUseCase {
constructor(
private readonly invoiceService: ICustomerInvoiceService,
private readonly transactionManager: ITransactionManager
) {}
public execute(): Promise<Result<Collection<CustomerInvoice>, Error>> {
return this.transactionManager.complete((transaction) => {
return this.invoiceService.findCustomerInvoices(transaction);
});
}
}

View File

@ -1,78 +0,0 @@
import { AggregateRoot, UniqueID, UtcDate } from "@/core/common/domain";
import { Maybe, Result } from "@repo/rdx-utils";
import { Customer, CustomerInvoiceItem } from "../entities";
import { InvoiceStatus } from "../value-objetcs";
export interface ICustomerInvoiceProps {
status: InvoiceStatus;
issueDate: UtcDate;
invoiceNumber: string;
invoiceType: string;
invoiceCustomerReference: Maybe<string>;
customer: Customer;
items: CustomerInvoiceItem[];
}
export interface ICustomerInvoice {
id: UniqueID;
status: InvoiceStatus;
issueDate: UtcDate;
invoiceNumber: string;
invoiceType: string;
invoiceCustomerReference: Maybe<string>;
customer: Customer;
items: CustomerInvoiceItem[];
//send();
//accept();
}
export class CustomerInvoice
extends AggregateRoot<ICustomerInvoiceProps>
implements ICustomerInvoice
{
id: UniqueID;
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);
}
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 invoiceCustomerReference(): Maybe<string> {
return this.props.invoiceCustomerReference;
}
get customer(): Customer {
return this.props.customer;
}
get items(): CustomerInvoiceItem[] {
return this.props.items;
}
}

View File

@ -1 +0,0 @@
export * from "./customer-invoice";

View File

@ -1,107 +0,0 @@
import { DomainEntity, MoneyValue, Percentage, UniqueID } from "@/core/common/domain";
import { Quantity } from "@/core/common/domain/value-objects/quantity";
import { Maybe, Result } from "@repo/rdx-utils";
export interface ICustomerInvoiceItemProps {
description: Maybe<string>; // Descripción del artículo o servicio
quantity: Maybe<Quantity>; // Cantidad de unidades
unitPrice: Maybe<MoneyValue>; // Precio unitario en la moneda de la factura
// subtotalPrice: MoneyValue; // Precio unitario * Cantidad
discount: Maybe<Percentage>; // % descuento
// totalPrice: MoneyValue;
}
export interface ICustomerInvoiceItem {
description: Maybe<string>;
quantity: Maybe<Quantity>;
unitPrice: Maybe<MoneyValue>;
subtotalPrice: Maybe<MoneyValue>;
discount: Maybe<Percentage>;
totalPrice: Maybe<MoneyValue>;
isEmptyLine(): boolean;
}
export class CustomerInvoiceItem
extends DomainEntity<ICustomerInvoiceItemProps>
implements ICustomerInvoiceItem
{
private readonly _subtotalPrice!: Maybe<MoneyValue>;
private readonly _totalPrice!: Maybe<MoneyValue>;
static validate(props: ICustomerInvoiceItemProps) {
return Result.ok(props);
}
static create(
props: ICustomerInvoiceItemProps,
id?: UniqueID
): Result<CustomerInvoiceItem, Error> {
const validation = CustomerInvoiceItem.validate(props);
if (!validation.isSuccess) {
Result.fail(new Error("Invalid invoice line data"));
}
return Result.ok(new CustomerInvoiceItem(props, id));
}
private constructor(props: ICustomerInvoiceItemProps, id?: UniqueID) {
super(props, id);
this._subtotalPrice = this.calculateSubtotal();
this._totalPrice = this.calculateTotal();
}
isEmptyLine(): boolean {
return this.quantity.isNone() && this.unitPrice.isNone() && this.discount.isNone();
}
calculateSubtotal(): Maybe<MoneyValue> {
if (this.quantity.isNone() || this.unitPrice.isNone()) {
return Maybe.none();
}
const _quantity = this.quantity.getOrUndefined()!;
const _unitPrice = this.unitPrice.getOrUndefined()!;
const _subtotal = _unitPrice.multiply(_quantity);
return Maybe.some(_subtotal);
}
calculateTotal(): Maybe<MoneyValue> {
const subtotal = this.calculateSubtotal();
if (subtotal.isNone()) {
return Maybe.none();
}
const _subtotal = subtotal.getOrUndefined()!;
const _discount = this.discount.getOrUndefined()!;
const _total = _subtotal.subtract(_subtotal.percentage(_discount));
return Maybe.some(_total);
}
get description(): Maybe<string> {
return this.props.description;
}
get quantity(): Maybe<Quantity> {
return this.props.quantity;
}
get unitPrice(): Maybe<MoneyValue> {
return this.props.unitPrice;
}
get subtotalPrice(): Maybe<MoneyValue> {
return this._subtotalPrice;
}
get discount(): Maybe<Percentage> {
return this.props.discount;
}
get totalPrice(): Maybe<MoneyValue> {
return this._totalPrice;
}
}

View File

@ -1,45 +0,0 @@
import { AggregateRoot, PostalAddress, TINNumber, UniqueID } from "@/core/common/domain";
import { Result } from "@repo/rdx-utils";
export interface ICustomerProps {
name: string;
tin: TINNumber;
address: PostalAddress;
}
export interface ICustomer {
id: UniqueID;
name: string;
tin: TINNumber;
address: PostalAddress;
}
export class Customer extends AggregateRoot<ICustomerProps> implements ICustomer {
id: UniqueID;
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 name() {
return this.props.name;
}
get tin(): TINNumber {
return this.props.tin;
}
get address(): PostalAddress {
return this.props.address;
}
}

View File

@ -1,4 +0,0 @@
export * from "./customer";
export * from "./customer-invoice-item";
export * from "./tax";
export * from "./tax-collection";

View File

@ -1,33 +0,0 @@
import { Slug } from "@/core/common/domain";
import { Collection } from "@repo/rdx-utils";
import { Tax } from "./tax";
export class TaxCollection extends Collection<Tax> {
constructor(items: Tax[] = []) {
super(items);
}
/**
Agrega un impuesto a la colección garantizando que el slug sea único. */
add(tax: Tax): void {
if (this.exists(tax.slug)) {
throw new Error(`(El impuesto con slug "${tax.slug.toString()}" ya existe.`);
}
this.add(tax);
}
/**
Verifica si un slug ya existe en la colección. */
exists(slug: Slug): boolean {
return this.some((tax) => tax.slug.equals(slug));
}
/**
Encuentra un impuesto por su slug. */
findBySlug(slug: Slug): Tax | undefined {
return this.find((tax) => tax.slug.equals(slug));
}
}

View File

@ -1,32 +0,0 @@
import { DomainEntity, Percentage, Slug, UniqueID } from "@/core/common/domain";
import { Result } from "@repo/rdx-utils";
interface ITaxProps {
slug: Slug;
name: string;
taxValue: Percentage;
}
interface ITax {
slug: Slug;
name: string;
taxValue: Percentage;
}
export class Tax extends DomainEntity<ITaxProps> implements ITax {
static create(props: ITaxProps, id?: UniqueID): Result<Tax, Error> {
return Result.ok(new Tax(props, id));
}
get slug(): Slug {
return this.props.slug;
}
get name(): string {
return this.props.name;
}
get taxValue(): Percentage {
return this.props.taxValue;
}
}

View File

@ -1,4 +0,0 @@
export * from "./aggregates";
export * from "./entities";
export * from "./repositories";
export * from "./services";

View File

@ -1,8 +0,0 @@
import { UniqueID } from "@/core/common/domain";
import { Collection, Result } from "@repo/rdx-utils";
import { CustomerInvoice } from "../aggregates";
export interface ICustomerInvoiceRepository {
findAll(transaction?: any): Promise<Result<Collection<CustomerInvoice>, Error>>;
findById(id: UniqueID, transaction?: any): Promise<Result<CustomerInvoice, Error>>;
}

View File

@ -1 +0,0 @@
export * from "./customer-invoice-repository.interface";

View File

@ -1,8 +0,0 @@
import { UniqueID } from "@/core/common/domain";
import { Collection, Result } from "@repo/rdx-utils";
import { CustomerInvoice } from "../aggregates";
export interface ICustomerInvoiceService {
findCustomerInvoices(transaction?: any): Promise<Result<Collection<CustomerInvoice>, Error>>;
findCustomerInvoiceById(invoiceId: UniqueID, transaction?: any): Promise<Result<CustomerInvoice>>;
}

View File

@ -1,27 +0,0 @@
import { UniqueID } from "@/core/common/domain";
import { Collection, Result } from "@repo/rdx-utils";
import { CustomerInvoice } from "../aggregates";
import { ICustomerInvoiceRepository } from "../repositories";
import { ICustomerInvoiceService } from "./customer-invoice-service.interface";
export class CustomerInvoiceService implements ICustomerInvoiceService {
constructor(private readonly invoiceRepository: ICustomerInvoiceRepository) {}
async findCustomerInvoices(
transaction?: any
): Promise<Result<Collection<CustomerInvoice>, Error>> {
const invoicesOrError = await this.invoiceRepository.findAll(transaction);
if (invoicesOrError.isFailure) {
return Result.fail(invoicesOrError.error);
}
return Result.ok(invoicesOrError.data);
}
async findCustomerInvoiceById(
invoiceId: UniqueID,
transaction?: any
): Promise<Result<CustomerInvoice>> {
return await this.invoiceRepository.findById(invoiceId, transaction);
}
}

View File

@ -1,2 +0,0 @@
export * from "./customer-invoice-service.interface";
export * from "./customer-invoice.service";

View File

@ -1 +0,0 @@
export * from "./invoice-status";

View File

@ -1,56 +0,0 @@
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");
});
});

View File

@ -1,56 +0,0 @@
import { ValueObject } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
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();
}
}

View File

@ -1,2 +0,0 @@
export * from "./mappers";
export * from "./sequelize";

View File

@ -1,111 +0,0 @@
import { Customer, CustomerInvoice } from "@/contexts/customer-billing/domain";
import { InvoiceStatus } from "@/contexts/customer-billing/domain/value-objetcs";
import { PostalAddress, TINNumber, UniqueID, UtcDate } from "@/core/common/domain";
import {
type ISequelizeMapper,
type MapperParamsType,
SequelizeMapper,
} from "@/core/common/infrastructure/sequelize/sequelize-mapper";
import { Maybe, Result } from "@repo/rdx-utils";
import {
CustomerInvoiceCreationAttributes,
CustomerInvoiceModel,
} from "../sequelize/customer-invoice.model";
export interface ICustomerInvoiceMapper
extends ISequelizeMapper<
CustomerInvoiceModel,
CustomerInvoiceCreationAttributes,
CustomerInvoice
> {}
export class CustomerInvoiceMapper
extends SequelizeMapper<CustomerInvoiceModel, CustomerInvoiceCreationAttributes, CustomerInvoice>
implements ICustomerInvoiceMapper
{
public mapToDomain(
source: CustomerInvoiceModel,
params?: MapperParamsType
): Result<CustomerInvoice, Error> {
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, issueDateOrError]);
if (result.isFailure) {
return Result.fail(result.error);
}
// Customer
const customerIdOrError = UniqueID.create(source.customer_id);
const tinOrError = TINNumber.create(source.customer_tin);
const postalAddressOrError = PostalAddress.create({
street: source.customer_street,
street2: source.customer_street2,
city: source.customer_city,
state: source.customer_state,
postalCode: source.customer_postal_code,
country: source.customer_country,
});
const check2 = Result.combine([idOrError, tinOrError, postalAddressOrError]);
if (check2.isFailure) {
return Result.fail(check2.error);
}
const customerOrError = Customer.create(
{
name: source.customer_name,
tin: tinOrError.data,
address: postalAddressOrError.data,
},
customerIdOrError.data
);
return CustomerInvoice.create(
{
status: statusOrError.data,
issueDate: issueDateOrError.data,
invoiceNumber: source.invoice_number,
invoiceType: source.invoice_type,
invoiceCustomerReference: Maybe.fromNullable(source.invoice_customer_reference),
customer: customerOrError.data,
items: [],
},
idOrError.data
);
}
public mapToPersistence(
source: CustomerInvoice,
params?: MapperParamsType
): Result<CustomerInvoiceCreationAttributes, Error> {
return Result.ok({
id: source.id.toString(),
status: source.status.toString(),
issue_date: source.issueDate.toDateString(),
invoice_number: source.invoiceNumber,
invoice_type: source.invoiceType,
invoice_customer_reference: source.invoiceCustomerReference.getOrUndefined(),
lang_code: "es",
currency_code: "EUR",
customer_id: source.customer.id.toString(),
customer_name: source.customer.name,
customer_tin: source.customer.tin.toString(),
customer_street: source.customer.address.street,
customer_street2: source.customer.address.street2.getOrUndefined(),
customer_city: source.customer.address.city,
customer_postal_code: source.customer.address.postalCode,
customer_state: source.customer.address.state,
customer_country: source.customer.address.country,
});
}
}
const customerInvoiceMapper: CustomerInvoiceMapper = new CustomerInvoiceMapper();
export { customerInvoiceMapper };

View File

@ -1 +0,0 @@
export * from "./customer-invoice.mapper";

View File

@ -1,107 +0,0 @@
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: "id",
onDelete: "CASCADE",
});
}
declare customer_invoice_id: string;
declare item_id: string;
declare id_article: CreationOptional<number>;
declare position: number;
declare description: CreationOptional<string>;
declare quantity: CreationOptional<number>;
declare unit_price: CreationOptional<number>;
declare subtotal_price: CreationOptional<number>;
declare discount: CreationOptional<number>;
declare total_price: CreationOptional<number>;
declare invoice: NonAttribute<CustomerInvoiceModel>;
}
export default (sequelize: Sequelize) => {
CustomerInvoiceItemModel.init(
{
item_id: {
type: new DataTypes.UUID(),
primaryKey: true,
},
customer_invoice_id: {
type: new DataTypes.UUID(),
primaryKey: true,
},
id_article: {
type: DataTypes.BIGINT().UNSIGNED,
allowNull: true,
defaultValue: null,
},
position: {
type: new DataTypes.MEDIUMINT(),
autoIncrement: false,
allowNull: false,
},
description: {
type: new DataTypes.TEXT(),
allowNull: true,
defaultValue: null,
},
quantity: {
type: DataTypes.BIGINT(),
allowNull: true,
defaultValue: null,
},
unit_price: {
type: new DataTypes.BIGINT(),
allowNull: true,
defaultValue: null,
},
subtotal_price: {
type: new DataTypes.BIGINT(),
allowNull: true,
defaultValue: null,
},
discount: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
total_price: {
type: new DataTypes.BIGINT(),
allowNull: true,
defaultValue: null,
},
},
{
sequelize,
tableName: "customer_invoice_items",
timestamps: false,
indexes: [],
}
);
return CustomerInvoiceItemModel;
};

View File

@ -1,263 +0,0 @@
import {
CreationOptional,
DataTypes,
InferAttributes,
InferCreationAttributes,
Model,
NonAttribute,
Sequelize,
} from "sequelize";
import { CustomerInvoiceItemModel } from "./customer-invoice-item.model";
export type CustomerInvoiceCreationAttributes = InferCreationAttributes<
CustomerInvoiceModel,
{ omit: "items" }
> & {
// creo que no es necesario
//items: CustomerInvoiceItemCreationAttributes[];
//customer_id: string;
};
export class CustomerInvoiceModel extends Model<
InferAttributes<CustomerInvoiceModel, { omit: "items" }>,
InferCreationAttributes<CustomerInvoiceModel, { omit: "items" }>
> {
// 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: "customer_invoice_id",
onDelete: "CASCADE",
});
}
declare id: string;
declare status: string;
declare issue_date: string;
declare invoice_number: string;
declare invoice_type: string;
declare invoice_customer_reference?: CreationOptional<string>;
declare lang_code: string;
declare currency_code: string;
declare customer_id: string;
declare customer_tin: string;
declare customer_name: string;
declare customer_street: string;
declare customer_street2?: CreationOptional<string>;
declare customer_city: string;
declare customer_state: string;
declare customer_postal_code: string;
declare customer_country: string;
declare subtotal_price?: CreationOptional<number>;
declare discount?: CreationOptional<number>;
declare discount_price?: CreationOptional<number>;
declare before_tax_price?: CreationOptional<number>;
declare tax?: CreationOptional<number>;
declare tax_price?: CreationOptional<number>;
declare total_price?: CreationOptional<number>;
declare notes?: CreationOptional<string>;
declare items: NonAttribute<CustomerInvoiceItemModel[]>;
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,
},
invoice_customer_reference: {
type: new DataTypes.STRING(),
},
invoice_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(),
allowNull: false,
},
customer_name: {
type: DataTypes.STRING,
allowNull: false,
},
customer_tin: {
type: DataTypes.STRING,
allowNull: false,
},
customer_street: {
type: DataTypes.STRING,
allowNull: false,
},
customer_street2: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
},
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,
defaultValue: null,
},
discount: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
discount_price: {
type: new DataTypes.BIGINT(),
allowNull: true,
defaultValue: null,
},
before_tax_price: {
type: new DataTypes.BIGINT(),
allowNull: true,
defaultValue: null,
},
tax: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
tax_price: {
type: new DataTypes.BIGINT(),
allowNull: true,
defaultValue: null,
},
total_price: {
type: new DataTypes.BIGINT(),
allowNull: true,
defaultValue: null,
},
notes: {
type: DataTypes.TEXT,
allowNull: true,
defaultValue: null,
},
integrity_hash: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
comment: "Hash criptográfico para asegurar integridad",
},
previous_invoice_id: {
type: DataTypes.UUID,
allowNull: true,
defaultValue: null,
comment: "Referencia a la factura anterior (si aplica)",
},
signed_at: {
type: DataTypes.DATE,
allowNull: true,
defaultValue: null,
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: "invoice_number_idx", fields: ["invoice_number"] },
{ 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;
};

View File

@ -1,64 +0,0 @@
import { CustomerInvoice, ICustomerInvoiceRepository } from "@/contexts/customer-billing/domain";
import { UniqueID } from "@/core/common/domain";
import { SequelizeRepository } from "@/core/common/infrastructure";
import { Collection, Result } from "@repo/rdx-utils";
import { Transaction } from "sequelize";
import {
type ICustomerInvoiceMapper,
customerInvoiceMapper,
} from "../mappers/customer-invoice.mapper";
import { CustomerInvoiceModel } from "./customer-invoice.model";
class CustomerInvoiceRepository
extends SequelizeRepository<CustomerInvoice>
implements ICustomerInvoiceRepository
{
private readonly _mapper!: ICustomerInvoiceMapper;
/**
* 🔹 Función personalizada para mapear errores de unicidad en autenticación
*/
private _customErrorMapper(error: Error): string | null {
if (error.name === "SequelizeUniqueConstraintError") {
return "Customer invoice with this email already exists";
}
return null;
}
constructor(mapper: ICustomerInvoiceMapper) {
super();
this._mapper = mapper;
}
async findAll(transaction?: Transaction): Promise<Result<Collection<CustomerInvoice>, Error>> {
try {
const rawCustomerInvoices: any = await this._findAll(CustomerInvoiceModel, {}, transaction);
if (!rawCustomerInvoices === true) {
return Result.fail(new Error("Customer with email not exists"));
}
return this._mapper.mapArrayToDomain(rawCustomerInvoices);
} catch (error: any) {
return this._handleDatabaseError(error, this._customErrorMapper);
}
}
async findById(id: UniqueID, transaction?: Transaction): Promise<Result<CustomerInvoice, Error>> {
try {
const rawInvoice: any = await this._getById(CustomerInvoiceModel, id, {}, transaction);
if (!rawInvoice === true) {
return Result.fail(new Error(`Customer with id ${id.toString()} not exists`));
}
return this._mapper.mapToDomain(rawInvoice);
} catch (error: any) {
return this._handleDatabaseError(error, this._customErrorMapper);
}
}
}
const customerInvoiceRepository = new CustomerInvoiceRepository(customerInvoiceMapper);
export { customerInvoiceRepository };

View File

@ -1,175 +0,0 @@
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_companyr: 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_companyr: {
type: DataTypes.BOOLEAN,
allowNull: false,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
trade_name: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
},
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,
defaultValue: null,
},
website: {
type: DataTypes.STRING,
allowNull: true,
defaultValue: null,
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;
};

View File

@ -1,15 +0,0 @@
import { ICustomerInvoiceRepository } from "@/contexts/customer-billing/domain/";
import { customerInvoiceRepository } from "./customer-invoice.repository";
export * from "./customer-invoice.model";
export * from "./customer.model";
export * from "./customer-invoice.repository";
/*export const createCustomerRepository = (): ICustomerRepository => {
return customerRepository;
};*/
export const createCustomerInvoiceRepository = (): ICustomerInvoiceRepository => {
return customerInvoiceRepository;
};

View File

@ -1,44 +0,0 @@
import { GetCustomerInvoiceUseCase } from "@/contexts/customer-billing/application";
import { UniqueID } from "@/core/common/domain";
import { ExpressController } from "@/core/common/presentation";
import { IGetCustomerInvoicePresenter } from "./get-customer-invoice.presenter";
export class GetCustomerInvoiceController extends ExpressController {
public constructor(
private readonly getCustomerInvoice: GetCustomerInvoiceUseCase,
private readonly presenter: IGetCustomerInvoicePresenter
) {
super();
}
protected async executeImpl() {
const { invoiceId } = this.req.params;
// Validar ID
const invoiceIdOrError = UniqueID.create(invoiceId);
if (invoiceIdOrError.isFailure) return this.invalidInputError("Invoice ID not valid");
const invoiceOrError = await this.getCustomerInvoice.execute(invoiceIdOrError.data);
if (invoiceOrError.isFailure) {
return this.handleError(invoiceOrError.error);
}
return this.ok(this.presenter.toDTO(invoiceOrError.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);
}
}

View File

@ -1,12 +0,0 @@
import { CustomerInvoice } from "@/contexts/customer-billing/domain";
import { IGetCustomerInvoiceResponseDTO } from "@/contexts/customer-billing/presentation/dto";
export interface IGetCustomerInvoicePresenter {
toDTO: (invoice: CustomerInvoice) => IGetCustomerInvoiceResponseDTO;
}
export const getCustomerInvoicesPresenter: IGetCustomerInvoicePresenter = {
toDTO: (invoice: CustomerInvoice): IGetCustomerInvoiceResponseDTO => {
return {} as IGetCustomerInvoiceResponseDTO;
},
};

View File

@ -1,16 +0,0 @@
import { GetCustomerInvoiceUseCase } from "@/contexts/customer-billing/application/";
import { CustomerInvoiceService } from "@/contexts/customer-billing/domain";
import { customerInvoiceRepository } from "@/contexts/customer-billing/infraestructure";
import { SequelizeTransactionManager } from "@/core/common/infrastructure";
import { GetCustomerInvoiceController } from "./get-customer-invoice.controller";
import { getCustomerInvoicesPresenter } from "./get-customer-invoice.presenter";
export const getCustomerInvoiceController = () => {
const transactionManager = new SequelizeTransactionManager();
const customerInvoiceService = new CustomerInvoiceService(customerInvoiceRepository);
const useCase = new GetCustomerInvoiceUseCase(customerInvoiceService, transactionManager);
const presenter = getCustomerInvoicesPresenter;
return new GetCustomerInvoiceController(useCase, presenter);
};

View File

@ -1,2 +0,0 @@
export * from "./get";
export * from "./list";

View File

@ -1,16 +0,0 @@
import { CustomerInvoiceService } from "@/contexts/customer-billing/domain";
import { customerInvoiceRepository } from "@/contexts/customer-billing/infraestructure";
import { SequelizeTransactionManager } from "@/core/common/infrastructure";
import { ListCustomerInvoicesUseCase } from "../../../../application";
import { ListCustomerInvoicesController } from "./list-customer-invoices.controller";
import { listCustomerInvoicesPresenter } from "./list-customer-invoices.presenter";
export const listCustomerInvoicesController = () => {
const transactionManager = new SequelizeTransactionManager();
const customerInvoiceService = new CustomerInvoiceService(customerInvoiceRepository);
const useCase = new ListCustomerInvoicesUseCase(customerInvoiceService, transactionManager);
const presenter = listCustomerInvoicesPresenter;
return new ListCustomerInvoicesController(useCase, presenter);
};

View File

@ -1,37 +0,0 @@
import { ListCustomerInvoicesUseCase } from "@/contexts/customer-billing/application";
import { ExpressController } from "@/core/common/presentation";
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);
}
}

View File

@ -1,39 +0,0 @@
import { Collection, ensureString } from "@repo/rdx-utils";
import { CustomerInvoice } from "@/contexts/customer-billing/domain";
import { IListCustomerInvoicesResponseDTO } from "../../../dto";
export interface IListCustomerInvoicesPresenter {
toDTO: (customers: Collection<CustomerInvoice>) => IListCustomerInvoicesResponseDTO[];
}
export const listCustomerInvoicesPresenter: IListCustomerInvoicesPresenter = {
toDTO: (invoice: Collection<CustomerInvoice>): IListCustomerInvoicesResponseDTO[] =>
invoice.map((customer) => ({
id: ensureString(customer.id.toString()),
/*reference: ensureString(customer.),
is_companyr: 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),*/
})),
};

View File

@ -1 +0,0 @@
export * from "./customer-invoices";

View File

@ -1 +0,0 @@
export type IListCustomerInvoicesRequestDTO = {}

View File

@ -1,29 +0,0 @@
export interface IListCustomerInvoicesResponseDTO {
id: string;
/*reference: string;
is_companyr: 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;*/
}
export type IGetCustomerInvoiceResponseDTO = {};

View File

@ -1,4 +0,0 @@
import * as z from "zod/v4";
export const ListCustomerInvoicesSchema = z.object({});
export const GetCustomerInvoiceSchema = z.object({});

View File

@ -1,3 +0,0 @@
export * from "./customer-invoices.request.dto";
export * from "./customer-invoices.response.dto";
export * from "./customer-invoices.validation.dto";

View File

@ -1 +0,0 @@
export type IListCustomersRequestDTO = {}

View File

@ -1,27 +0,0 @@
export interface IListCustomersResponseDTO {
id: string;
reference: string;
is_companyr: 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;
}

View File

@ -1,3 +0,0 @@
import * as z from "zod/v4";
export const ListCustomersSchema = z.object({});

View File

@ -1,4 +0,0 @@
export * from "./customer-invoices";
export * from "./customers.request.dto";
export * from "./customers.response.dto";
export * from "./customers.validation.dto";

View File

@ -1,2 +0,0 @@
export * from "./controllers";
export * from "./dto";

View File

@ -1,29 +0,0 @@
{
"name": "@modules/invoices",
"version": "0.0.0",
"private": true,
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"files": ["dist/**"],
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"lint": "eslint src/",
"lint:fix": "eslint src/ --fix",
"typecheck": "tsc --noEmit",
"test": "jest"
},
"jest": {
"preset": "@repo/jest-presets/node"
},
"devDependencies": {
"@rdx/utils": "workspace:*",
"@repo/jest-presets": "workspace:*",
"@repo/typescript-config": "workspace:*",
"@types/dinero.js": "^1.9.4",
"jest": "^29.7.0",
"typescript": "^5.8.3"
},
"dependencies": {}
}

View File

@ -1 +0,0 @@
export default () => {};

View File

@ -1 +0,0 @@
export * from "./client";

View File

@ -1,147 +0,0 @@
import { UniqueID, UtcDate } from "core/common/domain";
import {
IInvoiceProps,
IInvoiceService,
Invoice,
InvoiceNumber,
InvoiceSerie,
InvoiceStatus,
} from "@contexts/invoices/domain";
import { Result } from "core/common/helpers";
import { ITransactionManager } from "core/common/infrastructure/database";
import { logger } from "core/common/infrastructure/logger";
import { ICreateInvoiceRequestDTO } from "../presentation/dto";
export class CreateInvoiceUseCase {
constructor(
private readonly invoiceService: IInvoiceService,
private readonly transactionManager: ITransactionManager
) {}
public execute(
invoiceID: UniqueID,
dto: ICreateInvoiceRequestDTO
): Promise<Result<Invoice, Error>> {
return this.transactionManager.complete(async (transaction) => {
try {
const validOrErrors = this.validateInvoiceData(dto);
if (validOrErrors.isFailure) {
return Result.fail(validOrErrors.error);
}
const data = validOrErrors.data;
// Update invoice with dto
return await this.invoiceService.createInvoice(invoiceID, data, transaction);
} catch (error: unknown) {
logger.error(error as Error);
return Result.fail(error as Error);
}
});
}
private validateInvoiceData(dto: ICreateInvoiceRequestDTO): Result<IInvoiceProps, Error> {
const errors: Error[] = [];
const invoiceNumerOrError = InvoiceNumber.create(dto.invoice_number);
const invoiceSeriesOrError = InvoiceSerie.create(dto.invoice_series);
const issueDateOrError = UtcDate.create(dto.issue_date);
const operationDateOrError = UtcDate.create(dto.operation_date);
const result = Result.combine([
invoiceNumerOrError,
invoiceSeriesOrError,
issueDateOrError,
operationDateOrError,
]);
if (result.isFailure) {
return Result.fail(result.error);
}
const validatedData: IInvoiceProps = {
status: InvoiceStatus.createDraft(),
invoiceNumber: invoiceNumerOrError.data,
invoiceSeries: invoiceSeriesOrError.data,
issueDate: issueDateOrError.data,
operationDate: operationDateOrError.data,
invoiceCurrency: dto.currency_code,
};
/*if (errors.length > 0) {
const message = errors.map((err) => err.message).toString();
return Result.fail(new Error(message));
}*/
return Result.ok(validatedData);
/*let invoice_status = InvoiceStatus.create(dto.status).object;
if (invoice_status.isEmpty()) {
invoice_status = InvoiceStatus.createDraft();
}
let invoice_series = InvoiceSeries.create(dto.invoice_series).object;
if (invoice_series.isEmpty()) {
invoice_series = InvoiceSeries.create(dto.invoice_series).object;
}
let issue_date = InvoiceDate.create(dto.issue_date).object;
if (issue_date.isEmpty()) {
issue_date = InvoiceDate.createCurrentDate().object;
}
let operation_date = InvoiceDate.create(dto.operation_date).object;
if (operation_date.isEmpty()) {
operation_date = InvoiceDate.createCurrentDate().object;
}
let invoiceCurrency = Currency.createFromCode(dto.currency).object;
if (invoiceCurrency.isEmpty()) {
invoiceCurrency = Currency.createDefaultCode().object;
}
let invoiceLanguage = Language.createFromCode(dto.language_code).object;
if (invoiceLanguage.isEmpty()) {
invoiceLanguage = Language.createDefaultCode().object;
}
const items = new Collection<InvoiceItem>(
dto.items?.map(
(item) =>
InvoiceSimpleItem.create({
description: Description.create(item.description).object,
quantity: Quantity.create(item.quantity).object,
unitPrice: UnitPrice.create({
amount: item.unit_price.amount,
currencyCode: item.unit_price.currency,
precision: item.unit_price.precision,
}).object,
}).object
)
);
if (!invoice_status.isDraft()) {
throw Error("Error al crear una factura que no es borrador");
}
return DraftInvoice.create(
{
invoiceSeries: invoice_series,
issueDate: issue_date,
operationDate: operation_date,
invoiceCurrency,
language: invoiceLanguage,
invoiceNumber: InvoiceNumber.create(undefined).object,
//notes: Note.create(invoiceDTO.notes).object,
//senderId: UniqueID.create(null).object,
recipient,
items,
},
invoiceId
);*/
}
}

View File

@ -1,23 +0,0 @@
import { UniqueID } from "core/common/domain";
import { Result } from "core/common/helpers";
import { ITransactionManager } from "core/common/infrastructure/database";
import { logger } from "core/common/infrastructure/logger";
import { IInvoiceService, Invoice } from "../domain";
export class DeleteInvoiceUseCase {
constructor(
private readonly invoiceService: IInvoiceService,
private readonly transactionManager: ITransactionManager
) {}
public execute(invoiceID: UniqueID): Promise<Result<Invoice, Error>> {
return this.transactionManager.complete(async (transaction) => {
try {
return await this.invoiceService.deleteInvoiceById(invoiceID, transaction);
} catch (error: unknown) {
logger.error(error as Error);
return Result.fail(error as Error);
}
});
}
}

View File

@ -1,23 +0,0 @@
import { UniqueID } from "core/common/domain";
import { Result } from "core/common/helpers";
import { ITransactionManager } from "core/common/infrastructure/database";
import { logger } from "core/common/infrastructure/logger";
import { IInvoiceService, Invoice } from "../domain";
export class GetInvoiceUseCase {
constructor(
private readonly invoiceService: IInvoiceService,
private readonly transactionManager: ITransactionManager
) {}
public execute(invoiceID: UniqueID): Promise<Result<Invoice, Error>> {
return this.transactionManager.complete(async (transaction) => {
try {
return await this.invoiceService.findInvoiceById(invoiceID, transaction);
} catch (error: unknown) {
logger.error(error as Error);
return Result.fail(error as Error);
}
});
}
}

View File

@ -1,5 +0,0 @@
export * from "./create-invoice.use-case";
export * from "./delete-invoice.use-case";
export * from "./get-invoice.use-case";
export * from "./list-invoices.use-case";
export * from "./update-invoice.use-case";

View File

@ -1,22 +0,0 @@
import { Collection, Result } from "core/common/helpers";
import { ITransactionManager } from "core/common/infrastructure/database";
import { logger } from "core/common/infrastructure/logger";
import { IInvoiceService, Invoice } from "../domain";
export class ListInvoicesUseCase {
constructor(
private readonly invoiceService: IInvoiceService,
private readonly transactionManager: ITransactionManager
) {}
public execute(): Promise<Result<Collection<Invoice>, Error>> {
return this.transactionManager.complete(async (transaction) => {
try {
return await this.invoiceService.findInvoices(transaction);
} catch (error: unknown) {
logger.error(error as Error);
return Result.fail(error as Error);
}
});
}
}

View File

@ -1,2 +0,0 @@
export * from "./participantAddressFinder";
export * from "./participantFinder";

View File

@ -1,70 +0,0 @@
import {
ApplicationServiceError,
IApplicationServiceError,
} from "@/contexts/common/application/services/ApplicationServiceError";
import { IAdapter, RepositoryBuilder } from "@/contexts/common/domain";
import { Result, UniqueID } from "@shared/contexts";
import { NullOr } from "@shared/utilities";
import {
IInvoiceParticipantAddress,
IInvoiceParticipantAddressRepository,
} from "../../domain";
export const participantAddressFinder = async (
addressId: UniqueID,
adapter: IAdapter,
repository: RepositoryBuilder<IInvoiceParticipantAddressRepository>,
) => {
if (addressId.isNull()) {
return Result.fail<IApplicationServiceError>(
ApplicationServiceError.create(
ApplicationServiceError.INVALID_REQUEST_PARAM,
`Participant address ID required`,
),
);
}
const transaction = adapter.startTransaction();
let address: NullOr<IInvoiceParticipantAddress> = null;
try {
await transaction.complete(async (t) => {
address = await repository({ transaction: t }).getById(addressId);
});
if (address === null) {
return Result.fail<IApplicationServiceError>(
ApplicationServiceError.create(
ApplicationServiceError.NOT_FOUND_ERROR,
"",
{
id: addressId.toString(),
entity: "participant address",
},
),
);
}
return Result.ok<IInvoiceParticipantAddress>(address);
} catch (error: unknown) {
const _error = error as Error;
if (repository().isRepositoryError(_error)) {
return Result.fail<IApplicationServiceError>(
ApplicationServiceError.create(
ApplicationServiceError.REPOSITORY_ERROR,
_error.message,
_error,
),
);
}
return Result.fail<IApplicationServiceError>(
ApplicationServiceError.create(
ApplicationServiceError.UNEXCEPTED_ERROR,
_error.message,
_error,
),
);
}
};

View File

@ -1,20 +0,0 @@
import { IAdapter, RepositoryBuilder } from "@/contexts/common/domain";
import { UniqueID } from "@shared/contexts";
import { IInvoiceParticipantRepository } from "../../domain";
import { InvoiceCustomer } from "../../domain/entities/invoice-customer/invoice-customer";
export const participantFinder = async (
participantId: UniqueID,
adapter: IAdapter,
repository: RepositoryBuilder<IInvoiceParticipantRepository>
): Promise<InvoiceCustomer | undefined> => {
if (!participantId || (participantId && participantId.isNull())) {
return Promise.resolve(undefined);
}
const participant = await adapter
.startTransaction()
.complete((t) => repository({ transaction: t }).getById(participantId));
return Promise.resolve(participant ? participant : undefined);
};

View File

@ -1,398 +0,0 @@
import { UniqueID } from "core/common/domain";
import { Result } from "core/common/helpers";
import { ITransactionManager } from "core/common/infrastructure/database";
import { logger } from "core/common/infrastructure/logger";
import { IUpdateInvoiceRequestDTO } from "../presentation/dto";
export class CreateInvoiceUseCase {
constructor(
private readonly invoiceService: IInvoiceService,
private readonly transactionManager: ITransactionManager
) {}
public execute(
invoiceID: UniqueID,
dto: Partial<IUpdateInvoiceRequestDTO>
): Promise<Result<Invoice, Error>> {
return this.transactionManager.complete(async (transaction) => {
try {
const validOrErrors = this.validateInvoiceData(dto);
if (validOrErrors.isFailure) {
return Result.fail(validOrErrors.error);
}
const data = validOrErrors.data;
// Update invoice with dto
return await this.invoiceService.updateInvoiceById(invoiceID, data, transaction);
} catch (error: unknown) {
logger.error(error as Error);
return Result.fail(error as Error);
}
});
}
private validateInvoiceData(
dto: Partial<IUpdateInvoiceRequestDTO>
): Result<Partial<IInvoiceProps>, Error> {
const errors: Error[] = [];
const validatedData: Partial<IInvoiceProps> = {};
// Create invoice
let invoice_status = InvoiceStatus.create(invoiceDTO.status).object;
if (invoice_status.isEmpty()) {
invoice_status = InvoiceStatus.createDraft();
}
let invoice_series = InvoiceSeries.create(invoiceDTO.invoice_series).object;
if (invoice_series.isEmpty()) {
invoice_series = InvoiceSeries.create(invoiceDTO.invoice_series).object;
}
let issue_date = InvoiceDate.create(invoiceDTO.issue_date).object;
if (issue_date.isEmpty()) {
issue_date = InvoiceDate.createCurrentDate().object;
}
let operation_date = InvoiceDate.create(invoiceDTO.operation_date).object;
if (operation_date.isEmpty()) {
operation_date = InvoiceDate.createCurrentDate().object;
}
let invoiceCurrency = Currency.createFromCode(invoiceDTO.currency).object;
if (invoiceCurrency.isEmpty()) {
invoiceCurrency = Currency.createDefaultCode().object;
}
let invoiceLanguage = Language.createFromCode(invoiceDTO.language_code).object;
if (invoiceLanguage.isEmpty()) {
invoiceLanguage = Language.createDefaultCode().object;
}
const items = new Collection<InvoiceItem>(
invoiceDTO.items?.map(
(item) =>
InvoiceSimpleItem.create({
description: Description.create(item.description).object,
quantity: Quantity.create(item.quantity).object,
unitPrice: UnitPrice.create({
amount: item.unit_price.amount,
currencyCode: item.unit_price.currency,
precision: item.unit_price.precision,
}).object,
}).object
)
);
if (!invoice_status.isDraft()) {
throw Error("Error al crear una factura que no es borrador");
}
return DraftInvoice.create(
{
invoiceSeries: invoice_series,
issueDate: issue_date,
operationDate: operation_date,
invoiceCurrency,
language: invoiceLanguage,
invoiceNumber: InvoiceNumber.create(undefined).object,
//notes: Note.create(invoiceDTO.notes).object,
//senderId: UniqueID.create(null).object,
recipient,
items,
},
invoiceId
);
}
}
export type UpdateInvoiceResponseOrError =
| Result<never, IUseCaseError> // Misc errors (value objects)
| Result<Invoice, never>; // Success!
export class UpdateInvoiceUseCase2
implements
IUseCase<{ id: UniqueID; data: IUpdateInvoice_DTO }, Promise<UpdateInvoiceResponseOrError>>
{
private _context: IInvoicingContext;
private _adapter: ISequelizeAdapter;
private _repositoryManager: IRepositoryManager;
constructor(context: IInvoicingContext) {
this._context = context;
this._adapter = context.adapter;
this._repositoryManager = context.repositoryManager;
}
private getRepository<T>(name: string) {
return this._repositoryManager.getRepository<T>(name);
}
private handleValidationFailure(
validationError: Error,
message?: string
): Result<never, IUseCaseError> {
return Result.fail<IUseCaseError>(
UseCaseError.create(
UseCaseError.INVALID_INPUT_DATA,
message ? message : validationError.message,
validationError
)
);
}
async execute(request: {
id: UniqueID;
data: IUpdateInvoice_DTO;
}): Promise<UpdateInvoiceResponseOrError> {
const { id, data: invoiceDTO } = request;
// Validaciones
const invoiceDTOOrError = ensureUpdateInvoice_DTOIsValid(invoiceDTO);
if (invoiceDTOOrError.isFailure) {
return this.handleValidationFailure(invoiceDTOOrError.error);
}
const transaction = this._adapter.startTransaction();
const invoiceRepoBuilder = this.getRepository<IInvoiceRepository>("Invoice");
let invoice: Invoice | null = null;
try {
await transaction.complete(async (t) => {
invoice = await invoiceRepoBuilder({ transaction: t }).getById(id);
});
if (invoice === null) {
return Result.fail<IUseCaseError>(
UseCaseError.create(UseCaseError.NOT_FOUND_ERROR, `Invoice not found`, {
id: request.id.toString(),
entity: "invoice",
})
);
}
return Result.ok<Invoice>(invoice);
} catch (error: unknown) {
const _error = error as Error;
if (invoiceRepoBuilder().isRepositoryError(_error)) {
return this.handleRepositoryError(error as BaseError, invoiceRepoBuilder());
} else {
return this.handleUnexceptedError(error);
}
}
// Recipient validations
/*const recipientIdOrError = ensureParticipantIdIsValid(
invoiceDTO?.recipient?.id,
);
if (recipientIdOrError.isFailure) {
return this.handleValidationFailure(
recipientIdOrError.error,
"Recipient ID not valid",
);
}
const recipientId = recipientIdOrError.object;
const recipientBillingIdOrError = ensureParticipantAddressIdIsValid(
invoiceDTO?.recipient?.billing_address_id,
);
if (recipientBillingIdOrError.isFailure) {
return this.handleValidationFailure(
recipientBillingIdOrError.error,
"Recipient billing address ID not valid",
);
}
const recipientBillingId = recipientBillingIdOrError.object;
const recipientShippingIdOrError = ensureParticipantAddressIdIsValid(
invoiceDTO?.recipient?.shipping_address_id,
);
if (recipientShippingIdOrError.isFailure) {
return this.handleValidationFailure(
recipientShippingIdOrError.error,
"Recipient shipping address ID not valid",
);
}
const recipientShippingId = recipientShippingIdOrError.object;
const recipientContact = await this.findContact(
recipientId,
recipientBillingId,
recipientShippingId,
);
if (!recipientContact) {
return this.handleValidationFailure(
new Error(`Recipient with ID ${recipientId.toString()} does not exist`),
);
}
// Crear invoice
const invoiceOrError = await this.tryUpdateInvoiceInstance(
invoiceDTO,
invoiceIdOrError.object,
//senderId,
//senderBillingId,
//senderShippingId,
recipientContact,
);
if (invoiceOrError.isFailure) {
const { error: domainError } = invoiceOrError;
let errorCode = "";
let message = "";
switch (domainError.code) {
case Invoice.ERROR_CUSTOMER_WITHOUT_NAME:
errorCode = UseCaseError.INVALID_INPUT_DATA;
message =
"El cliente debe ser una compañía o tener nombre y apellidos.";
break;
default:
errorCode = UseCaseError.UNEXCEPTED_ERROR;
message = "";
break;
}
return Result.fail<IUseCaseError>(
UseCaseError.create(errorCode, message, domainError),
);
}
return this.saveInvoice(invoiceOrError.object);
*/
}
private async tryUpdateInvoiceInstance(invoiceDTO, invoiceId, recipient) {
// Create invoice
let invoice_status = InvoiceStatus.create(invoiceDTO.status).object;
if (invoice_status.isEmpty()) {
invoice_status = InvoiceStatus.createDraft();
}
let invoice_series = InvoiceSeries.create(invoiceDTO.invoice_series).object;
if (invoice_series.isEmpty()) {
invoice_series = InvoiceSeries.create(invoiceDTO.invoice_series).object;
}
let issue_date = InvoiceDate.create(invoiceDTO.issue_date).object;
if (issue_date.isEmpty()) {
issue_date = InvoiceDate.createCurrentDate().object;
}
let operation_date = InvoiceDate.create(invoiceDTO.operation_date).object;
if (operation_date.isEmpty()) {
operation_date = InvoiceDate.createCurrentDate().object;
}
let invoiceCurrency = Currency.createFromCode(invoiceDTO.currency).object;
if (invoiceCurrency.isEmpty()) {
invoiceCurrency = Currency.createDefaultCode().object;
}
let invoiceLanguage = Language.createFromCode(invoiceDTO.language_code).object;
if (invoiceLanguage.isEmpty()) {
invoiceLanguage = Language.createDefaultCode().object;
}
const items = new Collection<InvoiceItem>(
invoiceDTO.items?.map(
(item) =>
InvoiceSimpleItem.create({
description: Description.create(item.description).object,
quantity: Quantity.create(item.quantity).object,
unitPrice: UnitPrice.create({
amount: item.unit_price.amount,
currencyCode: item.unit_price.currency,
precision: item.unit_price.precision,
}).object,
}).object
)
);
if (!invoice_status.isDraft()) {
throw Error("Error al crear una factura que no es borrador");
}
return DraftInvoice.create(
{
invoiceSeries: invoice_series,
issueDate: issue_date,
operationDate: operation_date,
invoiceCurrency,
language: invoiceLanguage,
invoiceNumber: InvoiceNumber.create(undefined).object,
//notes: Note.create(invoiceDTO.notes).object,
//senderId: UniqueID.create(null).object,
recipient,
items,
},
invoiceId
);
}
private async findContact(
contactId: UniqueID,
billingAddressId: UniqueID,
shippingAddressId: UniqueID
) {
const contactRepoBuilder = this.getRepository<IContactRepository>("Contact");
const contact = await contactRepoBuilder().getById2(
contactId,
billingAddressId,
shippingAddressId
);
return contact;
}
private async saveInvoice(invoice: DraftInvoice) {
const transaction = this._adapter.startTransaction();
const invoiceRepoBuilder = this.getRepository<IInvoiceRepository>("Invoice");
try {
await transaction.complete(async (t) => {
const invoiceRepo = invoiceRepoBuilder({ transaction: t });
await invoiceRepo.save(invoice);
});
return Result.ok<DraftInvoice>(invoice);
} catch (error: unknown) {
const _error = error as Error;
if (invoiceRepoBuilder().isRepositoryError(_error)) {
return this.handleRepositoryError(error as BaseError, invoiceRepoBuilder());
} else {
return this.handleUnexceptedError(error);
}
}
}
private handleUnexceptedError(error): Result<never, IUseCaseError> {
return Result.fail<IUseCaseError>(
UseCaseError.create(UseCaseError.UNEXCEPTED_ERROR, error.message, error)
);
}
private handleRepositoryError(
error: BaseError,
repository: IInvoiceRepository
): Result<never, IUseCaseError> {
const { message, details } = repository.handleRepositoryError(error);
return Result.fail<IUseCaseError>(
UseCaseError.create(UseCaseError.REPOSITORY_ERROR, message, details)
);
}
}

View File

@ -1 +0,0 @@
export * from "./invoice";

View File

@ -1,203 +0,0 @@
import { AggregateRoot, MoneyValue, UniqueID, UtcDate } from "core/common/domain";
import { Collection, Result } from "core/common/helpers";
import { InvoiceCustomer, InvoiceItem, InvoiceItems } from "../entities";
import { InvoiceNumber, InvoiceSerie, InvoiceStatus } from "../value-objects";
export interface IInvoiceProps {
invoiceNumber: InvoiceNumber;
invoiceSeries: InvoiceSerie;
status: InvoiceStatus;
issueDate: UtcDate;
operationDate: UtcDate;
//dueDate: UtcDate; // ? --> depende de la forma de pago
//tax: Tax; // ? --> detalles?
invoiceCurrency: string;
//language: Language;
//purchareOrderNumber: string;
//notes: Note;
//senderId: UniqueID;
//paymentInstructions: Note;
//paymentTerms: string;
customer?: InvoiceCustomer;
items?: InvoiceItems;
}
export interface IInvoice {
id: UniqueID;
invoiceNumber: InvoiceNumber;
invoiceSeries: InvoiceSerie;
status: InvoiceStatus;
issueDate: UtcDate;
operationDate: UtcDate;
//senderId: UniqueID;
customer?: InvoiceCustomer;
//dueDate
//tax: Tax;
//language: Language;
invoiceCurrency: string;
//purchareOrderNumber: string;
//notes: Note;
//paymentInstructions: Note;
//paymentTerms: string;
items: InvoiceItems;
calculateSubtotal: () => MoneyValue;
calculateTaxTotal: () => MoneyValue;
calculateTotal: () => MoneyValue;
}
export class Invoice extends AggregateRoot<IInvoiceProps> implements IInvoice {
private _items!: Collection<InvoiceItem>;
//protected _status: InvoiceStatus;
protected constructor(props: IInvoiceProps, id?: UniqueID) {
super(props, id);
this._items = props.items || InvoiceItems.create();
}
static create(props: IInvoiceProps, id?: UniqueID): Result<Invoice, Error> {
const invoice = new Invoice(props, id);
// Reglas de negocio / validaciones
// ...
// ...
// 🔹 Disparar evento de dominio "InvoiceAuthenticatedEvent"
//const { invoice } = props;
//user.addDomainEvent(new InvoiceAuthenticatedEvent(id, invoice.toString()));
return Result.ok(invoice);
}
get invoiceNumber() {
return this.props.invoiceNumber;
}
get invoiceSeries() {
return this.props.invoiceSeries;
}
get issueDate() {
return this.props.issueDate;
}
/*get senderId(): UniqueID {
return this.props.senderId;
}*/
get customer(): InvoiceCustomer | undefined {
return this.props.customer;
}
get operationDate() {
return this.props.operationDate;
}
/*get language() {
return this.props.language;
}*/
get dueDate() {
return undefined;
}
get tax() {
return undefined;
}
get status() {
return this.props.status;
}
get items() {
return this._items;
}
/*get purchareOrderNumber() {
return this.props.purchareOrderNumber;
}
get paymentInstructions() {
return this.props.paymentInstructions;
}
get paymentTerms() {
return this.props.paymentTerms;
}
get billTo() {
return this.props.billTo;
}
get shipTo() {
return this.props.shipTo;
}*/
get invoiceCurrency() {
return this.props.invoiceCurrency;
}
/*get notes() {
return this.props.notes;
}*/
// Method to get the complete list of line items
/*get lineItems(): InvoiceLineItem[] {
return this._lineItems;
}
addLineItem(lineItem: InvoiceLineItem, position?: number): void {
if (position === undefined) {
this._lineItems.push(lineItem);
} else {
this._lineItems.splice(position, 0, lineItem);
}
}*/
calculateSubtotal(): MoneyValue {
const invoiceSubtotal = MoneyValue.create({
amount: 0,
currency_code: this.props.invoiceCurrency,
scale: 2,
}).data;
return this._items.getAll().reduce((subtotal, item) => {
return subtotal.add(item.calculateTotal());
}, invoiceSubtotal);
}
// Method to calculate the total tax in the invoice
calculateTaxTotal(): MoneyValue {
const taxTotal = MoneyValue.create({
amount: 0,
currency_code: this.props.invoiceCurrency,
scale: 2,
}).data;
return taxTotal;
}
// Method to calculate the total invoice amount, including taxes
calculateTotal(): MoneyValue {
return this.calculateSubtotal().add(this.calculateTaxTotal());
}
}

View File

@ -1,2 +0,0 @@
export * from "./invoice-customer";
export * from "./invoice-items";

View File

@ -1,2 +0,0 @@
export * from "./invoice-address";
export * from "./invoice-customer";

View File

@ -1,78 +0,0 @@
import { EmailAddress, Name, PostalAddress, ValueObject } from "core/common/domain";
import { Result } from "core/common/helpers";
import { PhoneNumber } from "libphonenumber-js";
import { InvoiceAddressType } from "../../value-objects";
export interface IInvoiceAddressProps {
type: InvoiceAddressType;
title: Name;
address: PostalAddress;
email: EmailAddress;
phone: PhoneNumber;
}
export interface IInvoiceAddress {
type: InvoiceAddressType;
title: Name;
address: PostalAddress;
email: EmailAddress;
phone: PhoneNumber;
}
export class InvoiceAddress extends ValueObject<IInvoiceAddressProps> implements IInvoiceAddress {
public static create(props: IInvoiceAddressProps) {
return Result.ok(new this(props));
}
public static createShippingAddress(props: IInvoiceAddressProps) {
return Result.ok(
new this({
...props,
type: InvoiceAddressType.create("shipping").data,
})
);
}
public static createBillingAddress(props: IInvoiceAddressProps) {
return Result.ok(
new this({
...props,
type: InvoiceAddressType.create("billing").data,
})
);
}
get title(): Name {
return this.props.title;
}
get address(): PostalAddress {
return this.props.address;
}
get email(): EmailAddress {
return this.props.email;
}
get phone(): PhoneNumber {
return this.props.phone;
}
get type(): InvoiceAddressType {
return this.props.type;
}
getValue(): IInvoiceAddressProps {
return this.props;
}
toPrimitive() {
return {
type: this.type.toString(),
title: this.title.toString(),
address: this.address.toString(),
email: this.email.toString(),
phone: this.phone.toString(),
};
}
}

View File

@ -1,61 +0,0 @@
import { DomainEntity, Name, TINNumber, UniqueID } from "core/common/domain";
import { Result } from "core/common/helpers";
import { InvoiceAddress } from "./invoice-address";
export interface IInvoiceCustomerProps {
tin: TINNumber;
companyName: Name;
firstName: Name;
lastName: Name;
billingAddress?: InvoiceAddress;
shippingAddress?: InvoiceAddress;
}
export interface IInvoiceCustomer {
id: UniqueID;
tin: TINNumber;
companyName: Name;
firstName: Name;
lastName: Name;
billingAddress?: InvoiceAddress;
shippingAddress?: InvoiceAddress;
}
export class InvoiceCustomer
extends DomainEntity<IInvoiceCustomerProps>
implements IInvoiceCustomer
{
public static create(
props: IInvoiceCustomerProps,
id?: UniqueID
): Result<InvoiceCustomer, Error> {
const participant = new InvoiceCustomer(props, id);
return Result.ok<InvoiceCustomer>(participant);
}
get tin(): TINNumber {
return this.props.tin;
}
get companyName(): Name {
return this.props.companyName;
}
get firstName(): Name {
return this.props.firstName;
}
get lastName(): Name {
return this.props.lastName;
}
get billingAddress() {
return this.props.billingAddress;
}
get shippingAddress() {
return this.props.shippingAddress;
}
}

View File

@ -1,2 +0,0 @@
export * from "./invoice-item";
export * from "./invoice-items";

View File

@ -1,83 +0,0 @@
import { MoneyValue, Percentage, Quantity } from "core/common/domain";
import { InvoiceItemDescription } from "../../value-objects";
import { InvoiceItem } from "./invoice-item";
describe("InvoiceItem", () => {
it("debería calcular correctamente el subtotal (unitPrice * quantity)", () => {
const props = {
description: InvoiceItemDescription.create("Producto A"),
quantity: Quantity.create({ amount: 200, scale: 2 }),
unitPrice: MoneyValue.create(50),
discount: Percentage.create(0),
};
const result = InvoiceItem.create(props);
expect(result.isOk()).toBe(true);
const invoiceItem = result.unwrap();
expect(invoiceItem.subtotalPrice.value).toBe(100); // 50 * 2
});
it("debería calcular correctamente el total con descuento", () => {
const props = {
description: new InvoiceItemDescription("Producto B"),
quantity: new Quantity(3),
unitPrice: new MoneyValue(30),
discount: new Percentage(10), // 10%
};
const result = InvoiceItem.create(props);
expect(result.isOk()).toBe(true);
const invoiceItem = result.unwrap();
expect(invoiceItem.totalPrice.value).toBe(81); // (30 * 3) - 10% de (30 * 3)
});
it("debería devolver los valores correctos de las propiedades", () => {
const props = {
description: new InvoiceItemDescription("Producto C"),
quantity: new Quantity(1),
unitPrice: new MoneyValue(100),
discount: new Percentage(5),
};
const result = InvoiceItem.create(props);
expect(result.isOk()).toBe(true);
const invoiceItem = result.unwrap();
expect(invoiceItem.description.value).toBe("Producto C");
expect(invoiceItem.quantity.value).toBe(1);
expect(invoiceItem.unitPrice.value).toBe(100);
expect(invoiceItem.discount.value).toBe(5);
});
it("debería manejar correctamente un descuento del 0%", () => {
const props = {
description: new InvoiceItemDescription("Producto D"),
quantity: new Quantity(4),
unitPrice: new MoneyValue(25),
discount: new Percentage(0),
};
const result = InvoiceItem.create(props);
expect(result.isOk()).toBe(true);
const invoiceItem = result.unwrap();
expect(invoiceItem.totalPrice.value).toBe(100); // 25 * 4
});
it("debería manejar correctamente un descuento del 100%", () => {
const props = {
description: new InvoiceItemDescription("Producto E"),
quantity: new Quantity(2),
unitPrice: new MoneyValue(50),
discount: new Percentage(100),
};
const result = InvoiceItem.create(props);
expect(result.isOk()).toBe(true);
const invoiceItem = result.unwrap();
expect(invoiceItem.totalPrice.value).toBe(0); // (50 * 2) - 100% de (50 * 2)
});
});

View File

@ -1,94 +0,0 @@
import { DomainEntity, MoneyValue, Percentage, Quantity, UniqueID } from "core/common/domain";
import { Result } from "core/common/helpers";
import { InvoiceItemDescription } from "../../value-objects";
export interface IInvoiceItemProps {
description: InvoiceItemDescription;
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 IInvoiceItem {
id: UniqueID;
description: InvoiceItemDescription;
quantity: Quantity;
unitPrice: MoneyValue;
subtotalPrice: MoneyValue;
discount: Percentage;
totalPrice: MoneyValue;
}
export class InvoiceItem extends DomainEntity<IInvoiceItemProps> implements IInvoiceItem {
private _subtotalPrice!: MoneyValue;
private _totalPrice!: MoneyValue;
public static create(props: IInvoiceItemProps, id?: UniqueID): Result<InvoiceItem, Error> {
const item = new InvoiceItem(props, id);
// Reglas de negocio / validaciones
// ...
// ...
// 🔹 Disparar evento de dominio "InvoiceItemCreatedEvent"
//const { invoice } = props;
//user.addDomainEvent(new InvoiceAuthenticatedEvent(id, invoice.toString()));
return Result.ok(item);
}
get description(): InvoiceItemDescription {
return this.props.description;
}
get quantity(): Quantity {
return this.props.quantity;
}
get unitPrice(): MoneyValue {
return this.props.unitPrice;
}
get subtotalPrice(): MoneyValue {
if (!this._subtotalPrice) {
this._subtotalPrice = this.calculateSubtotal();
}
return this._subtotalPrice;
}
get discount(): Percentage {
return this.props.discount;
}
get totalPrice(): MoneyValue {
if (!this._totalPrice) {
this._totalPrice = this.calculateTotal();
}
return this._totalPrice;
}
getValue() {
return this.props;
}
toPrimitive() {
return {
description: this.description.toPrimitive(),
quantity: this.quantity.toPrimitive(),
unit_price: this.unitPrice.toPrimitive(),
subtotal_price: this.subtotalPrice.toPrimitive(),
discount: this.discount.toPrimitive(),
total_price: this.totalPrice.toPrimitive(),
};
}
calculateSubtotal(): MoneyValue {
return this.unitPrice.multiply(this.quantity.toNumber()); // Precio unitario * Cantidad
}
calculateTotal(): MoneyValue {
return this.subtotalPrice.subtract(this.subtotalPrice.percentage(this.discount.toNumber()));
}
}

View File

@ -1,8 +0,0 @@
import { Collection } from "core/common/helpers";
import { InvoiceItem } from "./invoice-item";
export class InvoiceItems extends Collection<InvoiceItem> {
public static create(items?: InvoiceItem[]): InvoiceItems {
return new InvoiceItems(items);
}
}

View File

@ -1,5 +0,0 @@
export * from "./aggregates";
export * from "./entities";
export * from "./repositories";
export * from "./services";
export * from "./value-objects";

View File

@ -1 +0,0 @@
export * from "./invoice-repository.interface";

View File

@ -1,12 +0,0 @@
import { UniqueID } from "core/common/domain";
import { Collection, Result } from "core/common/helpers";
import { Invoice } from "../aggregates";
export interface IInvoiceRepository {
findAll(transaction?: any): Promise<Result<Collection<Invoice>, Error>>;
getById(id: UniqueID, transaction?: any): Promise<Result<Invoice, Error>>;
deleteByIdInCompany(id: UniqueID, transaction?: any): Promise<Result<boolean, Error>>;
create(invoice: Invoice, transaction?: any): Promise<void>;
update(invoice: Invoice, transaction?: any): Promise<void>;
}

View File

@ -1,2 +0,0 @@
export * from "./invoice-service.interface";
export * from "./invoice.service";

View File

@ -1,22 +0,0 @@
import { UniqueID } from "core/common/domain";
import { Collection, Result } from "core/common/helpers";
import { IInvoiceProps, Invoice } from "../aggregates";
export interface IInvoiceService {
findInvoices(transaction?: any): Promise<Result<Collection<Invoice>, Error>>;
findInvoiceById(invoiceId: UniqueID, transaction?: any): Promise<Result<Invoice>>;
updateInvoiceById(
invoiceId: UniqueID,
data: Partial<IInvoiceProps>,
transaction?: any
): Promise<Result<Invoice, Error>>;
createInvoice(
invoiceId: UniqueID,
data: IInvoiceProps,
transaction?: any
): Promise<Result<Invoice, Error>>;
deleteInvoiceById(invoiceId: UniqueID, transaction?: any): Promise<Result<boolean, Error>>;
}

View File

@ -1,84 +0,0 @@
import { UniqueID } from "core/common/domain";
import { Collection, Result } from "core/common/helpers";
import { Transaction } from "sequelize";
import { IInvoiceProps, Invoice } from "../aggregates";
import { IInvoiceRepository } from "../repositories";
import { IInvoiceService } from "./invoice-service.interface";
export class InvoiceService implements IInvoiceService {
constructor(private readonly repo: IInvoiceRepository) {}
deleteInvoiceById(invoiceId: UniqueID, transaction?: any): Promise<Result<boolean, Error>> {
throw new Error("Method not implemented.");
}
async findInvoices(transaction?: Transaction): Promise<Result<Collection<Invoice>, Error>> {
const invoicesOrError = await this.repo.findAll(transaction);
if (invoicesOrError.isFailure) {
return Result.fail(invoicesOrError.error);
}
// Solo devolver usuarios activos
//const allInvoices = invoicesOrError.data.filter((invoice) => invoice.isActive);
//return Result.ok(new Collection(allInvoices));
return invoicesOrError;
}
async findInvoiceById(invoiceId: UniqueID, transaction?: Transaction): Promise<Result<Invoice>> {
return await this.repo.getById(invoiceId, transaction);
}
async updateInvoiceById(
invoiceId: UniqueID,
data: Partial<IInvoiceProps>,
transaction?: Transaction
): Promise<Result<Invoice, Error>> {
// Verificar si la factura existe
const invoiceOrError = await this.repo.getById(invoiceId, transaction);
if (invoiceOrError.isFailure) {
return Result.fail(new Error("Invoice not found"));
}
const updatedInvoiceOrError = Invoice.update(invoiceOrError.data, data);
if (updatedInvoiceOrError.isFailure) {
return Result.fail(
new Error(`Error updating invoice: ${updatedInvoiceOrError.error.message}`)
);
}
const updateInvoice = updatedInvoiceOrError.data;
await this.repo.update(updateInvoice, transaction);
return Result.ok(updateInvoice);
}
async createInvoice(
invoiceId: UniqueID,
data: IInvoiceProps,
transaction?: Transaction
): Promise<Result<Invoice, Error>> {
// Verificar si la factura existe
const invoiceOrError = await this.repo.getById(invoiceId, transaction);
if (invoiceOrError.isSuccess) {
return Result.fail(new Error("Invoice exists"));
}
const newInvoiceOrError = Invoice.create(data, invoiceId);
if (newInvoiceOrError.isFailure) {
return Result.fail(new Error(`Error creating invoice: ${newInvoiceOrError.error.message}`));
}
const newInvoice = newInvoiceOrError.data;
await this.repo.create(newInvoice, transaction);
return Result.ok(newInvoice);
}
async deleteInvoiceByIdInCompnay(
companyId: UniqueID,
invoiceId: UniqueID,
transaction?: Transaction
): Promise<Result<boolean, Error>> {
return this.repo.deleteByIdInCompany(companyId, invoiceId, transaction);
}
}

View File

@ -1,5 +0,0 @@
export * from "./invoice-address-type";
export * from "./invoice-item-description";
export * from "./invoice-number";
export * from "./invoice-serie";
export * from "./invoice-status";

View File

@ -1,38 +0,0 @@
import { ValueObject } from "core/common/domain";
import { Result } from "core/common/helpers";
interface IInvoiceAddressTypeProps {
value: string;
}
export enum INVOICE_ADDRESS_TYPE {
SHIPPING = "shipping",
BILLING = "billing",
}
export class InvoiceAddressType extends ValueObject<IInvoiceAddressTypeProps> {
private static readonly ALLOWED_TYPES = ["shipping", "billing"];
static create(value: string): Result<InvoiceAddressType, Error> {
if (!this.ALLOWED_TYPES.includes(value)) {
return Result.fail(
new Error(
`Invalid address type: ${value}. Allowed types are: ${this.ALLOWED_TYPES.join(", ")}`
)
);
}
return Result.ok(new InvoiceAddressType({ value }));
}
getValue(): string {
return this.props.value;
}
toString(): string {
return this.getValue();
}
toPrimitive(): string {
return this.getValue();
}
}

View File

@ -1,50 +0,0 @@
import { ValueObject } from "core/common/domain";
import { Maybe, Result } from "core/common/helpers";
import * as z from "zod/v4";
interface IInvoiceItemDescriptionProps {
value: string;
}
export class InvoiceItemDescription extends ValueObject<IInvoiceItemDescriptionProps> {
private static readonly MAX_LENGTH = 255;
protected static validate(value: string) {
const schema = z
.string()
.trim()
.max(InvoiceItemDescription.MAX_LENGTH, {
message: `Description must be at most ${InvoiceItemDescription.MAX_LENGTH} characters long`,
});
return schema.safeParse(value);
}
static create(value: string) {
const valueIsValid = InvoiceItemDescription.validate(value);
if (!valueIsValid.success) {
return Result.fail(new Error(valueIsValid.error.issues[0].message));
}
return Result.ok(new InvoiceItemDescription({ value }));
}
static createNullable(value?: string): Result<Maybe<InvoiceItemDescription>, Error> {
if (!value || value.trim() === "") {
return Result.ok(Maybe.none<InvoiceItemDescription>());
}
return InvoiceItemDescription.create(value).map((value) => Maybe.some(value));
}
getValue(): string {
return this.props.value;
}
toString(): string {
return this.getValue();
}
toPrimitive() {
return this.getValue();
}
}

View File

@ -1,42 +0,0 @@
import { ValueObject } from "core/common/domain";
import { Result } from "core/common/helpers";
import * as z from "zod/v4";
interface IInvoiceNumberProps {
value: string;
}
export class InvoiceNumber extends ValueObject<IInvoiceNumberProps> {
private static readonly MAX_LENGTH = 255;
protected static validate(value: string) {
const schema = z
.string()
.trim()
.max(InvoiceNumber.MAX_LENGTH, {
message: `Name must be at most ${InvoiceNumber.MAX_LENGTH} characters long`,
});
return schema.safeParse(value);
}
static create(value: string) {
const valueIsValid = InvoiceNumber.validate(value);
if (!valueIsValid.success) {
return Result.fail(new Error(valueIsValid.error.issues[0].message));
}
return Result.ok(new InvoiceNumber({ value }));
}
getValue(): string {
return this.props.value;
}
toString(): string {
return this.getValue();
}
toPrimitive() {
return this.getValue();
}
}

View File

@ -1,50 +0,0 @@
import { ValueObject } from "core/common/domain";
import { Maybe, Result } from "core/common/helpers";
import * as z from "zod/v4";
interface IInvoiceSerieProps {
value: string;
}
export class InvoiceSerie extends ValueObject<IInvoiceSerieProps> {
private static readonly MAX_LENGTH = 255;
protected static validate(value: string) {
const schema = z
.string()
.trim()
.max(InvoiceSerie.MAX_LENGTH, {
message: `Name must be at most ${InvoiceSerie.MAX_LENGTH} characters long`,
});
return schema.safeParse(value);
}
static create(value: string) {
const valueIsValid = InvoiceSerie.validate(value);
if (!valueIsValid.success) {
return Result.fail(new Error(valueIsValid.error.issues[0].message));
}
return Result.ok(new InvoiceSerie({ value }));
}
static createNullable(value?: string): Result<Maybe<InvoiceSerie>, Error> {
if (!value || value.trim() === "") {
return Result.ok(Maybe.none<InvoiceSerie>());
}
return InvoiceSerie.create(value).map((value) => Maybe.some(value));
}
getValue(): string {
return this.props.value;
}
toString(): string {
return this.getValue();
}
toPrimitive() {
return this.getValue();
}
}

View File

@ -1,80 +0,0 @@
import { ValueObject } from "core/common/domain";
import { Result } from "core/common/helpers";
interface IInvoiceStatusProps {
value: string;
}
export enum INVOICE_STATUS {
DRAFT = "draft",
EMITTED = "emitted",
SENT = "sent",
REJECTED = "rejected",
}
export class InvoiceStatus extends ValueObject<IInvoiceStatusProps> {
private static readonly ALLOWED_STATUSES = ["draft", "emitted", "sent", "rejected"];
private static readonly TRANSITIONS: Record<string, string[]> = {
draft: [INVOICE_STATUS.EMITTED],
emitted: [INVOICE_STATUS.SENT, INVOICE_STATUS.REJECTED, INVOICE_STATUS.DRAFT],
sent: [INVOICE_STATUS.REJECTED],
rejected: [],
};
static create(value: string): Result<InvoiceStatus, Error> {
if (!this.ALLOWED_STATUSES.includes(value)) {
return Result.fail(new Error(`Estado de la factura no válido: ${value}`));
}
return Result.ok(
value === "rejected"
? InvoiceStatus.createRejected()
: value === "sent"
? InvoiceStatus.createSent()
: value === "emitted"
? InvoiceStatus.createSent()
: InvoiceStatus.createDraft()
);
}
public static createDraft(): InvoiceStatus {
return new InvoiceStatus({ value: INVOICE_STATUS.DRAFT });
}
public static createEmitted(): InvoiceStatus {
return new InvoiceStatus({ value: INVOICE_STATUS.EMITTED });
}
public static createSent(): InvoiceStatus {
return new InvoiceStatus({ value: INVOICE_STATUS.SENT });
}
public static createRejected(): InvoiceStatus {
return new InvoiceStatus({ value: INVOICE_STATUS.REJECTED });
}
getValue(): string {
return this.props.value;
}
toPrimitive() {
return this.getValue();
}
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();
}
}

View File

@ -1,25 +0,0 @@
/* import { getService } from "@apps/server/src/core/service-registry"; */
import { IPackageServer } from "@packages/package";
import { invoicesRouter } from "./intrastructure";
import initInvoiceModel from "./intrastructure/sequelize/invoice.model";
export const InvoicesPackage: IPackageServer = {
metadata: {
name: "invoices",
version: "1.0.0",
dependencies: ["contacts"],
},
init(app) {
// const contacts = getService<ContactsService>("contacts");
invoicesRouter(app);
},
registerDependencies() {
return {
models: [(sequelize) => initInvoiceModel(sequelize)],
services: {
getInvoice: () => {},
/*...*/
},
};
},
};

View File

@ -1,77 +0,0 @@
import {
ISequelizeAdapter,
SequelizeRepository,
} from "@/contexts/common/infrastructure/sequelize";
import { UniqueID } from "@shared/contexts";
import { Transaction } from "sequelize";
import { Contact, IContactRepository } from "../domain/Contact";
import { IContactMapper } from "./mappers/contact.mapper";
export class ContactRepository
extends SequelizeRepository<Contact>
implements IContactRepository
{
protected mapper: IContactMapper;
public constructor(props: {
mapper: IContactMapper;
adapter: ISequelizeAdapter;
transaction: Transaction;
}) {
const { adapter, mapper, transaction } = props;
super({ adapter, transaction });
this.mapper = mapper;
}
public async getById2(
id: UniqueID,
billingAddressId: UniqueID,
shippingAddressId: UniqueID,
) {
const Contact_Model = this.adapter.getModel("Contact_Model");
const ContactAddress_Model = this.adapter.getModel("ContactAddress_Model");
const rawContact: any = await Contact_Model.findOne({
where: { id: id.toString() },
include: [
{
model: ContactAddress_Model,
as: "billingAddress",
where: {
id: billingAddressId.toString(),
},
},
{
model: ContactAddress_Model,
as: "shippingAddress",
where: {
id: shippingAddressId.toString(),
},
},
],
transaction: this.transaction,
});
if (!rawContact === true) {
return null;
}
return this.mapper.mapToDomain(rawContact);
}
public async getById(id: UniqueID): Promise<Contact | null> {
const rawContact: any = await this._getById("Contact_Model", id, {
include: [{ all: true }],
});
if (!rawContact === true) {
return null;
}
return this.mapper.mapToDomain(rawContact);
}
public async exists(id: UniqueID): Promise<boolean> {
return this._exists("Customer", "id", id.toString());
}
}

View File

@ -1,87 +0,0 @@
import { SequelizeRepository } from "@/contexts/common/infrastructure/sequelize/SequelizeRepository";
import { ISequelizeAdapter } from "@/contexts/common/infrastructure/sequelize";
import { ICollection, IQueryCriteria, UniqueID } from "@shared/contexts";
import { Transaction } from "sequelize";
import { IInvoiceRepository, Invoice } from "../domain";
import { IInvoiceMapper } from "./mappers";
export type QueryParams = {
pagination: Record<string, unknown>;
filters: Record<string, unknown>;
};
export class InvoiceRepository extends SequelizeRepository<Invoice> implements IInvoiceRepository {
protected mapper: IInvoiceMapper;
public constructor(props: {
mapper: IInvoiceMapper;
adapter: ISequelizeAdapter;
transaction: Transaction;
}) {
const { adapter, mapper, transaction } = props;
super({ adapter, transaction });
this.mapper = mapper;
}
public async getById(id: UniqueID): Promise<Invoice | null> {
const rawContact: any = await this._getById("Invoice_Model", id, {
include: [
{ association: "items" },
{
association: "participants",
include: [{ association: "shippingAddress" }, { association: "billingAddress" }],
},
],
});
if (!rawContact === true) {
return null;
}
return this.mapper.mapToDomain(rawContact);
}
public async findAll(queryCriteria?: IQueryCriteria): Promise<ICollection<Invoice>> {
const { rows, count } = await this._findAll("Invoice_Model", queryCriteria, {
include: [
{
association: "participants",
separate: true,
},
],
});
return this.mapper.mapArrayAndCountToDomain(rows, count);
}
public async save(invoice: Invoice): Promise<void> {
const { items, participants, ...invoiceData } = this.mapper.mapToPersistence(invoice);
await this.adapter
.getModel("Invoice_Model")
.create(invoiceData, { transaction: this.transaction });
await this.adapter
.getModel("InvoiceItem_Model")
.bulkCreate(items, { transaction: this.transaction });
await this.adapter
.getModel("InvoiceParticipant_Model")
.bulkCreate(participants, { transaction: this.transaction });
await this.adapter
.getModel("InvoiceParticipantAddress_Model")
.bulkCreate([participants[0].billingAddress, participants[0].shippingAddress], {
transaction: this.transaction,
});
}
public removeById(id: UniqueID): Promise<void> {
return this._removeById("Invoice_Model", id);
}
public async exists(id: UniqueID): Promise<boolean> {
return this._exists("Invoice_Model", "id", id.toString());
}
}

View File

@ -1,54 +0,0 @@
import { ISequelizeAdapter, SequelizeRepository } from "@/contexts/common/infrastructure/sequelize";
import { Transaction } from "sequelize";
import { InvoiceCustomer } from "../domain";
import { IInvoiceParticipantMapper } from "./mappers";
export class InvoiceParticipantRepository extends SequelizeRepository<InvoiceCustomer> {
protected mapper: IInvoiceParticipantMapper;
public constructor(props: {
mapper: IInvoiceParticipantMapper;
adapter: ISequelizeAdapter;
transaction: Transaction;
}) {
const { adapter, mapper, transaction } = props;
super({ adapter, transaction });
this.mapper = mapper;
}
/*public async getParticipantById(
id: UniqueID,
): Promise<InvoiceParticipant | null> {
const rawParticipant: any = await this._getById(
"InvoiceParticipant_Model",
id,
{
include: [{ all: true }],
raw: true,
},
);
if (!rawParticipant === true) {
return null;
}
return this.mapper.mapToDomain(rawParticipant);
}
public async getContactById(id: UniqueID): Promise<any | null> {
const rawContact: any = await this._getById("Customer", id, {
include: [{ all: true }],
raw: true,
});
if (!rawContact === true) {
return null;
}
return this.mapper.mapToDomain(rawContact);
}
public async exists(id: UniqueID): Promise<boolean> {
return this._exists("Customer", "id", id.toString());
}*/
}

View File

@ -1,44 +0,0 @@
import {
ISequelizeAdapter,
SequelizeRepository,
} from "@/contexts/common/infrastructure/sequelize";
import { UniqueID } from "@shared/contexts";
import { Transaction } from "sequelize";
import { InvoiceParticipantAddress } from "../domain";
import { IInvoiceParticipantAddressMapper } from "./mappers";
export class InvoiceParticipantAddressRepository extends SequelizeRepository<InvoiceParticipantAddress> {
protected mapper: IInvoiceParticipantAddressMapper;
public constructor(props: {
mapper: IInvoiceParticipantAddressMapper;
adapter: ISequelizeAdapter;
transaction: Transaction;
}) {
const { adapter, mapper, transaction } = props;
super({ adapter, transaction });
this.mapper = mapper;
}
public async getById(
id: UniqueID,
): Promise<InvoiceParticipantAddress | null> {
const rawParticipant: any = await this._getById(
"InvoiceParticipantAddress_Model",
id,
{
include: [{ all: true }],
},
);
if (!rawParticipant === true) {
return null;
}
return this.mapper.mapToDomain(rawParticipant);
}
public async exists(id: UniqueID): Promise<boolean> {
return this._exists("CustomerAddress", "id", id.toString());
}
}

View File

@ -1,43 +0,0 @@
import {
IRepositoryManager,
RepositoryManager,
} from "@/contexts/common/domain";
import {
ISequelizeAdapter,
createSequelizeAdapter,
} from "@/contexts/common/infrastructure/sequelize";
import { InvoicingServices, TInvoicingServices } from "../application";
export interface IInvoicingContext {
adapter: ISequelizeAdapter;
repositoryManager: IRepositoryManager;
services: TInvoicingServices;
}
class InvoicingContext {
private static instance: InvoicingContext | null = null;
public static getInstance(): InvoicingContext {
if (!InvoicingContext.instance) {
InvoicingContext.instance = new InvoicingContext();
}
return InvoicingContext.instance;
}
private context: IInvoicingContext;
private constructor() {
this.context = {
adapter: createSequelizeAdapter(),
repositoryManager: RepositoryManager.getInstance(),
services: InvoicingServices,
};
}
public getContext(): IInvoicingContext {
return this.context;
}
}
const sharedInvoicingContext = InvoicingContext.getInstance().getContext();
export { sharedInvoicingContext };

View File

@ -1 +0,0 @@
export * from "./invoices.routes";

View File

@ -1,65 +0,0 @@
import { validateAndParseBody } from "@repo/shared";
import { Express } from "express";
import {
buildCreateInvoiceController,
buildGetInvoiceController,
buildListInvoicesController,
ICreateInvoiceRequestSchema,
} from "../../presentation";
import { NextFunction, Request, Response, Router } from "express";
export const invoicesRouter = (app: Express) => {
const routes: Router = Router({ mergeParams: true });
routes.get(
"/",
//checkTabContext,
//checkUser,
(req: Request, res: Response, next: NextFunction) => {
buildListInvoicesController().execute(req, res, next);
}
);
routes.get(
"/:invoiceId",
//checkTabContext,
//checkUser,
(req: Request, res: Response, next: NextFunction) => {
buildGetInvoiceController().execute(req, res, next);
}
);
routes.post(
"/",
validateAndParseBody(ICreateInvoiceRequestSchema, { sanitize: false }),
//checkTabContext,
//checkUser,
(req: Request, res: Response, next: NextFunction) => {
buildCreateInvoiceController().execute(req, res, next);
}
);
/*
routes.put(
"/:invoiceId",
validateAndParseBody(IUpdateInvoiceRequestSchema),
checkTabContext,
//checkUser,
(req: Request, res: Response, next: NextFunction) => {
buildUpdateInvoiceController().execute(req, res, next);
}
);
routes.delete(
"/:invoiceId",
validateAndParseBody(IDeleteInvoiceRequestSchema),
checkTabContext,
//checkUser,
(req: Request, res: Response, next: NextFunction) => {
buildDeleteInvoiceController().execute(req, res, next);
}
);*/
app.use("/invoices", routes);
};

View File

@ -1,3 +0,0 @@
export * from "./express";
export * from "./mappers";
export * from "./sequelize";

View File

@ -1,63 +0,0 @@
import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure";
import { Name, TINNumber, UniqueID } from "@shared/contexts";
import { Contact, IContactProps } from "../../domain";
import { IInvoicingContext } from "../InvoicingContext";
import { Contact_Model, TCreationContact_Model } from "../sequelize/contact.mo.del";
import { IContactAddressMapper, createContactAddressMapper } from "./contactAddress.mapper";
export interface IContactMapper
extends ISequelizeMapper<Contact_Model, TCreationContact_Model, Contact> {}
class ContactMapper
extends SequelizeMapper<Contact_Model, TCreationContact_Model, Contact>
implements IContactMapper
{
public constructor(props: { addressMapper: IContactAddressMapper; context: IInvoicingContext }) {
super(props);
}
protected toDomainMappingImpl(source: Contact_Model, params: any): Contact {
if (!source.billingAddress) {
this.handleRequiredFieldError(
"billingAddress",
new Error("Missing participant's billing address")
);
}
if (!source.shippingAddress) {
this.handleRequiredFieldError(
"shippingAddress",
new Error("Missing participant's shipping address")
);
}
const billingAddress = this.props.addressMapper.mapToDomain(source.billingAddress!, params);
const shippingAddress = this.props.addressMapper.mapToDomain(source.shippingAddress!, params);
const props: IContactProps = {
tin: this.mapsValue(source, "tin", TINNumber.create),
firstName: this.mapsValue(source, "first_name", Name.create),
lastName: this.mapsValue(source, "last_name", Name.create),
companyName: this.mapsValue(source, "company_name", Name.create),
billingAddress,
shippingAddress,
};
const id = this.mapsValue(source, "id", UniqueID.create);
const contactOrError = Contact.create(props, id);
if (contactOrError.isFailure) {
throw contactOrError.error;
}
return contactOrError.object;
}
}
export const createContactMapper = (context: IInvoicingContext): IContactMapper =>
new ContactMapper({
addressMapper: createContactAddressMapper(context),
context,
});

View File

@ -1,65 +0,0 @@
import {
ISequelizeMapper,
SequelizeMapper,
} from "@/contexts/common/infrastructure";
import {
City,
Country,
Email,
Note,
Phone,
PostalCode,
Province,
Street,
UniqueID,
} from "@shared/contexts";
import { ContactAddress, IContactAddressProps } from "../../domain";
import { IInvoicingContext } from "../InvoicingContext";
import {
ContactAddress_Model,
TCreationContactAddress_Attributes,
} from "../sequelize";
export interface IContactAddressMapper
extends ISequelizeMapper<
ContactAddress_Model,
TCreationContactAddress_Attributes,
ContactAddress
> {}
export const createContactAddressMapper = (
context: IInvoicingContext
): IContactAddressMapper => new ContactAddressMapper({ context });
class ContactAddressMapper
extends SequelizeMapper<
ContactAddress_Model,
TCreationContactAddress_Attributes,
ContactAddress
>
implements IContactAddressMapper
{
protected toDomainMappingImpl(source: ContactAddress_Model, params: any) {
const id = this.mapsValue(source, "id", UniqueID.create);
const props: IContactAddressProps = {
type: source.type,
street: this.mapsValue(source, "street", Street.create),
city: this.mapsValue(source, "city", City.create),
province: this.mapsValue(source, "province", Province.create),
postalCode: this.mapsValue(source, "postal_code", PostalCode.create),
country: this.mapsValue(source, "country", Country.create),
email: this.mapsValue(source, "email", Email.create),
phone: this.mapsValue(source, "phone", Phone.create),
notes: this.mapsValue(source, "notes", Note.create),
};
const addressOrError = ContactAddress.create(props, id);
if (addressOrError.isFailure) {
throw addressOrError.error;
}
return addressOrError.object;
}
}

View File

@ -1 +0,0 @@
export * from "./invoice.mapper";

View File

@ -1,104 +0,0 @@
import { Invoice, InvoiceItem, InvoiceItemDescription } from "@contexts/invoices/domain/";
import { MoneyValue, Percentage, Quantity, UniqueID } from "core/common/domain";
import { Result } from "core/common/helpers";
import {
ISequelizeMapper,
MapperParamsType,
SequelizeMapper,
} from "core/common/infrastructure/sequelize/sequelize-mapper";
import { InferCreationAttributes } from "sequelize";
import { InvoiceItemCreationAttributes, InvoiceItemModel, InvoiceModel } from "../sequelize";
export interface IInvoiceItemMapper
extends ISequelizeMapper<InvoiceItemModel, InvoiceItemCreationAttributes, InvoiceItem> {}
export class InvoiceItemMapper
extends SequelizeMapper<InvoiceItemModel, InvoiceItemCreationAttributes, InvoiceItem>
implements IInvoiceItemMapper
{
public mapToDomain(
source: InvoiceItemModel,
params?: MapperParamsType
): Result<InvoiceItem, Error> {
const { sourceParent } = params as { sourceParent: InvoiceModel };
const idOrError = UniqueID.create(source.item_id);
const descriptionOrError = InvoiceItemDescription.create(source.description);
const quantityOrError = Quantity.create({
amount: source.quantity_amount,
scale: source.quantity_scale,
});
const unitPriceOrError = MoneyValue.create({
amount: source.unit_price_amount,
scale: source.unit_price_scale,
currency_code: sourceParent.invoice_currency,
});
const discountOrError = Percentage.create({
amount: source.discount_amount,
scale: source.discount_scale,
});
const result = Result.combine([
idOrError,
descriptionOrError,
quantityOrError,
unitPriceOrError,
discountOrError,
]);
if (result.isFailure) {
return Result.fail(result.error);
}
return InvoiceItem.create(
{
description: descriptionOrError.data,
quantity: quantityOrError.data,
unitPrice: unitPriceOrError.data,
discount: discountOrError.data,
},
idOrError.data
//sourceParent
);
}
public mapToPersistence(
source: InvoiceItem,
params?: MapperParamsType
): InferCreationAttributes<InvoiceItemModel, {}> {
const { index, sourceParent } = params as {
index: number;
sourceParent: Invoice;
};
const lineData = {
parent_id: undefined,
invoice_id: sourceParent.id.toPrimitive(),
item_type: "simple",
position: index,
item_id: source.id.toPrimitive(),
description: source.description.toPrimitive(),
quantity_amount: source.quantity.toPrimitive().amount,
quantity_scale: source.quantity.toPrimitive().scale,
unit_price_amount: source.unitPrice.toPrimitive().amount,
unit_price_scale: source.unitPrice.toPrimitive().scale,
subtotal_amount: source.subtotalPrice.toPrimitive().amount,
subtotal_scale: source.subtotalPrice.toPrimitive().scale,
discount_amount: source.discount.toPrimitive().amount,
discount_scale: source.discount.toPrimitive().scale,
total_amount: source.totalPrice.toPrimitive().amount,
total_scale: source.totalPrice.toPrimitive().scale,
};
return lineData;
}
}

View File

@ -1,101 +0,0 @@
import { Invoice, InvoiceNumber, InvoiceSerie, InvoiceStatus } from "@contexts/invoices/domain/";
import { UniqueID, UtcDate } from "core/common/domain";
import { Result } from "core/common/helpers";
import {
ISequelizeMapper,
MapperParamsType,
SequelizeMapper,
} from "core/common/infrastructure/sequelize/sequelize-mapper";
import { InvoiceCreationAttributes, InvoiceModel } from "../sequelize";
import { InvoiceItemMapper } from "./invoice-item.mapper"; // Importar el mapper de items
export interface IInvoiceMapper
extends ISequelizeMapper<InvoiceModel, InvoiceCreationAttributes, Invoice> {}
export class InvoiceMapper
extends SequelizeMapper<InvoiceModel, InvoiceCreationAttributes, Invoice>
implements IInvoiceMapper
{
private invoiceItemMapper: InvoiceItemMapper;
constructor() {
super();
this.invoiceItemMapper = new InvoiceItemMapper(); // Instanciar el mapper de items
}
public mapToDomain(source: InvoiceModel, params?: MapperParamsType): Result<Invoice, Error> {
const idOrError = UniqueID.create(source.id);
const statusOrError = InvoiceStatus.create(source.invoice_status);
const invoiceSeriesOrError = InvoiceSerie.create(source.invoice_series);
const invoiceNumberOrError = InvoiceNumber.create(source.invoice_number);
const issueDateOrError = UtcDate.create(source.issue_date);
const operationDateOrError = UtcDate.create(source.operation_date);
const result = Result.combine([
idOrError,
statusOrError,
invoiceSeriesOrError,
invoiceNumberOrError,
issueDateOrError,
operationDateOrError,
]);
if (result.isFailure) {
return Result.fail(result.error);
}
// Mapear los items de la factura
const itemsOrErrors = this.invoiceItemMapper.mapArrayToDomain(source.items, {
sourceParent: source,
...params,
});
if (itemsOrErrors.isFailure) {
return Result.fail(itemsOrErrors.error);
}
const invoiceCurrency = source.invoice_currency || "EUR";
return Invoice.create(
{
status: statusOrError.data,
invoiceSeries: invoiceSeriesOrError.data,
invoiceNumber: invoiceNumberOrError.data,
issueDate: issueDateOrError.data,
operationDate: operationDateOrError.data,
invoiceCurrency,
items: itemsOrErrors.data,
},
idOrError.data
);
}
public mapToPersistence(source: Invoice, params?: MapperParamsType): InvoiceCreationAttributes {
const subtotal = source.calculateSubtotal();
const total = source.calculateTotal();
const items = this.invoiceItemMapper.mapCollectionToPersistence(source.items, params);
return {
id: source.id.toString(),
invoice_status: source.status.toPrimitive(),
invoice_series: source.invoiceSeries.toPrimitive(),
invoice_number: source.invoiceNumber.toPrimitive(),
issue_date: source.issueDate.toPrimitive(),
operation_date: source.operationDate.toPrimitive(),
invoice_language: "es",
invoice_currency: source.invoiceCurrency || "EUR",
subtotal_amount: subtotal.amount,
subtotal_scale: subtotal.scale,
total_amount: total.amount,
total_scale: total.scale,
items,
};
}
}
const invoiceMapper: InvoiceMapper = new InvoiceMapper();
export { invoiceMapper };

View File

@ -1,119 +0,0 @@
import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure";
import { Name, TINNumber, UniqueID } from "@shared/contexts";
import {
IInvoiceCustomerProps,
Invoice,
InvoiceCustomer,
InvoiceParticipantBillingAddress,
InvoiceParticipantShippingAddress,
} from "../../domain";
import { IInvoicingContext } from "../InvoicingContext";
import { InvoiceParticipant_Model, TCreationInvoiceParticipant_Model } from "../sequelize";
import {
IInvoiceParticipantAddressMapper,
createInvoiceParticipantAddressMapper,
} from "./invoiceParticipantAddress.mapper";
export interface IInvoiceParticipantMapper
extends ISequelizeMapper<
InvoiceParticipant_Model,
TCreationInvoiceParticipant_Model,
InvoiceCustomer
> {}
export const createInvoiceParticipantMapper = (
context: IInvoicingContext
): IInvoiceParticipantMapper =>
new InvoiceParticipantMapper({
context,
addressMapper: createInvoiceParticipantAddressMapper(context),
});
class InvoiceParticipantMapper
extends SequelizeMapper<
InvoiceParticipant_Model,
TCreationInvoiceParticipant_Model,
InvoiceCustomer
>
implements IInvoiceParticipantMapper
{
public constructor(props: {
addressMapper: IInvoiceParticipantAddressMapper;
context: IInvoicingContext;
}) {
super(props);
}
protected toDomainMappingImpl(source: InvoiceParticipant_Model, params: any) {
/*if (!source.billingAddress) {
this.handleRequiredFieldError(
"billingAddress",
new Error("Missing participant's billing address"),
);
}
if (!source.shippingAddress) {
this.handleRequiredFieldError(
"shippingAddress",
new Error("Missing participant's shipping address"),
);
}
*/
const billingAddress = source.billingAddress
? ((this.props.addressMapper as IInvoiceParticipantAddressMapper).mapToDomain(
source.billingAddress,
params
) as InvoiceParticipantBillingAddress)
: undefined;
const shippingAddress = source.shippingAddress
? ((this.props.addressMapper as IInvoiceParticipantAddressMapper).mapToDomain(
source.shippingAddress,
params
) as InvoiceParticipantShippingAddress)
: undefined;
const props: IInvoiceCustomerProps = {
tin: this.mapsValue(source, "tin", TINNumber.create),
firstName: this.mapsValue(source, "first_name", Name.create),
lastName: this.mapsValue(source, "last_name", Name.create),
companyName: this.mapsValue(source, "company_name", Name.create),
billingAddress,
shippingAddress,
};
const id = this.mapsValue(source, "participant_id", UniqueID.create);
const participantOrError = InvoiceCustomer.create(props, id);
if (participantOrError.isFailure) {
throw participantOrError.error;
}
return participantOrError.object;
}
protected toPersistenceMappingImpl(
source: InvoiceCustomer,
params: { sourceParent: Invoice }
): TCreationInvoiceParticipant_Model {
const { sourceParent } = params;
return {
invoice_id: sourceParent.id.toPrimitive(),
participant_id: source.id.toPrimitive(),
tin: source.tin.toPrimitive(),
first_name: source.firstName.toPrimitive(),
last_name: source.lastName.toPrimitive(),
company_name: source.companyName.toPrimitive(),
billingAddress: (
this.props.addressMapper as IInvoiceParticipantAddressMapper
).mapToPersistence(source.billingAddress!, { sourceParent: source }),
shippingAddress: (
this.props.addressMapper as IInvoiceParticipantAddressMapper
).mapToPersistence(source.shippingAddress!, { sourceParent: source }),
};
}
}

View File

@ -1,87 +0,0 @@
import { ISequelizeMapper, SequelizeMapper } from "@/contexts/common/infrastructure";
import {
City,
Country,
Email,
Note,
Phone,
PostalCode,
Province,
Street,
UniqueID,
} from "@shared/contexts";
import {
IInvoiceParticipantAddressProps,
InvoiceCustomer,
InvoiceParticipantAddress,
} from "../../domain";
import { IInvoicingContext } from "../InvoicingContext";
import {
InvoiceParticipantAddress_Model,
TCreationInvoiceParticipantAddress_Model,
} from "../sequelize";
export interface IInvoiceParticipantAddressMapper
extends ISequelizeMapper<
InvoiceParticipantAddress_Model,
TCreationInvoiceParticipantAddress_Model,
InvoiceParticipantAddress
> {}
export const createInvoiceParticipantAddressMapper = (
context: IInvoicingContext
): IInvoiceParticipantAddressMapper => new InvoiceParticipantAddressMapper({ context });
class InvoiceParticipantAddressMapper
extends SequelizeMapper<
InvoiceParticipantAddress_Model,
TCreationInvoiceParticipantAddress_Model,
InvoiceParticipantAddress
>
implements IInvoiceParticipantAddressMapper
{
protected toDomainMappingImpl(source: InvoiceParticipantAddress_Model, params: any) {
const id = this.mapsValue(source, "address_id", UniqueID.create);
const props: IInvoiceParticipantAddressProps = {
type: source.type,
street: this.mapsValue(source, "street", Street.create),
city: this.mapsValue(source, "city", City.create),
province: this.mapsValue(source, "province", Province.create),
postalCode: this.mapsValue(source, "postal_code", PostalCode.create),
country: this.mapsValue(source, "country", Country.create),
email: this.mapsValue(source, "email", Email.create),
phone: this.mapsValue(source, "phone", Phone.create),
notes: this.mapsValue(source, "notes", Note.create),
};
const addressOrError = InvoiceParticipantAddress.create(props, id);
if (addressOrError.isFailure) {
throw addressOrError.error;
}
return addressOrError.object;
}
protected toPersistenceMappingImpl(
source: InvoiceParticipantAddress,
params: { sourceParent: InvoiceCustomer }
) {
const { sourceParent } = params;
return {
address_id: source.id.toPrimitive(),
participant_id: sourceParent.id.toPrimitive(),
type: String(source.type),
title: source.title,
street: source.street.toPrimitive(),
city: source.city.toPrimitive(),
postal_code: source.postalCode.toPrimitive(),
province: source.province.toPrimitive(),
country: source.country.toPrimitive(),
email: source.email.toPrimitive(),
phone: source.phone.toPrimitive(),
};
}
}

View File

@ -1,84 +0,0 @@
import {
CreationOptional,
DataTypes,
InferAttributes,
InferCreationAttributes,
Model,
NonAttribute,
Sequelize,
} from "sequelize";
import { ContactAddress_Model, TCreationContactAddress_Attributes } from "./contactAddress.mo.del";
export type TCreationContact_Model = InferCreationAttributes<
Contact_Model,
{ omit: "shippingAddress" | "billingAddress" }
> & {
billingAddress: TCreationContactAddress_Attributes;
shippingAddress: TCreationContactAddress_Attributes;
};
export class Contact_Model extends Model<
InferAttributes<Contact_Model, { omit: "shippingAddress" | "billingAddress" }>,
InferCreationAttributes<Contact_Model, { omit: "shippingAddress" | "billingAddress" }>
> {
// To avoid table creation
static async sync(): Promise<any> {
return Promise.resolve();
}
static associate(connection: Sequelize) {
const { Contact_Model, ContactAddress_Model } = connection.models;
Contact_Model.hasOne(ContactAddress_Model, {
as: "shippingAddress",
foreignKey: "customer_id",
onDelete: "CASCADE",
});
Contact_Model.hasOne(ContactAddress_Model, {
as: "billingAddress",
foreignKey: "customer_id",
onDelete: "CASCADE",
});
}
declare id: string;
declare tin: CreationOptional<string>;
declare company_name: CreationOptional<string>;
declare first_name: CreationOptional<string>;
declare last_name: CreationOptional<string>;
declare shippingAddress?: NonAttribute<ContactAddress_Model>;
declare billingAddress?: NonAttribute<ContactAddress_Model>;
}
export default (sequelize: Sequelize) => {
Contact_Model.init(
{
id: {
type: new DataTypes.UUID(),
primaryKey: true,
},
tin: {
type: new DataTypes.STRING(),
},
company_name: {
type: new DataTypes.STRING(),
},
first_name: {
type: new DataTypes.STRING(),
},
last_name: {
type: new DataTypes.STRING(),
},
},
{
sequelize,
tableName: "customers",
timestamps: false,
}
);
return Contact_Model;
};

View File

@ -1,75 +0,0 @@
import {
CreationOptional,
DataTypes,
ForeignKey,
InferAttributes,
InferCreationAttributes,
Model,
NonAttribute,
Sequelize,
} from "sequelize";
import { Contact_Model } from "./contact.mo.del";
export type TCreationContactAddress_Attributes = InferCreationAttributes<
ContactAddress_Model,
{ omit: "customer" }
>;
export class ContactAddress_Model extends Model<
InferAttributes<ContactAddress_Model, { omit: "customer" }>,
TCreationContactAddress_Attributes
> {
// To avoid table creation
static async sync(): Promise<any> {
return Promise.resolve();
}
static associate(connection: Sequelize) {
const { Contact_Model, ContactAddress_Model } = connection.models;
ContactAddress_Model.belongsTo(Contact_Model, {
as: "customer",
foreignKey: "customer_id",
});
}
declare id: string;
declare customer_id: ForeignKey<Contact_Model["id"]>;
declare type: string;
declare street: CreationOptional<string>;
declare postal_code: CreationOptional<string>;
declare city: CreationOptional<string>;
declare province: CreationOptional<string>;
declare country: CreationOptional<string>;
declare phone: CreationOptional<string>;
declare email: CreationOptional<string>;
declare customer?: NonAttribute<Contact_Model>;
}
export default (sequelize: Sequelize) => {
ContactAddress_Model.init(
{
id: {
type: DataTypes.UUID,
primaryKey: true,
},
customer_id: new DataTypes.UUID(),
type: DataTypes.STRING(),
street: DataTypes.STRING(),
postal_code: DataTypes.STRING(),
city: DataTypes.STRING,
province: DataTypes.STRING,
country: DataTypes.STRING,
email: DataTypes.STRING,
phone: DataTypes.STRING,
},
{
sequelize,
tableName: "customer_addresses",
timestamps: false,
}
);
return ContactAddress_Model;
};

View File

@ -1,11 +0,0 @@
import { IInvoiceRepository } from "@contexts/invoices/domain";
import { invoiceRepository } from "./invoice.repository";
export * from "./invoice-item.model";
export * from "./invoice.model";
export * from "./invoice.repository";
export const createInvoiceRepository = (): IInvoiceRepository => {
return invoiceRepository;
};

View File

@ -1,165 +0,0 @@
import {
CreationOptional,
DataTypes,
InferAttributes,
InferCreationAttributes,
Model,
} from "sequelize";
export type InvoiceItemCreationAttributes = InferCreationAttributes<InvoiceItemModel, {}> & {};
export class InvoiceItemModel extends Model<
InferAttributes<InvoiceItemModel>,
InvoiceItemCreationAttributes
> {
static associate(connection: Sequelize) {
/*const { Invoice_Model, InvoiceItem_Model } = connection.models;
InvoiceItem_Model.belongsTo(Invoice_Model, {
as: "invoice",
foreignKey: "invoice_id",
onDelete: "CASCADE",
});*/
}
declare item_id: string;
declare invoice_id: string;
declare parent_id: CreationOptional<string>;
declare position: number;
declare item_type: string;
declare description: CreationOptional<string>;
declare quantity_amount: CreationOptional<number>;
declare quantity_scale: CreationOptional<number>;
declare unit_price_amount: CreationOptional<number>;
declare unit_price_scale: CreationOptional<number>;
declare subtotal_amount: CreationOptional<number>;
declare subtotal_scale: CreationOptional<number>;
declare discount_amount: CreationOptional<number>;
declare discount_scale: CreationOptional<number>;
declare total_amount: CreationOptional<number>;
declare total_scale: CreationOptional<number>;
//declare invoice?: NonAttribute<InvoiceModel>;
}
export default (sequelize: Sequelize) => {
InvoiceItemModel.init(
{
item_id: {
type: new DataTypes.UUID(),
primaryKey: true,
},
invoice_id: {
type: new DataTypes.UUID(),
primaryKey: true,
},
parent_id: {
type: new DataTypes.UUID(),
allowNull: true, // Puede ser nulo para elementos de nivel superior
},
position: {
type: new DataTypes.MEDIUMINT(),
autoIncrement: false,
allowNull: false,
},
item_type: {
type: new DataTypes.STRING(),
allowNull: false,
defaultValue: "simple",
},
description: {
type: new DataTypes.TEXT(),
allowNull: true,
},
quantity_amount: {
type: new DataTypes.BIGINT(),
allowNull: true,
defaultValue: null,
},
quantity_scale: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
unit_price_amount: {
type: new DataTypes.BIGINT(),
allowNull: true,
defaultValue: null,
},
unit_price_scale: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
/*tax_slug: {
type: new DataTypes.DECIMAL(3, 2),
allowNull: true,
},
tax_rate: {
type: new DataTypes.DECIMAL(3, 2),
allowNull: true,
},
tax_equalization: {
type: new DataTypes.DECIMAL(3, 2),
allowNull: true,
},*/
subtotal_amount: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
},
subtotal_scale: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
discount_amount: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
discount_scale: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
/*tax_amount: {
type: new DataTypes.BIGINT(),
allowNull: true,
},*/
total_amount: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
},
total_scale: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
},
{
sequelize,
tableName: "invoice_items",
defaultScope: {},
scopes: {},
}
);
return InvoiceItemModel;
};

View File

@ -1,144 +0,0 @@
import {
CreationOptional,
DataTypes,
InferAttributes,
InferCreationAttributes,
Model,
NonAttribute,
Sequelize,
} from "sequelize";
import { InvoiceItemCreationAttributes, InvoiceItemModel } from "./invoice-item.model";
export type InvoiceCreationAttributes = InferCreationAttributes<InvoiceModel, { omit: "items" }> & {
items?: InvoiceItemCreationAttributes[];
};
export class InvoiceModel extends Model<
InferAttributes<InvoiceModel>,
InferCreationAttributes<InvoiceModel, { omit: "items" }>
> {
declare id: string;
declare invoice_status: string;
declare invoice_series: CreationOptional<string>;
declare invoice_number: CreationOptional<string>;
declare issue_date: CreationOptional<string>;
declare operation_date: CreationOptional<string>;
declare invoice_language: string;
declare invoice_currency: string;
// Subtotal
declare subtotal_amount: CreationOptional<number>;
declare subtotal_scale: CreationOptional<number>;
// Total
declare total_amount: CreationOptional<number>;
declare total_scale: CreationOptional<number>;
// Relaciones
declare items: NonAttribute<InvoiceItemModel[]>;
//declare customer: NonAttribute<InvoiceParticipant_Model[]>;
static associate(database: Sequelize) {
const { InvoiceModel, InvoiceItemModel } = database.models;
InvoiceModel.hasMany(InvoiceItemModel, {
as: "items",
foreignKey: "invoice_id",
onDelete: "CASCADE",
});
}
}
export default (database: Sequelize) => {
InvoiceModel.init(
{
id: {
type: new DataTypes.UUID(),
primaryKey: true,
},
invoice_status: {
type: new DataTypes.STRING(),
allowNull: false,
},
invoice_series: {
type: new DataTypes.STRING(),
allowNull: true,
defaultValue: null,
},
invoice_number: {
type: new DataTypes.STRING(),
allowNull: true,
defaultValue: null,
},
issue_date: {
type: new DataTypes.DATEONLY(),
allowNull: true,
defaultValue: null,
},
operation_date: {
type: new DataTypes.DATEONLY(),
allowNull: true,
defaultValue: null,
},
invoice_language: {
type: new DataTypes.STRING(),
allowNull: false,
},
invoice_currency: {
type: new DataTypes.STRING(3), // ISO 4217
allowNull: false,
},
subtotal_amount: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
},
subtotal_scale: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
total_amount: {
type: new DataTypes.BIGINT(), // importante: evita problemas de precisión con valores grandes
allowNull: true,
defaultValue: null,
},
total_scale: {
type: new DataTypes.SMALLINT(),
allowNull: true,
defaultValue: null,
},
},
{
sequelize: database,
tableName: "invoices",
paranoid: true, // softs deletes
timestamps: true,
createdAt: "created_at",
updatedAt: "updated_at",
deletedAt: "deleted_at",
indexes: [{ unique: true, fields: ["invoice_number"] }],
whereMergeStrategy: "and", // <- cómo tratar el merge de un scope
defaultScope: {},
scopes: {},
}
);
return InvoiceModel;
};

View File

@ -1,112 +0,0 @@
import { Invoice } from "@contexts/invoices/domain";
import { IInvoiceRepository } from "@contexts/invoices/domain/repositories/invoice-repository.interface";
import { UniqueID } from "core/common/domain";
import { Collection, Result } from "core/common/helpers";
import { SequelizeRepository } from "core/common/infrastructure";
import { Transaction } from "sequelize";
import { IInvoiceMapper, invoiceMapper } from "../mappers/invoice.mapper";
import { InvoiceItemModel } from "./invoice-item.model";
import { InvoiceModel } from "./invoice.model";
class InvoiceRepository extends SequelizeRepository<Invoice> implements IInvoiceRepository {
private readonly _mapper!: IInvoiceMapper;
/**
* 🔹 Función personalizada para mapear errores de unicidad en autenticación
*/
private _customErrorMapper(error: Error): string | null {
if (error.name === "SequelizeUniqueConstraintError") {
return "Invoice with this email already exists";
}
return null;
}
constructor(mapper: IInvoiceMapper) {
super();
this._mapper = mapper;
}
async invoiceExists(id: UniqueID, transaction?: Transaction): Promise<Result<boolean, Error>> {
try {
const _invoice = await this._getById(InvoiceModel, id, {}, transaction);
return Result.ok(Boolean(id.equals(_invoice.id)));
} catch (error: any) {
return this._handleDatabaseError(error, this._customErrorMapper);
}
}
async findAll(transaction?: Transaction): Promise<Result<Collection<Invoice>, Error>> {
try {
const rawInvoices: any = await this._findAll(
InvoiceModel,
{
include: [
{
model: InvoiceItemModel,
as: "items",
},
],
},
transaction
);
if (!rawInvoices === true) {
return Result.fail(new Error("Invoice with email not exists"));
}
return this._mapper.mapArrayToDomain(rawInvoices);
} catch (error: any) {
return this._handleDatabaseError(error, this._customErrorMapper);
}
}
async getById(id: UniqueID, transaction?: Transaction): Promise<Result<Invoice, Error>> {
try {
const rawInvoice: any = await this._getById(
InvoiceModel,
id,
{
include: [
{
model: InvoiceItemModel,
as: "items",
},
],
},
transaction
);
if (!rawInvoice === true) {
return Result.fail(new Error(`Invoice with id ${id.toString()} not exists`));
}
return this._mapper.mapToDomain(rawInvoice);
} catch (error: any) {
return this._handleDatabaseError(error, this._customErrorMapper);
}
}
async deleteById(id: UniqueID, transaction?: Transaction): Promise<Result<boolean, Error>> {
try {
this._deleteById(InvoiceModel, id);
return Result.ok<boolean>(true);
} catch (error: any) {
return this._handleDatabaseError(error, this._customErrorMapper);
}
}
async create(invoice: Invoice, transaction?: Transaction): Promise<void> {
const invoiceData = this._mapper.mapToPersistence(invoice);
await this._save(InvoiceModel, invoice.id, invoiceData, {}, transaction);
}
async update(invoice: Invoice, transaction?: Transaction): Promise<void> {
const invoiceData = this._mapper.mapToPersistence(invoice);
await this._save(InvoiceModel, invoice.id, invoiceData, {}, transaction);
}
}
const invoiceRepository = new InvoiceRepository(invoiceMapper);
export { invoiceRepository };

Some files were not shown because too many files have changed in this diff Show More