Facturas de cliente

This commit is contained in:
David Arranz 2025-09-11 14:05:50 +02:00
parent 877d98f188
commit e80cc572f9
40 changed files with 741 additions and 646 deletions

View File

@ -1,2 +1,3 @@
export * from "./errors";
export * from "./repositories";
export * from "./value-objects";

View File

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

View File

@ -0,0 +1,78 @@
import { Collection, Result } from "@repo/rdx-utils";
/**
* Tipo para los parámetros que reciben los métodos de los mappers
* Es un objeto que puede contener cualquier cosa.
*/
export type MapperParamsType = Record<string, any>;
/**
* 🧭 Mapper de Dominio (Persistencia Dominio/Agregado)
* - Responsabilidad: transformar un registro de persistencia en un agregado de dominio y viceversa.
* - No debe contener lógica de negocio; sólo construcción/serialización de objetos.
*/
export interface IDomainMapper<TPersistence, TDomain> {
/**
* Convierte un registro crudo de persistencia (ORM/row) en un agregado/entidad de dominio.
* Debe devolver Result.fail(...) si la construcción del dominio no es posible/consistente.
*/
mapToDomain(raw: TPersistence, params?: MapperParamsType): Result<TDomain, Error>;
/**
* Convierte un agregado/entidad de dominio en un objeto de persistencia listo para el ORM.
* Debe devolver Result.fail(...) si hay incoherencias o datos no serializables.
*/
mapToPersistence(domain: TDomain, params?: MapperParamsType): Result<TPersistence, Error>;
}
/**
* 📦 Extensión opcional para operaciones en lote de mapeo de dominio.
* Útil para repos que recuperan múltiples filas y necesitan entregar colecciones de agregados.
*/
export interface IBulkDomainMapper<TPersistence, TDomain> {
/**
* Mapea múltiples registros crudos de persistencia a una Collection de agregados de dominio.
*/
mapToDomainCollection(
raws: TPersistence[],
totalCount: number,
params?: MapperParamsType
): Result<Collection<TDomain>, Error>;
/**
* Mapea múltiples agregados de dominio a un array de objetos de persistencia.
*/
mapToPersistenceArray(
domains: Collection<TDomain>,
params?: MapperParamsType
): Result<TPersistence[], Error>;
}
/**
* 🧩 Tipo para cuando se desea un mapper de dominio con soporte opcional de operaciones en lote.
* Puedes implementar sólo IDomainMapper y añadir IBulkDomainMapper cuando lo necesites.
*/
export type DomainMapperWithBulk<TPersistence, TDomain> = IDomainMapper<TPersistence, TDomain> &
Partial<IBulkDomainMapper<TPersistence, TDomain>>;
/**
*
* 👓 Mapper de Read Model (Persistencia DTO/Proyección de Lectura)
* - Responsabilidad: transformar registros de persistencia en DTOs para lectura (listados, resúmenes, informes).
* - No intenta reconstruir agregados ni validar value objects de dominio.
**/
export interface IReadModelMapperWithBulk<TPersistence, TDTO> {
/**
* Convierte un registro crudo en un DTO de lectura.
*/
mapToDTO(raw: TPersistence, params?: MapperParamsType): Result<TDTO, Error>;
/**
* Convierte múltiples registros crudos en una Collection de DTOs de lectura.
*/
mapToDTOCollection(
raws: TPersistence[],
totalCount: number,
params?: MapperParamsType
): Result<Collection<TDTO>, Error>;
}

View File

@ -1,4 +1,5 @@
export * from "./database";
export * from "./errors";
export * from "./express";
export * from "./mappers";
export * from "./sequelize";

View File

@ -0,0 +1,2 @@
export * from "./mapper-registry";
export * from "./mapper-registry.interface";

View File

@ -0,0 +1,38 @@
/**
* 🔑 Claves de proyección comunes para seleccionar mappers en lectura.
* Puedes extender con otras cadenas según tus necesidades ("SUMMARY", "EXPORT", etc.).
*/
export type MapperProjectionKey = "FULL" | "LIST" | "REPORTS" | (string & {});
/**
* 🏗 Registro/Fábrica de mappers (Strategy/Factory)
* - Permite resolver diferentes mappers según la proyección (FULL, SUMMARY, etc.)
* - Facilita inyección y test (DIP), evitando dependencias duras en implementaciones concretas.
*
* Ejemplo de uso:
* - registry.registerDomainMapper("FULL", customerInvoiceFullMapper);
* - registry.registerReadModelMapper("SUMMARY", customerInvoiceSummaryMapper);
* - registry.registerReadModelMapper("REPORT", customerInvoiceReportMapper);
*/
export interface IMapperRegistry {
/**
* Obtiene un mapper de dominio por clave de proyección.
*/
getDomainMapper<T>(key: MapperProjectionKey): T;
/**
* Obtiene un mapper de read model por clave de proyección.
*/
getReadModelMapper<T>(key: MapperProjectionKey): T;
/**
* Registra un mapper de dominio bajo una clave de proyección.
*/
registerDomainMapper<T>(key: MapperProjectionKey, mapper: T): void;
/**
* Registra un mapper de read model bajo una clave de proyección.
*/
registerReadModelMapper<T>(key: MapperProjectionKey, mapper: T): void;
}

View File

@ -0,0 +1,30 @@
import { InfrastructureError } from "../errors";
import { IMapperRegistry, MapperProjectionKey } from "./mapper-registry.interface";
export class InMemoryMapperRegistry implements IMapperRegistry {
private domainMappers: Map<MapperProjectionKey, any> = new Map();
private readModelMappers: Map<MapperProjectionKey, any> = new Map();
getDomainMapper<T>(key: MapperProjectionKey): T {
if (!this.readModelMappers.has(key)) {
throw new InfrastructureError(`Error. Domain model mapper ${key} not registred!`);
}
return this.domainMappers.get(key);
}
getReadModelMapper<T>(key: MapperProjectionKey): T {
if (!this.readModelMappers.has(key)) {
throw new InfrastructureError(`Error. Read model mapper ${key} not registred!`);
}
return this.readModelMappers.get(key);
}
registerDomainMapper<T>(key: MapperProjectionKey, mapper: T): void {
this.domainMappers.set(key, mapper);
}
registerReadModelMapper<T>(key: MapperProjectionKey, mapper: T): void {
this.readModelMappers.set(key, mapper);
}
}

View File

@ -1,4 +1,4 @@
export * from "./mappers";
export * from "./sequelize-error-translator";
export * from "./sequelize-mapper";
export * from "./sequelize-repository";
export * from "./sequelize-transaction-manager";

View File

@ -0,0 +1,3 @@
export * from "./sequelize-domain-mapper";
export * from "./sequelize-mapper.interface";
export * from "./sequelize-read-model-mapper";

View File

@ -0,0 +1,88 @@
import { Collection, Result, ResultCollection } from "@repo/rdx-utils";
import { Model } from "sequelize";
import { MapperParamsType } from "../../../domain";
import { ISequelizeDomainMapper } from "./sequelize-mapper.interface";
export abstract class SequelizeDomainMapper<TModel extends Model, TModelAttributes, TEntity>
implements ISequelizeDomainMapper<TModel, TModelAttributes, TEntity>
{
public abstract mapToDomain(raw: TModel, params?: MapperParamsType): Result<TEntity, Error>;
public abstract mapToPersistence(
domain: TEntity,
params?: MapperParamsType
): Result<TModelAttributes, Error>;
public mapToDomainCollection(
raws: (TModel | TModelAttributes)[],
totalCount: number,
params?: MapperParamsType
): Result<Collection<TEntity>, Error> {
const _source = raws ?? [];
try {
if (_source.length === 0) {
return Result.ok(new Collection([], totalCount));
}
const items = _source.map(
(value, index) => this.mapToDomain(value as TModel, { index, ...params }).data
);
return Result.ok(new Collection(items, totalCount));
} catch (error) {
return Result.fail(error as Error);
}
}
public mapToPersistenceArray(
domains: Collection<TEntity>,
params?: MapperParamsType
): Result<TModelAttributes[], Error> {
const results = new ResultCollection(
domains.map((domain, index) => this.mapToPersistence(domain, { index, ...params }))
);
if (results.hasSomeFaultyResult()) return results.getFirstFaultyResult();
return Result.ok(results.objects);
}
/*protected _safeMap<T>(operation: () => T, key: string): Result<T, Error> {
try {
return Result.ok(operation());
} catch (error: unknown) {
return Result.fail(error as Error);
}
}
protected _mapsValue(
row: TModel,
key: string,
customMapFn: (value: any, params: MapperParamsType) => Result<any, Error>,
params: MapperParamsType = { defaultValue: null }
): Result<any, Error> {
return customMapFn(row?.dataValues[key] ?? params.defaultValue, params);
}
protected _mapsAssociation(
row: TModel,
associationName: string,
customMapper: DomainMapperWithBulk<any, any>,
params: MapperParamsType = {}
): Result<any, Error> {
if (!customMapper) {
Result.fail(Error(`Custom mapper undefined for ${associationName}`));
}
const { filter, ...otherParams } = params;
let associationRows = row?.dataValues[associationName] ?? [];
if (filter) {
associationRows = Array.isArray(associationRows)
? associationRows.filter(filter)
: filter(associationRows);
}
return Array.isArray(associationRows)
? customMapper.mapToDomainCollection(associationRows, associationRows.length, otherParams)
: customMapper.mapToDomain(associationRows, otherParams);
}*/
}

View File

@ -0,0 +1,7 @@
import { DomainMapperWithBulk, IReadModelMapperWithBulk } from "../../../domain";
export interface ISequelizeDomainMapper<TModel, TModelAttributes, TEntity>
extends DomainMapperWithBulk<TModel | TModelAttributes, TEntity> {}
export interface ISequelizeReadModelMapper<TModel, TDTO>
extends IReadModelMapperWithBulk<TModel, TDTO> {}

View File

@ -0,0 +1,31 @@
import { Collection, Result } from "@repo/rdx-utils";
import { Model } from "sequelize";
import { MapperParamsType } from "../../../domain";
import { ISequelizeReadModelMapper } from "./sequelize-mapper.interface";
export abstract class SequelizeReadModelMapper<TModel extends Model, TEntity>
implements ISequelizeReadModelMapper<TModel, TEntity>
{
public abstract mapToDTO(raw: TModel, params?: MapperParamsType): Result<TEntity, Error>;
public mapToDTOCollection(
raws: TModel[],
totalCount: number,
params?: MapperParamsType
): Result<Collection<TEntity>, Error> {
const _source = raws ?? [];
try {
if (_source.length === 0) {
return Result.ok(new Collection([], totalCount));
}
const items = _source.map(
(value, index) => this.mapToDTO(value as TModel, { index, ...params }).data
);
return Result.ok(new Collection(items, totalCount));
} catch (error) {
return Result.fail(error as Error);
}
}
}

View File

@ -1,22 +0,0 @@
import { ICustomerInvoiceParticipant } from "@/contexts/invoicing/domain";
import { IListCustomerInvoice_Participant_Response_DTO } from "@shared/contexts";
import { CustomerInvoiceParticipantAddressAssembler } from "./CustomerInvoiceParticipantAddress.assembler";
export const CustomerInvoiceParticipantAssembler = (
participant: ICustomerInvoiceParticipant,
): IListCustomerInvoice_Participant_Response_DTO => {
return {
participant_id: participant?.id?.toString(),
tin: participant?.tin?.toString(),
first_name: participant?.firstName?.toString(),
last_name: participant?.lastName?.toString(),
company_name: participant?.companyName?.toString(),
billing_address: CustomerInvoiceParticipantAddressAssembler(
participant?.billingAddress!,
),
shipping_address: CustomerInvoiceParticipantAddressAssembler(
participant?.shippingAddress!,
),
};
};

View File

@ -1,14 +0,0 @@
export const CustomerInvoiceParticipantAddressAssembler = (
address: CustomerInvoiceParticipantAddress
): IListCustomerInvoice_AddressParticipant_Response_DTO => {
return {
address_id: address?.id.toString(),
street: address?.street.toString(),
city: address?.city.toString(),
postal_code: address?.postalCode.toString(),
province: address?.province.toString(),
country: address?.country.toString(),
email: address?.email.toString(),
phone: address?.phone.toString(),
};
};

View File

@ -1,71 +0,0 @@
import { Criteria } from "@repo/rdx-criteria/server";
import { toEmptyString } from "@repo/rdx-ddd";
import { Collection } from "@repo/rdx-utils";
import { CustomerInvoiceListResponseDTO } from "../../../../common/dto";
import { CustomerInvoice } from "../../../domain";
export class ListCustomerInvoicesAssembler {
toDTO(
customerInvoices: Collection<CustomerInvoice>,
criteria: Criteria
): CustomerInvoiceListResponseDTO {
const items = customerInvoices.map((invoice) => {
const recipient = invoice.recipient.match(
(recipient) => recipient.toString(),
() => ({
tin: "",
name: "",
street: "",
street2: "",
city: "",
postal_code: "",
province: "",
country: "",
})
);
return {
id: invoice.id.toString(),
company_id: invoice.companyId.toString(),
customer_id: invoice.customerId.toString(),
invoice_number: invoice.invoiceNumber.toString(),
status: invoice.status.toPrimitive(),
series: toEmptyString(invoice.series, (value) => value.toString()),
invoice_date: invoice.invoiceDate.toDateString(),
operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()),
recipient: {
customer_id: invoice.customerId.toString(),
...recipient,
},
items,
metadata: {
entity: "customer-invoice",
},
};
});
const totalItems = customerInvoices.total();
return {
page: criteria.pageNumber,
per_page: criteria.pageSize,
total_pages: Math.ceil(totalItems / criteria.pageSize),
total_items: totalItems,
items: items,
metadata: {
entity: "customer-invoices",
criteria: criteria.toJSON(),
//links: {
// self: `/api/customer-invoices?page=${criteria.pageNumber}&per_page=${criteria.pageSize}`,
// first: `/api/customer-invoices?page=1&per_page=${criteria.pageSize}`,
// last: `/api/customer-invoices?page=${Math.ceil(totalItems / criteria.pageSize)}&per_page=${criteria.pageSize}`,
//},
},
};
}
}

View File

@ -1,30 +1,16 @@
import { CustomerInvoiceListDTO } from "@erp/customer-invoices/api/infrastructure";
import { Criteria } from "@repo/rdx-criteria/server";
import { toEmptyString } from "@repo/rdx-ddd";
import { ArrayElement, Collection } from "@repo/rdx-utils";
import { CustomerInvoiceListResponseDTO } from "../../../../common/dto";
import { CustomerInvoice } from "../../../domain";
export class ListCustomerInvoicesAssembler {
toDTO(
customerInvoices: Collection<CustomerInvoice>,
customerInvoices: Collection<CustomerInvoiceListDTO>,
criteria: Criteria
): CustomerInvoiceListResponseDTO {
const invoices = customerInvoices.map((invoice) => {
const recipientDTO = invoice.recipient.match(
(recipient) => recipient.toString(),
() => ({
tin: "",
name: "",
street: "",
street2: "",
city: "",
postal_code: "",
province: "",
country: "",
})
);
const allAmounts = invoice.getAllAmounts();
const recipientDTO = invoice.recipient.toObjectString();
const invoiceDTO: ArrayElement<CustomerInvoiceListResponseDTO["items"]> = {
id: invoice.id.toString(),
@ -43,16 +29,13 @@ export class ListCustomerInvoicesAssembler {
...recipientDTO,
},
taxes: invoice.taxes
.getAll()
.map((taxItem) => taxItem.tax.code)
.join(","),
taxes: invoice.taxes,
subtotal_amount: allAmounts.subtotalAmount.toObjectString(),
/*subtotal_amount: allAmounts.subtotalAmount.toObjectString(),
discount_amount: allAmounts.discountAmount.toObjectString(),
taxable_amount: allAmounts.taxableAmount.toObjectString(),
taxes_amount: allAmounts.taxesAmount.toObjectString(),
total_amount: allAmounts.totalAmount.toObjectString(),
total_amount: allAmounts.totalAmount.toObjectString(),*/
metadata: {
entity: "customer-invoice",

View File

@ -86,7 +86,7 @@ export class InvoiceRecipient extends ValueObject<InvoiceRecipientProps> {
return this.getProps();
}
toString() {
toObjectString() {
return {
tin: this.tin.toString(),
name: this.name.toString(),

View File

@ -1,6 +1,6 @@
import { JsonTaxCatalogProvider, spainTaxCatalogProvider } from "@erp/core";
import type { ModuleParams } from "@erp/core/api";
import { SequelizeTransactionManager } from "@erp/core/api";
import type { IMapperRegistry, ModuleParams } from "@erp/core/api";
import { InMemoryMapperRegistry, SequelizeTransactionManager } from "@erp/core/api";
import {
CreateCustomerInvoiceAssembler,
CreateCustomerInvoiceUseCase,
@ -13,13 +13,13 @@ import {
UpdateCustomerInvoiceUseCase,
} from "../application";
import { CustomerInvoiceService } from "../domain";
import { CustomerInvoiceMapper } from "./mappers";
import { CustomerInvoiceFullMapper, CustomerInvoiceListMapper } from "./mappers";
import { CustomerInvoiceRepository } from "./sequelize";
type InvoiceDeps = {
transactionManager: SequelizeTransactionManager;
mapperRegistry: IMapperRegistry;
repo: CustomerInvoiceRepository;
mapper: CustomerInvoiceMapper;
service: CustomerInvoiceService;
catalogs: {
taxes: JsonTaxCatalogProvider;
@ -43,7 +43,7 @@ type InvoiceDeps = {
};
let _repo: CustomerInvoiceRepository | null = null;
let _mapper: CustomerInvoiceMapper | null = null;
let _mapperRegistry: IMapperRegistry | null = null;
let _service: CustomerInvoiceService | null = null;
let _assemblers: InvoiceDeps["assemblers"] | null = null;
let _catalogs: InvoiceDeps["catalogs"] | null = null;
@ -51,13 +51,19 @@ let _catalogs: InvoiceDeps["catalogs"] | null = null;
export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps {
const { database } = params;
const transactionManager = new SequelizeTransactionManager(database);
if (!_catalogs) _catalogs = { taxes: spainTaxCatalogProvider };
if (!_mapper)
_mapper = new CustomerInvoiceMapper({
taxCatalog: _catalogs!.taxes,
});
if (!_repo) _repo = new CustomerInvoiceRepository({ mapper: _mapper, database });
const fullMapper: CustomerInvoiceFullMapper = new CustomerInvoiceFullMapper({
taxCatalog: _catalogs!.taxes,
});
const listMapper = new CustomerInvoiceListMapper();
if (!_mapperRegistry) {
_mapperRegistry = new InMemoryMapperRegistry();
_mapperRegistry.registerDomainMapper("FULL", fullMapper);
_mapperRegistry.registerReadModelMapper("LIST", listMapper);
}
if (!_repo) _repo = new CustomerInvoiceRepository({ mapperRegistry: _mapperRegistry, database });
if (!_service) _service = new CustomerInvoiceService(_repo);
if (!_assemblers) {
@ -72,25 +78,14 @@ export function getInvoiceDependencies(params: ModuleParams): InvoiceDeps {
return {
transactionManager,
repo: _repo,
mapper: _mapper,
mapperRegistry: _mapperRegistry,
service: _service,
assemblers: _assemblers,
catalogs: _catalogs,
build: {
list: () =>
new ListCustomerInvoicesUseCase(
_service!,
transactionManager!,
_assemblers!.list,
_catalogs!.taxes
),
get: () =>
new GetCustomerInvoiceUseCase(
_service!,
transactionManager!,
_assemblers!.get,
_catalogs!.taxes
),
new ListCustomerInvoicesUseCase(_service!, transactionManager!, _assemblers!.list),
get: () => new GetCustomerInvoiceUseCase(_service!, transactionManager!, _assemblers!.get),
create: () =>
new CreateCustomerInvoiceUseCase(
_service!,

View File

@ -13,7 +13,13 @@ export class ListCustomerInvoicesController extends ExpressController {
const result = await this.useCase.execute({ criteria: this.criteria, companyId });
return result.match(
(data) => this.ok(data),
(data) =>
this.ok(data, {
"X-Total-Count": String(data.total_items),
"Pagination-Count": String(data.total_pages),
"Pagination-Page": String(data.page),
"Pagination-Limit": String(data.per_page),
}),
(err) => this.handleError(err)
);
}

View File

@ -1,56 +0,0 @@
import { Response } from "express";
export type ListResult<T> = {
items: T[];
total: number;
limit: number;
offset: number;
};
export type ListPresenterOptions = {
includeMetaInBody?: boolean; // por defecto false (solo items en body)
};
export class ListPresenter<T> {
constructor(
private readonly res: Response,
private readonly opts?: ListPresenterOptions
) {}
/**
Envía cabeceras de paginación y devuelve el cuerpo según la opción:
por defecto: items[]
includeMetaInBody: objeto con { items, total, limit, offset, page }
*/
present(result: ListResult<T>) {
const { total, limit } = result;
const safeLimit = Number.isFinite(limit) && limit > 0 ? limit : 25;
const page = Math.floor(result.offset / (safeLimit || 1)) + 1;
// Cabeceras de paginación (ya expuestas por CORS en app.ts)
this.res.setHeader("X-Total-Count", String(total));
this.res.setHeader("Pagination-Count", String(total));
this.res.setHeader("Pagination-Page", String(page));
this.res.setHeader("Pagination-Limit", String(safeLimit));
if (this.opts?.includeMetaInBody) {
return this.res.status(200).json({
items: result.items,
total,
limit: safeLimit,
offset: result.offset,
page,
});
}
// Contrato clásico: solo items en el body
return this.res.status(200).json(result.items);
}
}
/**
Factoría simple para integrarla en dependencies.ts
*/
export function createListPresenter<T>(res: Response, opts?: ListPresenterOptions) {
return new ListPresenter<T>(res, opts);
}

View File

@ -1,7 +1,7 @@
import {
ISequelizeMapper,
ISequelizeDomainMapper,
MapperParamsType,
SequelizeMapper,
SequelizeDomainMapper,
ValidationErrorCollection,
ValidationErrorDetail,
extractOrPushError,
@ -17,19 +17,19 @@ import {
ItemDiscount,
ItemQuantity,
ItemTaxes,
} from "../../domain";
import { CustomerInvoiceItemCreationAttributes, CustomerInvoiceItemModel } from "../sequelize";
import { ItemTaxesMapper } from "./item-taxes.mapper";
} from "../../../domain";
import { CustomerInvoiceItemCreationAttributes, CustomerInvoiceItemModel } from "../../sequelize";
import { ItemTaxesMapper } from "./item-taxes.full.mapper";
export interface ICustomerInvoiceItemMapper
extends ISequelizeMapper<
extends ISequelizeDomainMapper<
CustomerInvoiceItemModel,
CustomerInvoiceItemCreationAttributes,
CustomerInvoiceItem
> {}
export class CustomerInvoiceItemMapper
extends SequelizeMapper<
extends SequelizeDomainMapper<
CustomerInvoiceItemModel,
CustomerInvoiceItemCreationAttributes,
CustomerInvoiceItem
@ -123,10 +123,14 @@ export class CustomerInvoiceItemMapper
);
// 4) Taxes (colección a nivel de item/línea)
const taxesResults = this._taxesMapper.mapArrayToDomain(source.taxes, {
attributes,
...params,
});
const taxesResults = this._taxesMapper.mapToDomainCollection(
source.taxes,
source.taxes.length,
{
attributes,
...params,
}
);
if (taxesResults.isFailure) {
errors.push({
@ -168,7 +172,7 @@ export class CustomerInvoiceItemMapper
public mapToPersistence(
source: CustomerInvoiceItem,
params?: MapperParamsType
): InferCreationAttributes<CustomerInvoiceItemModel, {}> {
): Result<InferCreationAttributes<CustomerInvoiceItemModel, {}>, Error> {
throw new Error("not implemented");
/*

View File

@ -1,7 +1,7 @@
import {
ISequelizeMapper,
ISequelizeDomainMapper,
MapperParamsType,
SequelizeMapper,
SequelizeDomainMapper,
ValidationErrorCollection,
ValidationErrorDetail,
extractOrPushError,
@ -23,34 +23,38 @@ import {
CustomerInvoiceProps,
CustomerInvoiceSerie,
CustomerInvoiceStatus,
} from "../../domain";
import { InvoiceTaxes } from "../../domain/entities/invoice-taxes";
import { CustomerInvoiceCreationAttributes, CustomerInvoiceModel } from "../sequelize";
import { CustomerInvoiceItemMapper } from "./customer-invoice-item.mapper";
import { InvoiceRecipientMapper } from "./invoice-recipient.mapper";
import { TaxesMapper } from "./taxes.mapper";
} from "../../../domain";
import { InvoiceTaxes } from "../../../domain/entities/invoice-taxes";
import { CustomerInvoiceCreationAttributes, CustomerInvoiceModel } from "../../sequelize";
import { CustomerInvoiceItemMapper as CustomerInvoiceItemFullMapper } from "./customer-invoice-item.full.mapper";
import { InvoiceRecipientMapper as InvoiceRecipientFullMapper } from "./invoice-recipient.full.mapper";
import { TaxesMapper as TaxesFullMapper } from "./taxes.full.mapper";
export interface ICustomerInvoiceMapper
extends ISequelizeMapper<
export interface ICustomerInvoiceFullMapper
extends ISequelizeDomainMapper<
CustomerInvoiceModel,
CustomerInvoiceCreationAttributes,
CustomerInvoice
> {}
export class CustomerInvoiceMapper
extends SequelizeMapper<CustomerInvoiceModel, CustomerInvoiceCreationAttributes, CustomerInvoice>
implements ICustomerInvoiceMapper
export class CustomerInvoiceFullMapper
extends SequelizeDomainMapper<
CustomerInvoiceModel,
CustomerInvoiceCreationAttributes,
CustomerInvoice
>
implements ICustomerInvoiceFullMapper
{
private _itemsMapper: CustomerInvoiceItemMapper;
private _recipientMapper: InvoiceRecipientMapper;
private _taxesMapper: TaxesMapper;
private _itemsMapper: CustomerInvoiceItemFullMapper;
private _recipientMapper: InvoiceRecipientFullMapper;
private _taxesMapper: TaxesFullMapper;
constructor(params: MapperParamsType) {
super();
this._itemsMapper = new CustomerInvoiceItemMapper(params); // Instanciar el mapper de items
this._recipientMapper = new InvoiceRecipientMapper();
this._taxesMapper = new TaxesMapper(params);
this._itemsMapper = new CustomerInvoiceItemFullMapper(params); // Instanciar el mapper de items
this._recipientMapper = new InvoiceRecipientFullMapper();
this._taxesMapper = new TaxesFullMapper(params);
}
private mapAttributesToDomain(source: CustomerInvoiceModel, params?: MapperParamsType) {
@ -169,11 +173,15 @@ export class CustomerInvoiceMapper
}
// 3) Items (colección)
const itemsResults = this._itemsMapper.mapArrayToDomain(source.items, {
errors,
attributes,
...params,
});
const itemsResults = this._itemsMapper.mapToDomainCollection(
source.items,
source.items.length,
{
errors,
attributes,
...params,
}
);
if (itemsResults.isFailure) {
errors.push({
@ -183,11 +191,15 @@ export class CustomerInvoiceMapper
}
// 4) Taxes (colección a nivel factura)
const taxesResults = this._taxesMapper.mapArrayToDomain(source.taxes, {
errors,
attributes,
...params,
});
const taxesResults = this._taxesMapper.mapToDomainCollection(
source.taxes,
source.taxes.length,
{
errors,
attributes,
...params,
}
);
if (taxesResults.isFailure) {
errors.push({
@ -260,7 +272,7 @@ export class CustomerInvoiceMapper
public mapToPersistence(
source: CustomerInvoice,
params?: MapperParamsType
): CustomerInvoiceCreationAttributes {
): Result<CustomerInvoiceCreationAttributes, Error> {
throw new Error("not implemented");
/*const items = this._itemsMapper.mapCollectionToPersistence(source.items, params);

View File

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

View File

@ -16,8 +16,8 @@ import {
extractOrPushError,
} from "@erp/core/api";
import { Maybe, Result } from "@repo/rdx-utils";
import { CustomerInvoiceProps, InvoiceRecipient } from "../../domain";
import { CustomerInvoiceModel } from "../sequelize";
import { CustomerInvoiceProps, InvoiceRecipient } from "../../../domain";
import { CustomerInvoiceModel } from "../../sequelize";
export class InvoiceRecipientMapper {
public mapToDomain(

View File

@ -1,17 +1,20 @@
import { JsonTaxCatalogProvider } from "@erp/core";
import {
MapperParamsType,
SequelizeMapper,
SequelizeDomainMapper,
Tax,
ValidationErrorCollection,
ValidationErrorDetail,
extractOrPushError,
} from "@erp/core/api";
import { Result } from "@repo/rdx-utils";
import { ItemTax } from "../../domain";
import { CustomerInvoiceItemCreationAttributes, CustomerInvoiceItemTaxModel } from "../sequelize";
import { ItemTax } from "../../../domain";
import {
CustomerInvoiceItemCreationAttributes,
CustomerInvoiceItemTaxModel,
} from "../../sequelize";
export class ItemTaxesMapper extends SequelizeMapper<
export class ItemTaxesMapper extends SequelizeDomainMapper<
CustomerInvoiceItemTaxModel,
CustomerInvoiceItemCreationAttributes,
ItemTax

View File

@ -1,18 +1,18 @@
import { JsonTaxCatalogProvider } from "@erp/core";
import {
MapperParamsType,
SequelizeMapper,
SequelizeDomainMapper,
Tax,
ValidationErrorCollection,
ValidationErrorDetail,
extractOrPushError,
} from "@erp/core/api";
import { Result } from "@repo/rdx-utils";
import { CustomerInvoiceProps } from "../../domain";
import { InvoiceTax } from "../../domain/entities/invoice-taxes";
import { CustomerInvoiceTaxCreationAttributes, CustomerInvoiceTaxModel } from "../sequelize";
import { CustomerInvoiceProps } from "../../../domain";
import { InvoiceTax } from "../../../domain/entities/invoice-taxes";
import { CustomerInvoiceTaxCreationAttributes, CustomerInvoiceTaxModel } from "../../sequelize";
export class TaxesMapper extends SequelizeMapper<
export class TaxesMapper extends SequelizeDomainMapper<
CustomerInvoiceTaxModel,
CustomerInvoiceTaxCreationAttributes,
InvoiceTax

View File

@ -1 +1,2 @@
export * from "./customer-invoice.mapper";
export * from "./full-domain";
export * from "./list";

View File

@ -0,0 +1,192 @@
import {
ISequelizeReadModelMapper,
MapperParamsType,
SequelizeReadModelMapper,
ValidationErrorCollection,
ValidationErrorDetail,
extractOrPushError,
} from "@erp/core/api";
import {
CurrencyCode,
LanguageCode,
Percentage,
UniqueID,
UtcDate,
maybeFromNullableVO,
} from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import {
CustomerInvoiceNumber,
CustomerInvoiceSerie,
CustomerInvoiceStatus,
InvoiceRecipient,
} from "../../../domain";
import { CustomerInvoiceModel } from "../../sequelize";
import { InvoiceRecipientListMapper } from "./invoice-recipient.list.mapper";
export type CustomerInvoiceListDTO = {
id: UniqueID;
companyId: UniqueID;
isProforma: boolean;
invoiceNumber: CustomerInvoiceNumber;
status: CustomerInvoiceStatus;
series: Maybe<CustomerInvoiceSerie>;
invoiceDate: UtcDate;
operationDate: Maybe<UtcDate>;
customerId: UniqueID;
recipient: InvoiceRecipient;
languageCode: LanguageCode;
currencyCode: CurrencyCode;
taxes: string;
discountPercentage: Percentage;
};
export interface ICustomerInvoiceListMapper
extends ISequelizeReadModelMapper<CustomerInvoiceModel, CustomerInvoiceListDTO> {}
export class CustomerInvoiceListMapper
extends SequelizeReadModelMapper<CustomerInvoiceModel, CustomerInvoiceListDTO>
implements ICustomerInvoiceListMapper
{
private _recipientMapper: InvoiceRecipientListMapper;
constructor() {
super();
this._recipientMapper = new InvoiceRecipientListMapper();
}
public mapToDTO(
raw: CustomerInvoiceModel,
params?: MapperParamsType
): Result<CustomerInvoiceListDTO, Error> {
const errors: ValidationErrorDetail[] = [];
// 1) Valores escalares (atributos generales)
const attributes = this.mapAttributesToDTO(raw, { errors, ...params });
// 2) Recipient (snapshot en la factura o include)
const recipientResult = this._recipientMapper.mapToDTO(raw, {
errors,
attributes,
...params,
});
if (recipientResult.isFailure) {
errors.push({
path: "recipient",
message: recipientResult.error.message,
});
}
// 3) Taxes
const taxes = raw.taxes.map((tax) => tax.tax_code).join(", ");
// 5) Si hubo errores de mapeo, devolvemos colección de validación
if (errors.length > 0) {
return Result.fail(new ValidationErrorCollection("Customer invoice mapping failed", errors));
}
return Result.ok({
id: attributes.invoiceId!,
companyId: attributes.companyId!,
isProforma: attributes.isProforma,
status: attributes.status!,
series: attributes.series!,
invoiceNumber: attributes.invoiceNumber!,
invoiceDate: attributes.invoiceDate!,
operationDate: attributes.operationDate!,
customerId: attributes.customerId!,
recipient: recipientResult.data,
languageCode: attributes.languageCode!,
currencyCode: attributes.currencyCode!,
discountPercentage: attributes.discountPercentage!,
taxes,
});
}
private mapAttributesToDTO(raw: CustomerInvoiceModel, params?: MapperParamsType) {
const { errors } = params as {
errors: ValidationErrorDetail[];
};
const invoiceId = extractOrPushError(UniqueID.create(raw.id), "id", errors);
const companyId = extractOrPushError(UniqueID.create(raw.company_id), "company_id", errors);
const customerId = extractOrPushError(UniqueID.create(raw.customer_id), "customer_id", errors);
const isProforma = Boolean(raw.is_proforma);
const status = extractOrPushError(CustomerInvoiceStatus.create(raw.status), "status", errors);
const series = extractOrPushError(
maybeFromNullableVO(raw.series, (value) => CustomerInvoiceSerie.create(value)),
"serie",
errors
);
const invoiceNumber = extractOrPushError(
CustomerInvoiceNumber.create(raw.invoice_number),
"invoice_number",
errors
);
const invoiceDate = extractOrPushError(
UtcDate.createFromISO(raw.invoice_date),
"invoice_date",
errors
);
const operationDate = extractOrPushError(
maybeFromNullableVO(raw.operation_date, (value) => UtcDate.createFromISO(value)),
"operation_date",
errors
);
const languageCode = extractOrPushError(
LanguageCode.create(raw.language_code),
"language_code",
errors
);
const currencyCode = extractOrPushError(
CurrencyCode.create(raw.currency_code),
"currency_code",
errors
);
const discountPercentage = extractOrPushError(
Percentage.create({
value: raw.discount_amount_scale,
scale: raw.discount_percentage_scale,
}),
"discount_percentage_value",
errors
);
return {
invoiceId,
companyId,
customerId,
isProforma,
status,
series,
invoiceNumber,
invoiceDate,
operationDate,
languageCode,
currencyCode,
discountPercentage,
};
}
}

View File

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

View File

@ -0,0 +1,116 @@
import {
City,
Country,
Name,
PostalCode,
Province,
Street,
TINNumber,
maybeFromNullableVO,
} from "@repo/rdx-ddd";
import {
IReadModelMapperWithBulk,
MapperParamsType,
SequelizeReadModelMapper,
ValidationErrorDetail,
extractOrPushError,
} from "@erp/core/api";
import { Result } from "@repo/rdx-utils";
import { InvoiceRecipient } from "../../../domain";
import { CustomerInvoiceModel } from "../../sequelize";
import { CustomerInvoiceListDTO } from "./customer-invoice.list.mapper";
interface IInvoiceRecipientListMapper
extends IReadModelMapperWithBulk<CustomerInvoiceModel, InvoiceRecipient> {}
export class InvoiceRecipientListMapper
extends SequelizeReadModelMapper<CustomerInvoiceModel, InvoiceRecipient>
implements IInvoiceRecipientListMapper
{
public mapToDTO(
raw: CustomerInvoiceModel,
params?: MapperParamsType
): Result<InvoiceRecipient, Error> {
/**
* - Factura === proforma -> datos de "current_customer"
* - Factura !== proforma -> snapshot de los datos (campos customer_*)
*/
const { errors, attributes } = params as {
errors: ValidationErrorDetail[];
attributes: Partial<CustomerInvoiceListDTO>;
};
const { isProforma } = attributes;
if (isProforma && !raw.current_customer) {
errors.push({
path: "current_customer",
message: "Current customer not included in query (InvoiceRecipientListMapper)",
});
}
const _name = isProforma ? raw.current_customer.name : raw.customer_name;
const _tin = isProforma ? raw.current_customer.tin : raw.customer_tin;
const _street = isProforma ? raw.current_customer.street : raw.customer_street;
const _street2 = isProforma ? raw.current_customer.street2 : raw.customer_street2;
const _city = isProforma ? raw.current_customer.city : raw.customer_city;
const _postal_code = isProforma ? raw.current_customer.postal_code : raw.customer_postal_code;
const _province = isProforma ? raw.current_customer.province : raw.customer_province;
const _country = isProforma ? raw.current_customer.country : raw.customer_country;
// Customer (snapshot)
const customerName = extractOrPushError(Name.create(_name), "customer_name", errors);
const customerTin = extractOrPushError(TINNumber.create(_tin), "customer_tin", errors);
const customerStreet = extractOrPushError(
maybeFromNullableVO(_street, (value) => Street.create(value)),
"customer_street",
errors
);
const customerStreet2 = extractOrPushError(
maybeFromNullableVO(_street2, (value) => Street.create(value)),
"customer_street2",
errors
);
const customerCity = extractOrPushError(
maybeFromNullableVO(_city, (value) => City.create(value)),
"customer_city",
errors
);
const customerProvince = extractOrPushError(
maybeFromNullableVO(_province, (value) => Province.create(value)),
"customer_province",
errors
);
const customerPostalCode = extractOrPushError(
maybeFromNullableVO(_postal_code, (value) => PostalCode.create(value)),
"customer_postal_code",
errors
);
const customerCountry = extractOrPushError(
maybeFromNullableVO(_country, (value) => Country.create(value)),
"customer_country",
errors
);
return InvoiceRecipient.create({
name: customerName!,
tin: customerTin!,
street: customerStreet!,
street2: customerStreet2!,
city: customerCity!,
postalCode: customerPostalCode!,
province: customerProvince!,
country: customerCountry!,
});
}
}

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: 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.ts.bak";
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: 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,10 +1,15 @@
import { EntityNotFoundError, SequelizeRepository, translateSequelizeError } from "@erp/core/api";
import {
EntityNotFoundError,
IMapperRegistry,
SequelizeRepository,
translateSequelizeError,
} from "@erp/core/api";
import { Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
import { UniqueID } from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils";
import { Sequelize, Transaction } from "sequelize";
import { CustomerInvoice, ICustomerInvoiceRepository } from "../../domain";
import { ICustomerInvoiceMapper } from "../mappers/customer-invoice.mapper";
import { ICustomerInvoiceFullMapper, ICustomerInvoiceListMapper } from "../mappers";
import { CustomerInvoiceItemTaxModel } from "./customer-invoice-item-tax.model";
import { CustomerInvoiceItemModel } from "./customer-invoice-item.model";
import { CustomerInvoiceTaxModel } from "./customer-invoice-tax.model";
@ -15,11 +20,11 @@ export class CustomerInvoiceRepository
implements ICustomerInvoiceRepository
{
private readonly _database!: Sequelize;
private readonly _mapper!: ICustomerInvoiceMapper;
private readonly _registry!: IMapperRegistry;
constructor(params: { mapper: ICustomerInvoiceMapper; database: Sequelize }) {
constructor(params: { mapperRegistry: IMapperRegistry; database: Sequelize }) {
super();
this._mapper = params.mapper;
this._registry = params.mapperRegistry;
this._database = params.database;
}
@ -69,9 +74,17 @@ export class CustomerInvoiceRepository
transaction: Transaction
): Promise<Result<CustomerInvoice, Error>> {
try {
const data = this._mapper.mapToPersistence(invoice);
const mapper: ICustomerInvoiceFullMapper = this._registry.getDomainMapper("FULL");
const mapperData = mapper.mapToPersistence(invoice);
if (mapperData.isFailure) {
return Result.fail(mapperData.error);
}
const { data } = mapperData;
const [instance] = await CustomerInvoiceModel.upsert(data, { transaction, returning: true });
const savedInvoice = this._mapper.mapToDomain(instance);
const savedInvoice = mapper.mapToDomain(instance);
return savedInvoice;
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
@ -97,7 +110,7 @@ export class CustomerInvoiceRepository
transaction,
});
return Result.ok(Boolean(count > 0));
} catch (error: any) {
} catch (error: unknown) {
return Result.fail(translateSequelizeError(error));
}
}
@ -117,6 +130,7 @@ export class CustomerInvoiceRepository
transaction: Transaction
): Promise<Result<CustomerInvoice, Error>> {
try {
const mapper: ICustomerInvoiceFullMapper = this._registry.getReadModelMapper("FULL");
const { CustomerModel } = this._database.models;
const row = await CustomerInvoiceModel.findOne({
@ -152,7 +166,7 @@ export class CustomerInvoiceRepository
return Result.fail(new EntityNotFoundError("CustomerInvoice", "id", id.toString()));
}
const customer = this._mapper.mapToDomain(row);
const customer = mapper.mapToDomain(row);
return customer;
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
@ -174,8 +188,9 @@ export class CustomerInvoiceRepository
companyId: UniqueID,
criteria: Criteria,
transaction: Transaction
): Promise<Result<Collection<CustomerInvoice>, Error>> {
): Promise<Result<Collection<any>, Error>> {
try {
const mapper: ICustomerInvoiceListMapper = this._registry.getReadModelMapper("LIST");
const { CustomerModel } = this._database.models;
const converter = new CriteriaToSequelizeConverter();
const query = converter.convert(criteria);
@ -199,12 +214,12 @@ export class CustomerInvoiceRepository
},
];
const instances = await CustomerInvoiceModel.findAll({
const raws = await CustomerInvoiceModel.findAll({
...query,
transaction,
});
return this._mapper.mapArrayToDomain(instances);
return mapper.mapToDTOCollection(raws, raws.length);
} catch (err: unknown) {
return Result.fail(translateSequelizeError(err));
}

View File

@ -1,106 +0,0 @@
import {
CreationOptional,
DataTypes,
InferAttributes,
InferCreationAttributes,
Model,
NonAttribute,
Sequelize,
} from "sequelize";
import { CustomerInvoiceModel } from "./customer-invoice.model";
import {
CustomerInvoiceParticipantAddress_Model,
TCreationCustomerInvoiceParticipantAddress_Model,
} from "./customer-invoiceParticipantAddress.mo.del.ts.bak";
export type TCreationCustomerInvoiceParticipant_Model = InferCreationAttributes<
CustomerInvoiceParticipant_Model,
{ omit: "shippingAddress" | "billingAddress" | "customerInvoice" }
> & {
billingAddress: TCreationCustomerInvoiceParticipantAddress_Model;
shippingAddress: TCreationCustomerInvoiceParticipantAddress_Model;
};
export class CustomerInvoiceParticipant_Model extends Model<
InferAttributes<
CustomerInvoiceParticipant_Model,
{ omit: "shippingAddress" | "billingAddress" | "customerInvoice" }
>,
InferCreationAttributes<
CustomerInvoiceParticipant_Model,
{ omit: "shippingAddress" | "billingAddress" | "customerInvoice" }
>
> {
static associate(connection: Sequelize) {
const { CustomerInvoice_Model, CustomerInvoiceParticipantAddress_Model, CustomerInvoiceParticipant_Model } =
connection.models;
CustomerInvoiceParticipant_Model.belongsTo(CustomerInvoice_Model, {
as: "customerInvoice",
foreignKey: "customerInvoice_id",
onDelete: "CASCADE",
});
CustomerInvoiceParticipant_Model.hasOne(CustomerInvoiceParticipantAddress_Model, {
as: "shippingAddress",
foreignKey: "participant_id",
onDelete: "CASCADE",
});
CustomerInvoiceParticipant_Model.hasOne(CustomerInvoiceParticipantAddress_Model, {
as: "billingAddress",
foreignKey: "participant_id",
onDelete: "CASCADE",
});
}
declare participant_id: string;
declare customerInvoice_id: string;
declare tin: CreationOptional<string>;
declare company_name: CreationOptional<string>;
declare first_name: CreationOptional<string>;
declare last_name: CreationOptional<string>;
declare shippingAddress?: NonAttribute<CustomerInvoiceParticipantAddress_Model>;
declare billingAddress?: NonAttribute<CustomerInvoiceParticipantAddress_Model>;
declare customerInvoice?: NonAttribute<CustomerInvoiceModel>;
}
export default (sequelize: Sequelize) => {
CustomerInvoiceParticipant_Model.init(
{
participant_id: {
type: DataTypes.UUID,
primaryKey: true,
},
customerInvoice_id: {
type: DataTypes.UUID,
primaryKey: true,
},
tin: {
type: new DataTypes.STRING(),
allowNull: true,
},
company_name: {
type: new DataTypes.STRING(),
allowNull: true,
},
first_name: {
type: new DataTypes.STRING(),
allowNull: true,
},
last_name: {
type: new DataTypes.STRING(),
allowNull: true,
},
},
{
sequelize,
tableName: "customerInvoice_participants",
timestamps: false,
}
);
return CustomerInvoiceParticipant_Model;
};

View File

@ -1,94 +0,0 @@
import {
CreationOptional,
DataTypes,
InferAttributes,
InferCreationAttributes,
Model,
NonAttribute,
Sequelize,
} from "sequelize";
import { CustomerInvoiceParticipant_Model } from "./customer-invoiceParticipant.mo.del.ts.bak";
export type TCreationCustomerInvoiceParticipantAddress_Model = InferCreationAttributes<
CustomerInvoiceParticipantAddress_Model,
{ omit: "participant" }
>;
export class CustomerInvoiceParticipantAddress_Model extends Model<
InferAttributes<CustomerInvoiceParticipantAddress_Model, { omit: "participant" }>,
InferCreationAttributes<CustomerInvoiceParticipantAddress_Model, { omit: "participant" }>
> {
static associate(connection: Sequelize) {
const { CustomerInvoiceParticipantAddress_Model, CustomerInvoiceParticipant_Model } = connection.models;
CustomerInvoiceParticipantAddress_Model.belongsTo(CustomerInvoiceParticipant_Model, {
as: "participant",
foreignKey: "participant_id",
});
}
declare address_id: string;
declare participant_id: string;
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 participant?: NonAttribute<CustomerInvoiceParticipant_Model>;
}
export default (sequelize: Sequelize) => {
CustomerInvoiceParticipantAddress_Model.init(
{
address_id: {
type: DataTypes.UUID,
primaryKey: true,
},
participant_id: {
type: DataTypes.UUID,
primaryKey: true,
},
type: {
type: new DataTypes.STRING(),
allowNull: false,
},
street: {
type: new DataTypes.STRING(),
allowNull: true,
},
postal_code: {
type: new DataTypes.STRING(),
allowNull: true,
},
city: {
type: new DataTypes.STRING(),
allowNull: true,
},
province: {
type: new DataTypes.STRING(),
allowNull: true,
},
country: {
type: new DataTypes.STRING(),
allowNull: true,
},
email: {
type: new DataTypes.STRING(),
allowNull: true,
},
phone: {
type: new DataTypes.STRING(),
allowNull: true,
},
},
{
sequelize,
tableName: "customerInvoice_participant_addresses",
}
);
return CustomerInvoiceParticipantAddress_Model;
};

View File

@ -40,7 +40,9 @@ export class DeleteCustomerUseCase {
const customerExists = existsCheck.data;
if (!customerExists) {
return Result.fail(new EntityNotFoundError("Customer", "id", customerId.toString()));
return Result.fail(
new EntityNotFoundError("Customer", "id", customerId.toObjectString())
);
}
return await this.service.deleteCustomerByIdInCompany(customerId, companyId, transaction);

View File

@ -13,7 +13,13 @@ export class ListCustomersController extends ExpressController {
const result = await this.listCustomers.execute({ criteria: this.criteria, companyId });
return result.match(
(data) => this.ok(data),
(data) =>
this.ok(data, {
"X-Total-Count": String(data.total_items),
"Pagination-Count": String(data.total_pages),
"Pagination-Page": String(data.page),
"Pagination-Limit": String(data.per_page),
}),
(err) => this.handleError(err)
);
}

View File

@ -1,7 +1,7 @@
import {
ISequelizeMapper,
MapperParamsType,
SequelizeMapper,
SequelizeDomainMapper,
ValidationErrorCollection,
ValidationErrorDetail,
extractOrPushError,
@ -34,7 +34,7 @@ export interface ICustomerMapper
extends ISequelizeMapper<CustomerModel, CustomerCreationAttributes, Customer> {}
export class CustomerMapper
extends SequelizeMapper<CustomerModel, CustomerCreationAttributes, Customer>
extends SequelizeDomainMapper<CustomerModel, CustomerCreationAttributes, Customer>
implements ICustomerMapper
{
public mapToDomain(source: CustomerModel, params?: MapperParamsType): Result<Customer, Error> {

View File

@ -25,9 +25,9 @@ export class ResultCollection<T, E extends Error = Error> implements IResultColl
return this._collection.some((result) => result.isFailure);
}
public getFirstFaultyResult(): Result<T, E> {
// biome-ignore lint/style/noNonNullAssertion: <explanation>
return this._collection.find((result) => result.isFailure)!;
public getFirstFaultyResult(): Result<never, E> {
const firstFaultyResult = this._collection.find((result) => result.isFailure);
return Result.fail(firstFaultyResult?.error);
}
public getAllFaultyResults(): Result<T, E>[] {