Supplier invoices (incompleto)

This commit is contained in:
David Arranz 2026-03-30 11:36:56 +02:00
parent 936c440cf3
commit 96ddb559b2
56 changed files with 3794 additions and 0 deletions

View File

@ -0,0 +1,33 @@
{
"name": "@erp/supplier-invoices",
"description": "Supplier invoices",
"version": "0.5.0",
"private": true,
"type": "module",
"sideEffects": false,
"scripts": {
"typecheck": "tsc -p tsconfig.json --noEmit",
"clean": "rimraf .turbo node_modules dist"
},
"exports": {
".": "./src/common/index.ts",
"./common": "./src/common/index.ts",
"./api": "./src/api/index.ts"
},
"devDependencies": {
"@types/express": "^4.17.21",
"typescript": "^5.9.3"
},
"dependencies": {
"@erp/auth": "workspace:*",
"@erp/core": "workspace:*",
"@repo/i18next": "workspace:*",
"@repo/rdx-criteria": "workspace:*",
"@repo/rdx-ddd": "workspace:*",
"@repo/rdx-logger": "workspace:*",
"@repo/rdx-utils": "workspace:*",
"express": "^4.18.2",
"sequelize": "^6.37.5",
"zod": "^4.1.11"
}
}

View File

@ -0,0 +1,6 @@
export * from "./di";
export * from "./models";
export * from "./repositories";
export * from "./services";
export * from "./snapshot-builders";
export * from "./use-cases";

View File

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

View File

@ -0,0 +1,29 @@
import type { CurrencyCode, LanguageCode, UniqueID, UtcDate } from "@repo/rdx-ddd";
import type { Maybe } from "@repo/rdx-utils";
import type { InvoiceAmount, SupplierInvoiceStatus } from "../../domain";
export type SupplierInvoiceSummary = {
id: UniqueID;
companyId: UniqueID;
//invoiceNumber: InvoiceNumber;
status: SupplierInvoiceStatus;
//series: Maybe<InvoiceSerie>;
invoiceDate: UtcDate;
dueDate: Maybe<UtcDate>;
reference: Maybe<string>;
description: Maybe<string>;
supplierId: UniqueID;
//supplier: InvoiceSupplier;
languageCode: LanguageCode;
currencyCode: CurrencyCode;
taxableAmount: InvoiceAmount;
taxesAmount: InvoiceAmount;
totalAmount: InvoiceAmount;
};

View File

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

View File

@ -0,0 +1,40 @@
import type { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
import type { Collection, Result } from "@repo/rdx-utils";
import type { SupplierInvoice } from "../../domain";
import type { SupplierInvoiceSummary } from "../models";
export interface ISupplierInvoiceRepository {
create(invoice: SupplierInvoice, transaction?: unknown): Promise<Result<void, Error>>;
update(invoice: SupplierInvoice, transaction?: unknown): Promise<Result<void, Error>>;
save(invoice: SupplierInvoice, transaction?: unknown): Promise<Result<void, Error>>;
findByIdInCompany(
companyId: UniqueID,
invoiceId: UniqueID,
transaction?: unknown
): Promise<Result<SupplierInvoice, Error>>;
findBySupplierAndNumberInCompany(
companyId: UniqueID,
supplierId: UniqueID,
invoiceNumber: string,
transaction?: unknown
): Promise<Result<SupplierInvoice, Error>>;
existsBySupplierAndNumberInCompany(
companyId: UniqueID,
supplierId: UniqueID,
invoiceNumber: string,
transaction?: unknown
): Promise<Result<boolean, Error>>;
findByCriteriaInCompany(
companyId: UniqueID,
criteria: Criteria,
transaction?: unknown
): Promise<Result<Collection<SupplierInvoiceSummary>, Error>>;
}

View File

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

View File

@ -0,0 +1,313 @@
import {
AggregateRoot,
type CurrencyCode,
type LanguageCode,
type UniqueID,
type UtcDate,
} from "@repo/rdx-ddd";
import { type Maybe, Result } from "@repo/rdx-utils";
import type { InvoicePaymentMethod, SupplierInvoiceTaxes } from "../entities";
import type { SupplierInvoiceSourceType } from "../enums";
import { type InvoiceAmount, SupplierInvoiceStatus } from "../value-objects";
export interface ISupplierInvoiceCreateProps {
companyId: UniqueID;
status: SupplierInvoiceStatus;
supplierInvoiceCategoryId: UniqueID;
supplierId: UniqueID;
invoiceNumber: string;
invoiceDate: UtcDate;
dueDate: Maybe<UtcDate>;
description: Maybe<string>;
notes: Maybe<string>;
paymentMethod: Maybe<InvoicePaymentMethod>;
taxes: SupplierInvoiceTaxes;
currencyCode: CurrencyCode;
languageCode: LanguageCode;
sourceType: SupplierInvoiceSourceType;
documentId: Maybe<UniqueID>;
version?: number;
}
export interface ISupplierInvoiceTotals {
taxableAmount: InvoiceAmount;
ivaAmount: InvoiceAmount;
recAmount: InvoiceAmount;
retentionAmount: InvoiceAmount;
taxesAmount: InvoiceAmount;
transferredTaxesAmount: InvoiceAmount;
netTaxesAmount: InvoiceAmount;
totalAmount: InvoiceAmount;
}
interface ISupplierInvoice {
companyId: UniqueID;
status: SupplierInvoiceStatus;
supplierInvoiceCategoryId: UniqueID;
supplierId: UniqueID;
invoiceNumber: string;
invoiceDate: UtcDate;
dueDate: Maybe<UtcDate>;
description: Maybe<string>;
notes: Maybe<string>;
paymentMethod: Maybe<InvoicePaymentMethod>;
currencyCode: CurrencyCode;
languageCode: LanguageCode;
sourceType: SupplierInvoiceSourceType;
documentId: Maybe<UniqueID>;
version: number;
taxes: SupplierInvoiceTaxes;
totals(): ISupplierInvoiceTotals;
}
export type SupplierInvoicePatchProps = Partial<
Omit<ISupplierInvoiceCreateProps, "companyId" | "status" | "sourceType" | "version">
>;
export type InternalSupplierInvoiceProps = Omit<ISupplierInvoiceCreateProps, "version"> & {
version: number;
};
/**
* Aggregate raíz de factura de proveedor.
*
* Reglas MVP:
* - supplierId requerido
* - invoiceNumber requerido
* - invoiceDate requerido
* - totalAmount > 0
* - companyId requerido
* - editable en cualquier estado
*
* Notas:
* - create() aplica validaciones de negocio de alta.
* - rehydrate() reconstruye desde persistencia sin revalidar.
*/
export class SupplierInvoice
extends AggregateRoot<InternalSupplierInvoiceProps>
implements ISupplierInvoice
{
private constructor(props: InternalSupplierInvoiceProps, id?: UniqueID) {
super(props, id);
}
public static create(
props: ISupplierInvoiceCreateProps,
id?: UniqueID
): Result<SupplierInvoice, Error> {
const validationResult = SupplierInvoice.validateCreateProps(props);
if (validationResult.isFailure) {
return Result.fail(validationResult.error);
}
const supplierInvoice = new SupplierInvoice(
{
...props,
status: props.status ?? SupplierInvoiceStatus.draft(),
version: props.version ?? 1,
},
id
);
return Result.ok(supplierInvoice);
}
/**
* Reconstruye el agregado desde persistencia.
*
* Debe usarse exclusivamente desde infraestructura.
*/
public static rehydrate(props: InternalSupplierInvoiceProps, id: UniqueID): SupplierInvoice {
return new SupplierInvoice(props, id);
}
public get companyId(): UniqueID {
return this.props.companyId;
}
public get supplierId(): UniqueID {
return this.props.supplierId;
}
public get invoiceNumber(): string {
return this.props.invoiceNumber;
}
public get invoiceDate(): UtcDate {
return this.props.invoiceDate;
}
public get currencyCode(): CurrencyCode {
return this.props.currencyCode;
}
public get languageCode(): LanguageCode {
return this.props.languageCode;
}
public get taxes(): SupplierInvoiceTaxes {
return this.props.taxes;
}
public get status(): SupplierInvoiceStatus {
return this.props.status;
}
public get sourceType(): SupplierInvoiceSourceType {
return this.props.sourceType;
}
public get documentId(): Maybe<UniqueID> {
return this.props.documentId;
}
public get version(): number {
return this.props.version;
}
public get description(): Maybe<string> {
return this.props.description;
}
public get notes(): Maybe<string> {
return this.props.notes;
}
public get paymentMethod(): Maybe<InvoicePaymentMethod> {
return this.props.paymentMethod;
}
public get dueDate(): Maybe<UtcDate> {
return this.props.dueDate;
}
public get supplierInvoiceCategoryId(): UniqueID {
return this.props.supplierInvoiceCategoryId;
}
public totals(): ISupplierInvoiceTotals {
return {
taxableAmount: this.taxes.getTaxableAmount(),
ivaAmount: this.taxes.getIvaAmount(),
recAmount: this.taxes.getRecAmount(),
retentionAmount: this.taxes.getRetentionAmount(),
taxesAmount: this.taxes.getTaxesAmount(),
transferredTaxesAmount: this.taxes.getTransferredTaxesAmount(),
netTaxesAmount: this.taxes.getNetTaxesAmount(),
totalAmount: this.taxes.getTotalAmount(),
} as const;
}
/**
* Actualiza campos editables del agregado.
*
* Regla MVP:
* - la factura es editable en cualquier estado
*
* Notas:
* - no permite alterar companyId, status, sourceType ni version externamente
* - incrementa version tras mutación válida
*/
public update(patch: SupplierInvoicePatchProps): Result<void, Error> {
const candidateProps: InternalSupplierInvoiceProps = {
...this.props,
...patch,
version: this.props.version + 1,
};
// Validacciones
Object.assign(this.props, candidateProps);
return Result.ok();
}
/**
* Marca la factura como confirmada.
*/
public confirm(): Result<void, Error> {
if (this.props.status === SupplierInvoiceStatus.confirmed()) {
return Result.ok();
}
this.props.status = SupplierInvoiceStatus.confirmed();
this.incrementVersion();
return Result.ok();
}
/**
* Marca la factura como cancelada.
*/
public cancel(): Result<void, Error> {
if (this.props.status === SupplierInvoiceStatus.cancelled()) {
return Result.ok();
}
this.props.status = SupplierInvoiceStatus.cancelled();
this.incrementVersion();
return Result.ok();
}
private incrementVersion(): void {
this.props.version += 1;
}
private static validateCreateProps(props: ISupplierInvoiceCreateProps): Result<void, Error> {
if (props.dueDate.isSome() && props.dueDate.unwrap() < props.invoiceDate) {
return Result.fail(
new Error("La fecha de vencimiento no puede ser anterior a la fecha de factura")
);
}
/*if (!props.companyId?.trim()) {
return Result.fail(new InvalidSupplierInvoiceCompanyError());
}
if (!props.supplierId?.trim()) {
return Result.fail(new InvalidSupplierInvoiceSupplierError());
}
if (!props.invoiceNumber?.trim()) {
return Result.fail(new InvalidSupplierInvoiceNumberError());
}
if (!(props.invoiceDate instanceof Date) || Number.isNaN(props.invoiceDate.getTime())) {
return Result.fail(new InvalidSupplierInvoiceDateError());
}
if (props.totalAmount <= 0) {
return Result.fail(new InvalidSupplierInvoiceAmountError());
}*/
return Result.ok();
}
}

View File

@ -0,0 +1,2 @@
export * from "./invoice-payment-method";
export * from "./supplier-invoice-taxes";

View File

@ -0,0 +1,32 @@
import { DomainEntity, type UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
export interface InvoicePaymentMethodProps {
paymentDescription: string;
}
export class InvoicePaymentMethod extends DomainEntity<InvoicePaymentMethodProps> {
public static create(
props: InvoicePaymentMethodProps,
id?: UniqueID
): Result<InvoicePaymentMethod, Error> {
const item = new InvoicePaymentMethod(props, id);
return Result.ok(item);
}
get paymentDescription(): string {
return this.props.paymentDescription;
}
getProps(): InvoicePaymentMethodProps {
return this.props;
}
toObjectString() {
return {
id: String(this.id),
payment_description: String(this.paymentDescription),
};
}
}

View File

@ -0,0 +1,2 @@
export * from "./supplier-invoice-tax.entity";
export * from "./supplier-invoice-taxes.collection";

View File

@ -0,0 +1,74 @@
import type { TaxPercentage } from "@erp/core/api";
import { DomainEntity, type Percentage, type UniqueID } from "@repo/rdx-ddd";
import { type Maybe, Result } from "@repo/rdx-utils";
import type { InvoiceAmount } from "../../value-objects";
export type SupplierInvoiceTaxProps = {
taxableAmount: InvoiceAmount;
ivaCode: string;
ivaPercentage: Percentage;
ivaAmount: InvoiceAmount;
recCode: Maybe<string>;
recPercentage: Maybe<Percentage>;
recAmount: InvoiceAmount;
retentionCode: Maybe<string>;
retentionPercentage: Maybe<Percentage>;
retentionAmount: InvoiceAmount;
taxesAmount: InvoiceAmount;
};
export class SupplierInvoiceTax extends DomainEntity<SupplierInvoiceTaxProps> {
public static create(
props: SupplierInvoiceTaxProps,
id?: UniqueID
): Result<SupplierInvoiceTax, Error> {
return Result.ok(new SupplierInvoiceTax(props, id));
}
public get taxableAmount(): InvoiceAmount {
return this.props.taxableAmount;
}
public get ivaCode(): string {
return this.props.ivaCode;
}
public get ivaPercentage(): TaxPercentage {
return this.props.ivaPercentage;
}
public get ivaAmount(): InvoiceAmount {
return this.props.ivaAmount;
}
public get recCode(): Maybe<string> {
return this.props.recCode;
}
public get recPercentage(): Maybe<TaxPercentage> {
return this.props.recPercentage;
}
public get recAmount(): InvoiceAmount {
return this.props.recAmount;
}
public get retentionCode(): Maybe<string> {
return this.props.retentionCode;
}
public get retentionPercentage(): Maybe<TaxPercentage> {
return this.props.retentionPercentage;
}
public get retentionAmount(): InvoiceAmount {
return this.props.retentionAmount;
}
public get taxesAmount(): InvoiceAmount {
return this.props.taxesAmount;
}
public getProps(): SupplierInvoiceTaxProps {
return this.props;
}
}

View File

@ -0,0 +1,74 @@
import type { CurrencyCode, LanguageCode } from "@repo/rdx-ddd";
import { Collection } from "@repo/rdx-utils";
import { InvoiceAmount } from "../../value-objects";
import type { SupplierInvoiceTax } from "./supplier-invoice-tax.entity";
export type SupplierInvoiceTaxesProps = {
taxes?: SupplierInvoiceTax[];
languageCode: LanguageCode;
currencyCode: CurrencyCode;
};
export class SupplierInvoiceTaxes extends Collection<SupplierInvoiceTax> {
private languageCode!: LanguageCode;
private currencyCode!: CurrencyCode;
constructor(props: SupplierInvoiceTaxesProps) {
super(props.taxes ?? []);
this.languageCode = props.languageCode;
this.currencyCode = props.currencyCode;
}
public static create(props: SupplierInvoiceTaxesProps): SupplierInvoiceTaxes {
return new SupplierInvoiceTaxes(props);
}
public getTaxableAmount(): InvoiceAmount {
return this.items.reduce(
(acc, tax) => acc.add(tax.taxableAmount),
InvoiceAmount.zero(this.currencyCode.toString())
);
}
public getIvaAmount(): InvoiceAmount {
return this.items.reduce(
(acc, tax) => acc.add(tax.ivaAmount),
InvoiceAmount.zero(this.currencyCode.toString())
);
}
public getRecAmount(): InvoiceAmount {
return this.items.reduce(
(acc, tax) => acc.add(tax.recAmount),
InvoiceAmount.zero(this.currencyCode.toString())
);
}
public getRetentionAmount(): InvoiceAmount {
return this.items.reduce(
(acc, tax) => acc.add(tax.retentionAmount),
InvoiceAmount.zero(this.currencyCode.toString())
);
}
public getTaxesAmount(): InvoiceAmount {
return this.items.reduce(
(acc, tax) => acc.add(tax.taxesAmount),
InvoiceAmount.zero(this.currencyCode.toString())
);
}
public getTransferredTaxesAmount(): InvoiceAmount {
return this.getIvaAmount().add(this.getRecAmount());
}
public getNetTaxesAmount(): InvoiceAmount {
return this.getTransferredTaxesAmount().subtract(this.getRetentionAmount());
}
public getTotalAmount(): InvoiceAmount {
return this.getTaxableAmount().add(this.getNetTaxesAmount());
}
}

View File

@ -0,0 +1 @@
export * from "./supplier-invoice-source-type.enum";

View File

@ -0,0 +1,20 @@
/**
* Indica el origen de la factura.
*
* Reglas:
* - Determina cómo se ha creado la factura en el sistema
* - No implica calidad de datos ni estado de validación
* - No debe usarse para lógica de negocio compleja en MVP
*/
export enum SupplierInvoiceSourceType {
/**
* Creada manualmente por el usuario.
*/
MANUAL = "MANUAL",
/**
* Creada a partir de un documento (PDF) subido.
* Puede haber sido enriquecida parcialmente por parsing.
*/
DOCUMENT = "DOCUMENT",
}

View File

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

View File

@ -0,0 +1,4 @@
export * from "./invoice-amount.vo";
export * from "./invoice-date.vo";
export * from "./invoice-number.vo";
export * from "./supplier-invoice-status.vo";

View File

@ -0,0 +1,96 @@
import { MoneyValue, type MoneyValueProps, type Percentage, type Quantity } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
type InvoiceAmountProps = Pick<MoneyValueProps, "value" | "currency_code">;
export class InvoiceAmount extends MoneyValue {
public static DEFAULT_SCALE = 2;
static create({ value, currency_code }: InvoiceAmountProps) {
const props = {
value: Number(value),
scale: InvoiceAmount.DEFAULT_SCALE,
currency_code,
};
return Result.ok(new InvoiceAmount(props));
}
static zero(currency_code: string) {
const props = {
value: 0,
currency_code,
};
return InvoiceAmount.create(props).data;
}
toObjectString() {
return {
value: String(this.value),
scale: String(this.scale),
currency_code: this.currencyCode,
};
}
// Ensure fluent operations keep the subclass type
roundUsingScale(intermediateScale: number) {
const scaled = super.convertScale(intermediateScale);
const normalized = scaled.convertScale(InvoiceAmount.DEFAULT_SCALE);
const p = normalized.toPrimitive();
return new InvoiceAmount({
value: p.value,
currency_code: p.currency_code,
scale: InvoiceAmount.DEFAULT_SCALE,
});
}
add(addend: MoneyValue) {
const mv = super.add(addend);
const p = mv.toPrimitive();
return new InvoiceAmount({
value: p.value,
currency_code: p.currency_code,
scale: InvoiceAmount.DEFAULT_SCALE,
});
}
subtract(subtrahend: MoneyValue) {
const mv = super.subtract(subtrahend);
const p = mv.toPrimitive();
return new InvoiceAmount({
value: p.value,
currency_code: p.currency_code,
scale: InvoiceAmount.DEFAULT_SCALE,
});
}
multiply(multiplier: number | Quantity) {
const mv = super.multiply(multiplier);
const p = mv.toPrimitive();
return new InvoiceAmount({
value: p.value,
currency_code: p.currency_code,
scale: InvoiceAmount.DEFAULT_SCALE,
});
}
divide(divisor: number | Quantity) {
const mv = super.divide(divisor);
const p = mv.toPrimitive();
return new InvoiceAmount({
value: p.value,
currency_code: p.currency_code,
scale: InvoiceAmount.DEFAULT_SCALE,
});
}
percentage(percentage: number | Percentage) {
const mv = super.percentage(percentage);
const p = mv.toPrimitive();
return new InvoiceAmount({
value: p.value,
currency_code: p.currency_code,
scale: InvoiceAmount.DEFAULT_SCALE,
});
}
}

View File

@ -0,0 +1,115 @@
import { DomainValidationError, ValueObject } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
type ISupplierInvoiceStatusProps = {
value: string;
};
export enum SUPPLIER_INVOICE_STATUS {
DRAFT = "DRAFT",
NEEDS_REVIEW = "NEEDS_REVIEW",
CONFIRMED = "CONFIRMED",
CANCELLED = "CANCELLED",
}
const SUPPLIER_INVOICE_TRANSITIONS: Record<string, string[]> = {
[SUPPLIER_INVOICE_STATUS.DRAFT]: [
SUPPLIER_INVOICE_STATUS.NEEDS_REVIEW,
SUPPLIER_INVOICE_STATUS.CONFIRMED,
SUPPLIER_INVOICE_STATUS.CANCELLED,
],
[SUPPLIER_INVOICE_STATUS.NEEDS_REVIEW]: [
SUPPLIER_INVOICE_STATUS.DRAFT,
SUPPLIER_INVOICE_STATUS.CONFIRMED,
SUPPLIER_INVOICE_STATUS.CANCELLED,
],
[SUPPLIER_INVOICE_STATUS.CONFIRMED]: [
SUPPLIER_INVOICE_STATUS.NEEDS_REVIEW,
SUPPLIER_INVOICE_STATUS.CANCELLED,
],
[SUPPLIER_INVOICE_STATUS.CANCELLED]: [],
};
export class SupplierInvoiceStatus extends ValueObject<ISupplierInvoiceStatusProps> {
private static readonly ALLOWED_STATUSES = [
SUPPLIER_INVOICE_STATUS.DRAFT,
SUPPLIER_INVOICE_STATUS.NEEDS_REVIEW,
SUPPLIER_INVOICE_STATUS.CONFIRMED,
SUPPLIER_INVOICE_STATUS.CANCELLED,
];
private static readonly FIELD = "supplierInvoiceStatus";
private static readonly ERROR_CODE = "INVALID_SUPPLIER_INVOICE_STATUS";
public static create(value: string): Result<SupplierInvoiceStatus, Error> {
if (!SupplierInvoiceStatus.ALLOWED_STATUSES.includes(value as SUPPLIER_INVOICE_STATUS)) {
const detail = `Estado de la factura de proveedor no válido: ${value}`;
return Result.fail(
new DomainValidationError(
SupplierInvoiceStatus.ERROR_CODE,
SupplierInvoiceStatus.FIELD,
detail
)
);
}
return Result.ok(
value === SUPPLIER_INVOICE_STATUS.NEEDS_REVIEW
? SupplierInvoiceStatus.needsReview()
: value === SUPPLIER_INVOICE_STATUS.CONFIRMED
? SupplierInvoiceStatus.confirmed()
: value === SUPPLIER_INVOICE_STATUS.CANCELLED
? SupplierInvoiceStatus.cancelled()
: SupplierInvoiceStatus.draft()
);
}
public static draft(): SupplierInvoiceStatus {
return new SupplierInvoiceStatus({ value: SUPPLIER_INVOICE_STATUS.DRAFT });
}
public static needsReview(): SupplierInvoiceStatus {
return new SupplierInvoiceStatus({ value: SUPPLIER_INVOICE_STATUS.NEEDS_REVIEW });
}
public static confirmed(): SupplierInvoiceStatus {
return new SupplierInvoiceStatus({ value: SUPPLIER_INVOICE_STATUS.CONFIRMED });
}
public static cancelled(): SupplierInvoiceStatus {
return new SupplierInvoiceStatus({ value: SUPPLIER_INVOICE_STATUS.CANCELLED });
}
public isDraft(): boolean {
return this.props.value === SUPPLIER_INVOICE_STATUS.DRAFT;
}
public isNeedsReview(): boolean {
return this.props.value === SUPPLIER_INVOICE_STATUS.NEEDS_REVIEW;
}
public isConfirmed(): boolean {
return this.props.value === SUPPLIER_INVOICE_STATUS.CONFIRMED;
}
public isCancelled(): boolean {
return this.props.value === SUPPLIER_INVOICE_STATUS.CANCELLED;
}
public canTransitionTo(nextStatus: string): boolean {
return SUPPLIER_INVOICE_TRANSITIONS[this.props.value].includes(nextStatus);
}
public getProps(): string {
return this.props.value;
}
public toPrimitive(): string {
return this.getProps();
}
public toString(): string {
return String(this.props.value);
}
}

View File

@ -0,0 +1,70 @@
import type { IModuleServer } from "@erp/core/api";
export const supplierInvoicesAPIModule: IModuleServer = {
name: "supplier-invoices",
version: "1.0.0",
dependencies: [],
/**
* Fase de SETUP
* ----------------
* - Construye el dominio (una sola vez)
* - Define qué expone el módulo
* - NO conecta infraestructura
*/
async setup(params) {
const { env: ENV, app, database, baseRoutePath: API_BASE_PATH, logger } = params;
// 1) Dominio interno
const internal = buildSupplierInvoicesDependencies(params);
// 2) Servicios públicos (Application Services)
const supplierinvoicesServices: ISupplierInvoicePublicServices =
buildSupplierInvoicePublicServices(params, internal);
logger.info("🚀 Supplier invoices module dependencies registered", {
label: this.name,
});
return {
// Modelos Sequelize del módulo
models,
// Servicios expuestos a otros módulos
services: {
general: supplierinvoicesServices, // 'supplierinvoices:general'
},
// Implementación privada del módulo
internal,
};
},
/**
* Fase de START
* -------------
* - Conecta el módulo al runtime
* - Puede usar servicios e internals ya construidos
* - NO construye dominio
*/
async start(params) {
const { app, baseRoutePath, logger, getInternal } = params;
// Registro de rutas HTTP
supplierInvoicesRouter(params);
logger.info("🚀 Supplier invoices module started", {
label: this.name,
});
},
/**
* Warmup opcional (si lo necesitas en el futuro)
* ----------------------------------------------
* warmup(params) {
* ...
* }
*/
};
export default supplierInvoicesAPIModule;

View File

@ -0,0 +1,7 @@
import supplierInvoiceModelInit from "./models/supplier-invoice.model";
import supplierInvoiceTaxesModelInit from "./models/supplier-invoice-tax.model";
export * from "./models";
// Array de inicializadores para que registerModels() lo use
export const models = [supplierInvoiceModelInit, supplierInvoiceTaxesModelInit];

View File

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

View File

@ -0,0 +1,525 @@
import { DiscountPercentage, type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
import {
CurrencyCode,
LanguageCode,
TextValue,
UniqueID,
UtcDate,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableResult,
maybeToNullable,
} from "@repo/rdx-ddd";
import { Maybe, Result, isNullishOrEmpty } from "@repo/rdx-utils";
import {
type InternalIssuedInvoiceProps,
InvoiceAmount,
InvoiceNumber,
InvoicePaymentMethod,
InvoiceSerie,
InvoiceStatus,
IssuedInvoice,
IssuedInvoiceItems,
IssuedInvoiceTaxes,
} from "../../../../../../domain";
import type {
CustomerInvoiceCreationAttributes,
CustomerInvoiceModel,
} from "../../../../../common";
import { SequelizeIssuedInvoiceItemDomainMapper } from "./sequelize-issued-invoice-item-domain.mapper";
import { SequelizeIssuedInvoiceRecipientDomainMapper } from "./sequelize-issued-invoice-recipient-domain.mapper";
import { SequelizeIssuedInvoiceTaxesDomainMapper } from "./sequelize-issued-invoice-taxes-domain.mapper";
import { SequelizeIssuedInvoiceVerifactuDomainMapper } from "./sequelize-verifactu-record-domain.mapper";
export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
CustomerInvoiceModel,
CustomerInvoiceCreationAttributes,
IssuedInvoice
> {
private _itemsMapper: SequelizeIssuedInvoiceItemDomainMapper;
private _recipientMapper: SequelizeIssuedInvoiceRecipientDomainMapper;
private _taxesMapper: SequelizeIssuedInvoiceTaxesDomainMapper;
private _verifactuMapper: SequelizeIssuedInvoiceVerifactuDomainMapper;
constructor(params: MapperParamsType) {
super();
this._itemsMapper = new SequelizeIssuedInvoiceItemDomainMapper(params); // Instanciar el mapper de items
this._recipientMapper = new SequelizeIssuedInvoiceRecipientDomainMapper();
this._taxesMapper = new SequelizeIssuedInvoiceTaxesDomainMapper(params);
this._verifactuMapper = new SequelizeIssuedInvoiceVerifactuDomainMapper();
}
private _mapAttributesToDomain(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);
// Para issued invoices, proforma_id debe estar relleno
const proformaId = extractOrPushError(
UniqueID.create(String(raw.proforma_id)),
"proforma_id",
errors
);
const status = extractOrPushError(InvoiceStatus.create(raw.status), "status", errors);
const series = extractOrPushError(
maybeFromNullableResult(raw.series, (v) => InvoiceSerie.create(v)),
"series",
errors
);
const invoiceNumber = extractOrPushError(
InvoiceNumber.create(raw.invoice_number),
"invoice_number",
errors
);
// Fechas
const invoiceDate = extractOrPushError(
UtcDate.createFromISO(raw.invoice_date),
"invoice_date",
errors
);
const operationDate = extractOrPushError(
maybeFromNullableResult(raw.operation_date, (v) => UtcDate.createFromISO(v)),
"operation_date",
errors
);
// Idioma / divisa
const languageCode = extractOrPushError(
LanguageCode.create(raw.language_code),
"language_code",
errors
);
const currencyCode = extractOrPushError(
CurrencyCode.create(raw.currency_code),
"currency_code",
errors
);
// Textos opcionales
const reference = extractOrPushError(
maybeFromNullableResult(raw.reference, (value) => Result.ok(String(value))),
"reference",
errors
);
const description = extractOrPushError(
maybeFromNullableResult(raw.description, (value) => Result.ok(String(value))),
"description",
errors
);
const notes = extractOrPushError(
maybeFromNullableResult(raw.notes, (value) => TextValue.create(value)),
"notes",
errors
);
// Método de pago (VO opcional con id + descripción)
let paymentMethod = Maybe.none<InvoicePaymentMethod>();
if (!isNullishOrEmpty(raw.payment_method_id)) {
const paymentId = extractOrPushError(
UniqueID.create(String(raw.payment_method_id)),
"paymentMethod.id",
errors
);
const paymentVO = extractOrPushError(
InvoicePaymentMethod.create(
{ paymentDescription: String(raw.payment_method_description ?? "") },
paymentId ?? undefined
),
"payment_method_description",
errors
);
if (paymentVO) {
paymentMethod = Maybe.some(paymentVO);
}
}
const subtotalAmount = extractOrPushError(
InvoiceAmount.create({
value: raw.subtotal_amount_value,
currency_code: currencyCode?.code,
}),
"subtotal_amount_value",
errors
);
// Total descuento de líneas
const itemsDiscountAmount = extractOrPushError(
InvoiceAmount.create({
value: Number(raw.items_discount_amount_value ?? 0),
currency_code: currencyCode?.code,
}),
"items_discount_amount_value",
errors
);
// % descuento global (VO)
const globalDiscountPercentage = extractOrPushError(
DiscountPercentage.create({
value: Number(raw.global_discount_percentage_value ?? 0),
}),
"global_discount_percentage_value",
errors
);
const globalDiscountAmount = extractOrPushError(
InvoiceAmount.create({
value: Number(raw.global_discount_amount_value ?? 0),
currency_code: currencyCode?.code,
}),
"global_discount_amount_value",
errors
);
const totalDiscountAmount = extractOrPushError(
InvoiceAmount.create({
value: raw.total_discount_amount_value,
currency_code: currencyCode?.code,
}),
"total_discount_amount_value",
errors
);
const taxableAmount = extractOrPushError(
InvoiceAmount.create({
value: raw.taxable_amount_value,
currency_code: currencyCode?.code,
}),
"taxable_amount_value",
errors
);
const ivaAmount = extractOrPushError(
InvoiceAmount.create({
value: raw.iva_amount_value,
currency_code: currencyCode?.code,
}),
"iva_amount_value",
errors
);
const recAmount = extractOrPushError(
InvoiceAmount.create({
value: raw.rec_amount_value,
currency_code: currencyCode?.code,
}),
"rec_amount_value",
errors
);
const retentionAmount = extractOrPushError(
InvoiceAmount.create({
value: raw.retention_amount_value,
currency_code: currencyCode?.code,
}),
"retention_amount_value",
errors
);
const taxesAmount = extractOrPushError(
InvoiceAmount.create({
value: raw.taxes_amount_value,
currency_code: currencyCode?.code,
}),
"taxes_amount_value",
errors
);
const totalAmount = extractOrPushError(
InvoiceAmount.create({
value: raw.total_amount_value,
currency_code: currencyCode?.code,
}),
"total_amount_value",
errors
);
return {
invoiceId,
companyId,
customerId,
proformaId,
status,
series,
invoiceNumber,
invoiceDate,
operationDate,
reference,
description,
notes,
languageCode,
currencyCode,
paymentMethod,
subtotalAmount,
itemsDiscountAmount,
globalDiscountPercentage,
globalDiscountAmount,
totalDiscountAmount,
taxableAmount,
ivaAmount,
recAmount,
retentionAmount,
taxesAmount,
totalAmount,
};
}
public mapToDomain(
raw: CustomerInvoiceModel,
params?: MapperParamsType
): Result<IssuedInvoice, Error> {
try {
const errors: ValidationErrorDetail[] = [];
// 1) Valores escalares (atributos generales)
const attributes = this._mapAttributesToDomain(raw, { errors, ...params });
// 2) Recipient (snapshot en la factura o include)
const recipientResult = this._recipientMapper.mapToDomain(raw, {
errors,
attributes,
...params,
});
// 3) Verifactu (snapshot en la factura o include)
const verifactuResult = this._verifactuMapper.mapToDomain(raw.verifactu, {
errors,
attributes,
...params,
});
// 4) Items (colección)
const itemsResults = this._itemsMapper.mapToDomainCollection(raw.items, raw.items.length, {
errors,
attributes,
...params,
});
// 5) Taxes (colección)
const taxesResults = this._taxesMapper.mapToDomainCollection(raw.taxes, raw.taxes.length, {
errors,
attributes,
...params,
});
// 6) Si hubo errores de mapeo, devolvemos colección de validación
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Customer invoice mapping failed [mapToDomain]", errors)
);
}
// 6) Construcción del agregado (Dominio)
const verifactu = verifactuResult.data;
const items = IssuedInvoiceItems.create({
items: itemsResults.data.getAll(),
languageCode: attributes.languageCode!,
currencyCode: attributes.currencyCode!,
globalDiscountPercentage: attributes.globalDiscountPercentage!,
});
const taxes = IssuedInvoiceTaxes.create({
taxes: taxesResults.data.getAll(),
languageCode: attributes.languageCode!,
currencyCode: attributes.currencyCode!,
});
const invoiceProps: InternalIssuedInvoiceProps = {
companyId: attributes.companyId!,
proformaId: attributes.proformaId!,
status: attributes.status!,
series: attributes.series!,
invoiceNumber: attributes.invoiceNumber!,
invoiceDate: attributes.invoiceDate!,
operationDate: attributes.operationDate!,
customerId: attributes.customerId!,
recipient: recipientResult.data,
reference: attributes.reference!,
description: attributes.description!,
notes: attributes.notes!,
languageCode: attributes.languageCode!,
currencyCode: attributes.currencyCode!,
subtotalAmount: attributes.subtotalAmount!,
itemsDiscountAmount: attributes.itemsDiscountAmount!,
globalDiscountPercentage: attributes.globalDiscountPercentage!,
globalDiscountAmount: attributes.globalDiscountAmount!,
totalDiscountAmount: attributes.totalDiscountAmount!,
taxableAmount: attributes.taxableAmount!,
ivaAmount: attributes.ivaAmount!,
recAmount: attributes.recAmount!,
retentionAmount: attributes.retentionAmount!,
taxesAmount: attributes.taxesAmount!,
totalAmount: attributes.totalAmount!,
paymentMethod: attributes.paymentMethod!,
taxes,
verifactu,
};
const invoiceId = attributes.invoiceId!;
const invoice = IssuedInvoice.rehydrate(invoiceProps, items, invoiceId);
return Result.ok(invoice);
} catch (err: unknown) {
return Result.fail(err as Error);
}
}
public mapToPersistence(
source: IssuedInvoice,
params?: MapperParamsType
): Result<CustomerInvoiceCreationAttributes, Error> {
const errors: ValidationErrorDetail[] = [];
// 1) Items
const itemsResult = this._itemsMapper.mapToPersistenceArray(source.items, {
errors,
parent: source,
...params,
});
if (itemsResult.isFailure) {
errors.push({
path: "items",
message: itemsResult.error.message,
});
}
// 2) Taxes
const taxesResult = this._taxesMapper.mapToPersistenceArray(source.taxes, {
errors,
parent: source,
...params,
});
if (taxesResult.isFailure) {
errors.push({
path: "taxes",
message: taxesResult.error.message,
});
}
// 3) Cliente
const recipient = this._recipientMapper.mapToPersistence(source.recipient, {
errors,
parent: source,
...params,
});
// 4) Verifactu
const verifactuResult = this._verifactuMapper.mapToPersistence(source.verifactu, {
errors,
parent: source,
...params,
});
// 5) Si hubo errores de mapeo, devolvemos colección de validación
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors)
);
}
const items = itemsResult.data;
const taxes = taxesResult.data;
const verifactu = verifactuResult.data;
const invoiceValues: Partial<CustomerInvoiceCreationAttributes> = {
// Identificación
id: source.id.toPrimitive(),
company_id: source.companyId.toPrimitive(),
// Flags / estado / serie / número
is_proforma: false,
status: source.status.toPrimitive(),
proforma_id: source.proformaId.toPrimitive(),
series: maybeToNullable(source.series, (v) => v.toPrimitive()),
invoice_number: source.invoiceNumber.toPrimitive(),
invoice_date: source.invoiceDate.toPrimitive(),
operation_date: maybeToNullable(source.operationDate, (v) => v.toPrimitive()),
language_code: source.languageCode.toPrimitive(),
currency_code: source.currencyCode.toPrimitive(),
reference: maybeToNullable(source.reference, (reference) => reference),
description: maybeToNullable(source.description, (description) => description),
notes: maybeToNullable(source.notes, (v) => v.toPrimitive()),
payment_method_id: maybeToNullable(
source.paymentMethod,
(payment) => payment.toObjectString().id
),
payment_method_description: maybeToNullable(
source.paymentMethod,
(payment) => payment.toObjectString().payment_description
),
subtotal_amount_value: source.subtotalAmount.value,
subtotal_amount_scale: source.subtotalAmount.scale,
items_discount_amount_value: source.itemsDiscountAmount.value,
items_discount_amount_scale: source.itemsDiscountAmount.scale,
global_discount_percentage_value: source.globalDiscountPercentage.toPrimitive().value,
global_discount_percentage_scale: source.globalDiscountPercentage.toPrimitive().scale,
global_discount_amount_value: source.globalDiscountAmount.value,
global_discount_amount_scale: source.globalDiscountAmount.scale,
total_discount_amount_value: source.totalDiscountAmount.value,
total_discount_amount_scale: source.totalDiscountAmount.scale,
taxable_amount_value: source.taxableAmount.value,
taxable_amount_scale: source.taxableAmount.scale,
taxes_amount_value: source.taxesAmount.value,
taxes_amount_scale: source.taxesAmount.scale,
total_amount_value: source.totalAmount.value,
total_amount_scale: source.totalAmount.scale,
customer_id: source.customerId.toPrimitive(),
...recipient,
taxes,
items,
verifactu,
};
return Result.ok<CustomerInvoiceCreationAttributes>(
invoiceValues as CustomerInvoiceCreationAttributes
);
}
}

View File

@ -0,0 +1,416 @@
import type { JsonTaxCatalogProvider } from "@erp/core";
import {
DiscountPercentage,
type MapperParamsType,
SequelizeDomainMapper,
TaxPercentage,
} from "@erp/core/api";
import {
UniqueID,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableOrEmptyString,
maybeFromNullableResult,
maybeToNullable,
maybeToNullableString,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import {
type IIssuedInvoiceCreateProps,
type IIssuedInvoiceItemCreateProps,
type IssuedInvoice,
IssuedInvoiceItem,
ItemAmount,
ItemDescription,
ItemQuantity,
} from "../../../../../../domain";
import type {
CustomerInvoiceItemCreationAttributes,
CustomerInvoiceItemModel,
} from "../../../../../common";
export class SequelizeIssuedInvoiceItemDomainMapper extends SequelizeDomainMapper<
CustomerInvoiceItemModel,
CustomerInvoiceItemCreationAttributes,
IssuedInvoiceItem
> {
private readonly taxCatalog!: JsonTaxCatalogProvider;
constructor(params: MapperParamsType) {
super();
const { taxCatalog } = params as {
taxCatalog: JsonTaxCatalogProvider;
};
if (!taxCatalog) {
throw new Error('taxCatalog not defined ("SequelizeIssuedInvoiceItemDomainMapper")');
}
this.taxCatalog = taxCatalog;
}
private mapAttributesToDomain(
raw: CustomerInvoiceItemModel,
params?: MapperParamsType
): Partial<IIssuedInvoiceItemCreateProps> & { itemId?: UniqueID } {
const { errors, index, attributes } = params as {
index: number;
errors: ValidationErrorDetail[];
attributes: Partial<IIssuedInvoiceCreateProps>;
};
const itemId = extractOrPushError(
UniqueID.create(raw.item_id),
`items[${index}].item_id`,
errors
);
const description = extractOrPushError(
maybeFromNullableResult(raw.description, (v) => ItemDescription.create(v)),
`items[${index}].description`,
errors
);
const quantity = extractOrPushError(
maybeFromNullableResult(raw.quantity_value, (v) => ItemQuantity.create({ value: v })),
`items[${index}].quantity_value`,
errors
);
const unitAmount = extractOrPushError(
maybeFromNullableResult(raw.unit_amount_value, (value) =>
ItemAmount.create({ value, currency_code: attributes.currencyCode?.code })
),
`items[${index}].unit_amount_value`,
errors
);
const subtotalAmount = extractOrPushError(
ItemAmount.create({
value: raw.subtotal_amount_value,
currency_code: attributes.currencyCode?.code,
}),
`items[${index}].subtotal_amount_value`,
errors
);
const itemDiscountPercentage = extractOrPushError(
maybeFromNullableResult(raw.item_discount_percentage_value, (v) =>
DiscountPercentage.create({ value: v })
),
`items[${index}].item_discount_percentage_value`,
errors
);
const itemDiscountAmount = extractOrPushError(
ItemAmount.create({
value: raw.item_discount_amount_value,
currency_code: attributes.currencyCode?.code,
}),
`items[${index}].item_discount_amount_value`,
errors
);
const globalDiscountPercentage = extractOrPushError(
DiscountPercentage.create({ value: raw.global_discount_percentage_value }),
`items[${index}].global_discount_percentage_value`,
errors
);
const globalDiscountAmount = extractOrPushError(
ItemAmount.create({
value: raw.global_discount_amount_value,
currency_code: attributes.currencyCode?.code,
}),
`items[${index}].global_discount_amount_value`,
errors
);
const totalDiscountAmount = extractOrPushError(
ItemAmount.create({
value: raw.total_discount_amount_value,
currency_code: attributes.currencyCode?.code,
}),
`items[${index}].total_discount_amount_value`,
errors
);
const taxableAmount = extractOrPushError(
ItemAmount.create({
value: raw.taxable_amount_value,
currency_code: attributes.currencyCode?.code,
}),
`items[${index}].taxable_amount_value`,
errors
);
const ivaCode = maybeFromNullableOrEmptyString(raw.iva_code);
const ivaPercentage = extractOrPushError(
maybeFromNullableResult(raw.iva_percentage_value, (value) => TaxPercentage.create({ value })),
`items[${index}].iva_percentage_value`,
errors
);
const ivaAmount = extractOrPushError(
ItemAmount.create({
value: raw.iva_amount_value,
currency_code: attributes.currencyCode?.code,
}),
`items[${index}].iva_amount_value`,
errors
);
const recCode = maybeFromNullableOrEmptyString(raw.rec_code);
const recPercentage = extractOrPushError(
maybeFromNullableResult(raw.rec_percentage_value, (value) => TaxPercentage.create({ value })),
`items[${index}].rec_percentage_value`,
errors
);
const recAmount = extractOrPushError(
ItemAmount.create({
value: raw.rec_amount_value,
currency_code: attributes.currencyCode?.code,
}),
`items[${index}].rec_amount_value`,
errors
);
const retentionCode = maybeFromNullableOrEmptyString(raw.retention_code);
const retentionPercentage = extractOrPushError(
maybeFromNullableResult(raw.retention_percentage_value, (value) =>
TaxPercentage.create({ value })
),
`items[${index}].retention_percentage_value`,
errors
);
const retentionAmount = extractOrPushError(
ItemAmount.create({
value: raw.retention_amount_value,
currency_code: attributes.currencyCode?.code,
}),
`items[${index}].retention_amount_value`,
errors
);
const taxesAmount = extractOrPushError(
ItemAmount.create({
value: raw.taxes_amount_value,
currency_code: attributes.currencyCode?.code,
}),
`items[${index}].taxes_amount_value`,
errors
);
const totalAmount = extractOrPushError(
ItemAmount.create({
value: raw.total_amount_value,
currency_code: attributes.currencyCode?.code,
}),
`items[${index}].total_amount_value`,
errors
);
return {
itemId,
languageCode: attributes.languageCode,
currencyCode: attributes.currencyCode,
description,
quantity,
unitAmount,
subtotalAmount,
itemDiscountPercentage,
itemDiscountAmount,
globalDiscountPercentage,
globalDiscountAmount,
totalDiscountAmount,
taxableAmount,
ivaCode,
ivaPercentage,
ivaAmount,
recCode,
recPercentage,
recAmount,
retentionCode,
retentionPercentage,
retentionAmount,
taxesAmount,
totalAmount,
};
}
public mapToDomain(
source: CustomerInvoiceItemModel,
params?: MapperParamsType
): Result<IssuedInvoiceItem, Error> {
const { errors, index } = params as {
index: number;
errors: ValidationErrorDetail[];
attributes: Partial<IIssuedInvoiceCreateProps>;
};
// 1) Valores escalares (atributos generales)
const attributes = this.mapAttributesToDomain(source, params);
// Si hubo errores de mapeo, devolvemos colección de validación
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Customer invoice item mapping failed [mapToDomain]", errors)
);
}
// 2) Construcción del elemento de dominio
const itemId = attributes.itemId!;
const newItem = IssuedInvoiceItem.rehydrate(
{
description: attributes.description!,
quantity: attributes.quantity!,
unitAmount: attributes.unitAmount!,
subtotalAmount: attributes.subtotalAmount!,
itemDiscountPercentage: attributes.itemDiscountPercentage!,
itemDiscountAmount: attributes.itemDiscountAmount!,
globalDiscountPercentage: attributes.globalDiscountPercentage!,
globalDiscountAmount: attributes.globalDiscountAmount!,
totalDiscountAmount: attributes.totalDiscountAmount!,
taxableAmount: attributes.taxableAmount!,
ivaCode: attributes.ivaCode!,
ivaPercentage: attributes.ivaPercentage!,
ivaAmount: attributes.ivaAmount!,
recCode: attributes.recCode!,
recPercentage: attributes.recPercentage!,
recAmount: attributes.recAmount!,
retentionCode: attributes.retentionCode!,
retentionPercentage: attributes.retentionPercentage!,
retentionAmount: attributes.retentionAmount!,
taxesAmount: attributes.taxesAmount!,
totalAmount: attributes.totalAmount!,
languageCode: attributes.languageCode!,
currencyCode: attributes.currencyCode!,
},
itemId
);
return Result.ok(newItem);
}
public mapToPersistence(
source: IssuedInvoiceItem,
params?: MapperParamsType
): Result<CustomerInvoiceItemCreationAttributes, Error> {
const { errors, index, parent } = params as {
index: number;
parent: IssuedInvoice;
errors: ValidationErrorDetail[];
};
return Result.ok({
item_id: source.id.toPrimitive(),
invoice_id: parent.id.toPrimitive(),
position: index,
description: maybeToNullable(source.description, (v) => v.toPrimitive()),
quantity_value: maybeToNullable(source.quantity, (v) => v.toPrimitive().value),
quantity_scale:
maybeToNullable(source.quantity, (v) => v.toPrimitive().scale) ??
ItemQuantity.DEFAULT_SCALE,
unit_amount_value: maybeToNullable(source.unitAmount, (v) => v.toPrimitive().value),
unit_amount_scale:
maybeToNullable(source.unitAmount, (v) => v.toPrimitive().scale) ??
ItemAmount.DEFAULT_SCALE,
subtotal_amount_value: source.subtotalAmount.toPrimitive().value,
subtotal_amount_scale: source.subtotalAmount.toPrimitive().scale,
item_discount_percentage_value: maybeToNullable(
source.itemDiscountPercentage,
(v) => v.toPrimitive().value
),
item_discount_percentage_scale:
maybeToNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ??
DiscountPercentage.DEFAULT_SCALE,
item_discount_amount_value: source.itemDiscountAmount.toPrimitive().value,
item_discount_amount_scale: source.itemDiscountAmount.toPrimitive().scale,
global_discount_percentage_value: source.globalDiscountPercentage.toPrimitive().value,
global_discount_percentage_scale:
source.globalDiscountPercentage.toPrimitive().scale ?? DiscountPercentage.DEFAULT_SCALE,
global_discount_amount_value: source.globalDiscountAmount.value,
global_discount_amount_scale: source.globalDiscountAmount.scale,
total_discount_amount_value: source.totalDiscountAmount.value,
total_discount_amount_scale: source.totalDiscountAmount.scale,
taxable_amount_value: source.taxableAmount.value,
taxable_amount_scale: source.taxableAmount.scale,
// IVA
iva_code: maybeToNullableString(source.ivaCode),
iva_percentage_value: maybeToNullable(source.ivaPercentage, (v) => v.toPrimitive().value),
iva_percentage_scale:
maybeToNullable(source.ivaPercentage, (v) => v.toPrimitive().scale) ?? 2,
iva_amount_value: source.ivaAmount.value,
iva_amount_scale: source.ivaAmount.scale,
// REC
rec_code: maybeToNullableString(source.recCode),
rec_percentage_value: maybeToNullable(source.recPercentage, (v) => v.toPrimitive().value),
rec_percentage_scale:
maybeToNullable(source.recPercentage, (v) => v.toPrimitive().scale) ?? 2,
rec_amount_value: source.recAmount.value,
rec_amount_scale: source.recAmount.scale,
// RET
retention_code: maybeToNullableString(source.retentionCode),
retention_percentage_value: maybeToNullable(
source.retentionPercentage,
(v) => v.toPrimitive().value
),
retention_percentage_scale:
maybeToNullable(source.retentionPercentage, (v) => v.toPrimitive().scale) ?? 2,
retention_amount_value: source.retentionAmount.value,
retention_amount_scale: source.retentionAmount.scale,
//
taxes_amount_value: source.taxesAmount.value,
taxes_amount_scale: source.taxesAmount.scale,
//
total_amount_value: source.totalAmount.value,
total_amount_scale: source.totalAmount.scale,
});
}
}

View File

@ -0,0 +1,156 @@
import type { MapperParamsType } from "@erp/core/api";
import {
City,
Country,
Name,
PostalCode,
Province,
Street,
TINNumber,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableResult,
maybeToNullable,
} from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import {
type IIssuedInvoiceCreateProps,
InvoiceRecipient,
type IssuedInvoice,
} from "../../../../../../domain";
import type { CustomerInvoiceModel } from "../../../../../common";
export class SequelizeIssuedInvoiceRecipientDomainMapper {
public mapToDomain(
source: CustomerInvoiceModel,
params?: MapperParamsType
): Result<Maybe<InvoiceRecipient>, Error> {
/**
* - Issued invoice -> snapshot de los datos (campos customer_*)
*/
const { errors, attributes } = params as {
errors: ValidationErrorDetail[];
attributes: Partial<IIssuedInvoiceCreateProps>;
};
const _name = source.customer_name!;
const _tin = source.customer_tin!;
const _street = source.customer_street!;
const _street2 = source.customer_street2!;
const _city = source.customer_city!;
const _postal_code = source.customer_postal_code!;
const _province = source.customer_province!;
const _country = source.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(
maybeFromNullableResult(_street, (value) => Street.create(value)),
"customer_street",
errors
);
const customerStreet2 = extractOrPushError(
maybeFromNullableResult(_street2, (value) => Street.create(value)),
"customer_street2",
errors
);
const customerCity = extractOrPushError(
maybeFromNullableResult(_city, (value) => City.create(value)),
"customer_city",
errors
);
const customerProvince = extractOrPushError(
maybeFromNullableResult(_province, (value) => Province.create(value)),
"customer_province",
errors
);
const customerPostalCode = extractOrPushError(
maybeFromNullableResult(_postal_code, (value) => PostalCode.create(value)),
"customer_postal_code",
errors
);
const customerCountry = extractOrPushError(
maybeFromNullableResult(_country, (value) => Country.create(value)),
"customer_country",
errors
);
const createResult = InvoiceRecipient.create({
name: customerName!,
tin: customerTin!,
street: customerStreet!,
street2: customerStreet2!,
city: customerCity!,
postalCode: customerPostalCode!,
province: customerProvince!,
country: customerCountry!,
});
if (createResult.isFailure) {
return Result.fail(
new ValidationErrorCollection("Invoice recipient entity creation failed", [
{ path: "recipient", message: createResult.error.message },
])
);
}
return Result.ok(Maybe.some(createResult.data));
}
/**
* Mapea los datos del destinatario (recipient) de una factura de cliente
* al formato esperado por la capa de persistencia.
*
* Reglas:
* - Si la factura es proforma (`isProforma === true`), todos los campos de recipient son `null`.
* - Si la factura no es proforma (`isProforma === false`), debe existir `recipient`.
* En caso contrario, se agrega un error de validación.
*/
mapToPersistence(source: Maybe<InvoiceRecipient>, params?: MapperParamsType) {
const { errors, parent } = params as {
parent: IssuedInvoice;
errors: ValidationErrorDetail[];
};
const { hasRecipient } = parent;
// Validación: facturas emitidas deben tener destinatario.
if (!hasRecipient) {
errors.push({
path: "recipient",
message: "[InvoiceRecipientDomainMapper] Issued customer invoice without recipient data",
});
}
// Si hay errores previos, devolvemos fallo de validación inmediatamente.
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors)
);
}
const recipient = source.unwrap();
return {
customer_tin: recipient.tin.toPrimitive(),
customer_name: recipient.name.toPrimitive(),
customer_street: maybeToNullable(recipient.street, (v) => v.toPrimitive()),
customer_street2: maybeToNullable(recipient.street2, (v) => v.toPrimitive()),
customer_city: maybeToNullable(recipient.city, (v) => v.toPrimitive()),
customer_province: maybeToNullable(recipient.province, (v) => v.toPrimitive()),
customer_postal_code: maybeToNullable(recipient.postalCode, (v) => v.toPrimitive()),
customer_country: maybeToNullable(recipient.country, (v) => v.toPrimitive()),
};
}
}

View File

@ -0,0 +1,249 @@
import type { JsonTaxCatalogProvider } from "@erp/core";
import {
DiscountPercentage,
type MapperParamsType,
SequelizeDomainMapper,
TaxPercentage,
} from "@erp/core/api";
import {
Percentage,
UniqueID,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableOrEmptyString,
maybeFromNullableResult,
maybeToNullable,
maybeToNullableString,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import {
type IIssuedInvoiceCreateProps,
InvoiceAmount,
type IssuedInvoice,
IssuedInvoiceTax,
ItemAmount,
} from "../../../../../../domain";
import type {
CustomerInvoiceTaxCreationAttributes,
CustomerInvoiceTaxModel,
} from "../../../../../common";
/**
* Mapper para customer_invoice_taxes
*
* Domina estructuras:
* {
* tax: Tax
* taxableAmount: ItemAmount
* taxesAmount: ItemAmount
* }
*
* Cada fila = un impuesto agregado en toda la factura.
*/
export class SequelizeIssuedInvoiceTaxesDomainMapper extends SequelizeDomainMapper<
CustomerInvoiceTaxModel,
CustomerInvoiceTaxCreationAttributes,
IssuedInvoiceTax
> {
private taxCatalog!: JsonTaxCatalogProvider;
constructor(params: MapperParamsType) {
super();
const { taxCatalog } = params as {
taxCatalog: JsonTaxCatalogProvider;
};
if (!taxCatalog) {
throw new Error('taxCatalog not defined ("SequelizeIssuedInvoiceTaxesDomainMapper")');
}
this.taxCatalog = taxCatalog;
}
public mapToDomain(
raw: CustomerInvoiceTaxModel,
params?: MapperParamsType
): Result<IssuedInvoiceTax, Error> {
const { errors, index, attributes } = params as {
index: number;
errors: ValidationErrorDetail[];
attributes: Partial<IIssuedInvoiceCreateProps>;
};
const taxableAmount = extractOrPushError(
InvoiceAmount.create({
value: raw.taxable_amount_value,
currency_code: attributes.currencyCode?.code,
}),
`taxes[${index}].taxable_amount_value`,
errors
);
const ivaCode = raw.iva_code;
// Una issued invoice debe traer IVA
const ivaPercentage = extractOrPushError(
TaxPercentage.create({
value: Number(raw.iva_percentage_value),
}),
`taxes[${index}].iva_percentage_value`,
errors
);
const ivaAmount = extractOrPushError(
InvoiceAmount.create({
value: raw.iva_amount_value,
currency_code: attributes.currencyCode?.code,
}),
`taxes[${index}].iva_amount_value`,
errors
);
const recCode = maybeFromNullableOrEmptyString(raw.rec_code);
const recPercentage = extractOrPushError(
maybeFromNullableResult(raw.rec_percentage_value, (value) => TaxPercentage.create({ value })),
`taxes[${index}].rec_percentage_value`,
errors
);
const recAmount = extractOrPushError(
InvoiceAmount.create({
value: raw.rec_amount_value,
currency_code: attributes.currencyCode?.code,
}),
`taxes[${index}].rec_amount_value`,
errors
);
const retentionCode = maybeFromNullableOrEmptyString(raw.retention_code);
const retentionPercentage = extractOrPushError(
maybeFromNullableResult(raw.retention_percentage_value, (value) =>
TaxPercentage.create({ value })
),
`taxes[${index}].retention_percentage_value`,
errors
);
const retentionAmount = extractOrPushError(
InvoiceAmount.create({
value: raw.retention_amount_value,
currency_code: attributes.currencyCode?.code,
}),
`taxes[${index}].retention_amount_value`,
errors
);
const taxesAmount = extractOrPushError(
InvoiceAmount.create({
value: raw.taxes_amount_value,
currency_code: attributes.currencyCode?.code,
}),
`taxes[${index}].taxes_amount_value`,
errors
);
// Si hubo errores de mapeo, devolvemos colección de validación
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Customer invoice tax mapping failed [mapToDomain]", errors)
);
}
// 2) Construcción del elemento de dominio
const createResult = IssuedInvoiceTax.create({
taxableAmount: taxableAmount!,
ivaCode: ivaCode!,
ivaPercentage: ivaPercentage!,
ivaAmount: ivaAmount!,
recCode: recCode,
recPercentage: recPercentage!,
recAmount: recAmount!,
retentionCode: retentionCode,
retentionPercentage: retentionPercentage!,
retentionAmount: retentionAmount!,
taxesAmount: taxesAmount!,
});
if (createResult.isFailure) {
return Result.fail(
new ValidationErrorCollection("Invoice tax group entity creation failed", [
{ path: `taxes[${index}]`, message: "Invoice tax group entity creation failed" },
])
);
}
return createResult;
}
public mapToPersistence(
source: IssuedInvoiceTax,
params?: MapperParamsType
): Result<CustomerInvoiceTaxCreationAttributes, Error> {
const { errors, parent } = params as {
parent: IssuedInvoice;
errors: ValidationErrorDetail[];
};
try {
const dto: CustomerInvoiceTaxCreationAttributes = {
tax_id: UniqueID.generateNewID().toPrimitive(),
invoice_id: parent.id.toPrimitive(),
// TAXABLE AMOUNT
taxable_amount_value: source.taxableAmount.value,
taxable_amount_scale: source.taxableAmount.scale,
// IVA
iva_code: source.ivaCode,
iva_percentage_value: source.ivaPercentage.value,
iva_percentage_scale: source.ivaPercentage.scale,
iva_amount_value: source.ivaAmount.value,
iva_amount_scale: source.ivaAmount.scale,
// REC
rec_code: maybeToNullableString(source.recCode),
rec_percentage_value: maybeToNullable(source.recPercentage, (v) => v.toPrimitive().value),
rec_percentage_scale:
maybeToNullable(source.recPercentage, (v) => v.toPrimitive().scale) ??
DiscountPercentage.DEFAULT_SCALE,
rec_amount_value: source.recAmount.toPrimitive().value,
rec_amount_scale: source.recAmount.toPrimitive().scale ?? ItemAmount.DEFAULT_SCALE,
// RET
retention_code: maybeToNullableString(source.retentionCode),
retention_percentage_value: maybeToNullable(
source.retentionPercentage,
(v) => v.toPrimitive().value
),
retention_percentage_scale:
maybeToNullable(source.retentionPercentage, (v) => v.toPrimitive().scale) ??
Percentage.DEFAULT_SCALE,
retention_amount_value: source.retentionAmount.toPrimitive().value,
retention_amount_scale:
source.retentionAmount.toPrimitive().scale ?? ItemAmount.DEFAULT_SCALE,
// TOTAL
taxes_amount_value: source.taxesAmount.value,
taxes_amount_scale: source.taxesAmount.scale,
};
return Result.ok(dto);
} catch (error: unknown) {
return Result.fail(error as Error);
}
}
}

View File

@ -0,0 +1,135 @@
import type { MapperParamsType } from "@erp/core/api";
import { SequelizeDomainMapper } from "@erp/core/api";
import {
URLAddress,
UniqueID,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableResult,
maybeToEmptyString,
} from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import {
type IIssuedInvoiceCreateProps,
type IssuedInvoice,
VerifactuRecord,
VerifactuRecordEstado,
} from "../../../../../../domain";
import type {
VerifactuRecordCreationAttributes,
VerifactuRecordModel,
} from "../../../../../common";
export class SequelizeIssuedInvoiceVerifactuDomainMapper extends SequelizeDomainMapper<
VerifactuRecordModel,
VerifactuRecordCreationAttributes,
Maybe<VerifactuRecord>
> {
public mapToDomain(
source: VerifactuRecordModel,
params?: MapperParamsType
): Result<Maybe<VerifactuRecord>, Error> {
const { errors, attributes } = params as {
errors: ValidationErrorDetail[];
attributes: Partial<IIssuedInvoiceCreateProps>;
};
if (!source) {
return Result.ok(Maybe.none());
}
const recordId = extractOrPushError(UniqueID.create(source.id), "id", errors);
const estado = extractOrPushError(
VerifactuRecordEstado.create(source.estado),
"estado",
errors
);
const qr = extractOrPushError(
maybeFromNullableResult(source.qr, (value) => Result.ok(String(value))),
"qr",
errors
);
const url = extractOrPushError(
maybeFromNullableResult(source.url, (value) => URLAddress.create(value)),
"url",
errors
);
const uuid = extractOrPushError(
maybeFromNullableResult(source.uuid, (value) => Result.ok(String(value))),
"uuid",
errors
);
const operacion = extractOrPushError(
maybeFromNullableResult(source.operacion, (value) => Result.ok(String(value))),
"operacion",
errors
);
if (errors.length > 0) {
return Result.fail(
new ValidationErrorCollection("Verifactu record mapping failed [mapToDTO]", errors)
);
}
const createResult = VerifactuRecord.create(
{
estado: estado!,
qrCode: qr!,
url: url!,
uuid: uuid!,
operacion: operacion!,
},
recordId!
);
if (createResult.isFailure) {
return Result.fail(
new ValidationErrorCollection("Invoice verifactu entity creation failed", [
{ path: "verifactu", message: createResult.error.message },
])
);
}
return Result.ok(Maybe.some(createResult.data));
}
mapToPersistence(
source: Maybe<VerifactuRecord>,
params?: MapperParamsType
): Result<VerifactuRecordCreationAttributes, Error> {
const { errors, parent } = params as {
parent: IssuedInvoice;
errors: ValidationErrorDetail[];
};
if (source.isNone()) {
return Result.ok({
id: UniqueID.generateNewID().toPrimitive(),
invoice_id: parent.id.toPrimitive(),
estado: VerifactuRecordEstado.createPendiente().toPrimitive(),
qr: "",
url: "",
uuid: "",
operacion: "",
});
}
const verifactu = source.unwrap();
return Result.ok({
id: verifactu.id.toPrimitive(),
invoice_id: parent.id.toPrimitive(),
estado: verifactu.estado.toPrimitive(),
qr: maybeToEmptyString(verifactu.qrCode, (v) => v),
url: maybeToEmptyString(verifactu.url, (v) => v.toPrimitive()),
uuid: maybeToEmptyString(verifactu.uuid, (v) => v),
operacion: maybeToEmptyString(verifactu.operacion, (v) => v),
});
}
}

View File

@ -0,0 +1,2 @@
export * from "./domain";
export * from "./summary";

View File

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

View File

@ -0,0 +1,118 @@
import { type MapperParamsType, SequelizeQueryMapper } from "@erp/core/api";
import {
City,
Country,
Name,
PostalCode,
Province,
Street,
TINNumber,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableResult,
} from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { IssuedInvoiceSummary } from "../../../../../../application";
import { InvoiceRecipient } from "../../../../../../domain";
import type { CustomerInvoiceModel } from "../../../../../common";
export class SequelizeIssuedInvoiceRecipientListMapper extends SequelizeQueryMapper<
CustomerInvoiceModel,
InvoiceRecipient
> {
public mapToReadModel(
raw: CustomerInvoiceModel,
params?: MapperParamsType
): Result<InvoiceRecipient, Error> {
/**
* - Issued invoice => snapshot de los datos (campos customer_*)
*/
const { errors, attributes } = params as {
errors: ValidationErrorDetail[];
attributes: Partial<IssuedInvoiceSummary>;
};
const { isProforma } = attributes;
if (isProforma && !raw.current_customer) {
errors.push({
path: "current_customer",
message: "Current customer not included in query (InvoiceRecipientListMapper)",
});
}
const _name = raw.customer_name!;
const _tin = raw.customer_tin!;
const _street = raw.customer_street!;
const _street2 = raw.customer_street2!;
const _city = raw.customer_city!;
const _postal_code = raw.customer_postal_code!;
const _province = raw.customer_province!;
const _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(
maybeFromNullableResult(_street, (value) => Street.create(value)),
"customer_street",
errors
);
const customerStreet2 = extractOrPushError(
maybeFromNullableResult(_street2, (value) => Street.create(value)),
"customer_street2",
errors
);
const customerCity = extractOrPushError(
maybeFromNullableResult(_city, (value) => City.create(value)),
"customer_city",
errors
);
const customerProvince = extractOrPushError(
maybeFromNullableResult(_province, (value) => Province.create(value)),
"customer_province",
errors
);
const customerPostalCode = extractOrPushError(
maybeFromNullableResult(_postal_code, (value) => PostalCode.create(value)),
"customer_postal_code",
errors
);
const customerCountry = extractOrPushError(
maybeFromNullableResult(_country, (value) => Country.create(value)),
"customer_country",
errors
);
const createResult = InvoiceRecipient.create({
name: customerName!,
tin: customerTin!,
street: customerStreet!,
street2: customerStreet2!,
city: customerCity!,
postalCode: customerPostalCode!,
province: customerProvince!,
country: customerCountry!,
});
if (createResult.isFailure) {
return Result.fail(
new ValidationErrorCollection("Invoice recipient entity creation failed", [
{ path: "recipient", message: createResult.error.message },
])
);
}
return Result.ok(createResult.data);
}
}

View File

@ -0,0 +1,246 @@
import { type MapperParamsType, SequelizeQueryMapper } from "@erp/core/api";
import {
CurrencyCode,
LanguageCode,
UniqueID,
UtcDate,
ValidationErrorCollection,
type ValidationErrorDetail,
extractOrPushError,
maybeFromNullableResult,
} from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import type { IssuedInvoiceSummary } from "../../../../../../application";
import {
InvoiceAmount,
InvoiceNumber,
InvoiceSerie,
InvoiceStatus,
type VerifactuRecord,
} from "../../../../../../domain";
import type { CustomerInvoiceModel } from "../../../../../common";
import { SequelizeIssuedInvoiceRecipientListMapper } from "./sequelize-issued-invoice-recipient-summary.mapper";
import { SequelizeVerifactuRecordSummaryMapper } from "./sequelize-verifactu-record-summary.mapper";
export class SequelizeIssuedInvoiceSummaryMapper extends SequelizeQueryMapper<
CustomerInvoiceModel,
IssuedInvoiceSummary
> {
private _recipientMapper: SequelizeIssuedInvoiceRecipientListMapper;
private _verifactuMapper: SequelizeVerifactuRecordSummaryMapper;
constructor() {
super();
this._recipientMapper = new SequelizeIssuedInvoiceRecipientListMapper();
this._verifactuMapper = new SequelizeVerifactuRecordSummaryMapper();
}
public mapToReadModel(
raw: CustomerInvoiceModel,
params?: MapperParamsType
): Result<IssuedInvoiceSummary, Error> {
const errors: ValidationErrorDetail[] = [];
// 1) Valores escalares (atributos generales)
const attributes = this._mapAttributesToReadModel(raw, { errors, ...params });
// 2) Recipient (snapshot en la factura o include)
const recipientResult = this._recipientMapper.mapToReadModel(raw, {
errors,
attributes,
...params,
});
if (recipientResult.isFailure) {
errors.push({
path: "recipient",
message: recipientResult.error.message,
});
}
// 4) Verifactu record
let verifactu: Maybe<VerifactuRecord> = Maybe.none();
if (raw.verifactu) {
const verifactuResult = this._verifactuMapper.mapToReadModel(raw.verifactu, {
errors,
...params,
});
if (verifactuResult.isFailure) {
errors.push({
path: "verifactu",
message: verifactuResult.error.message,
});
} else {
verifactu = Maybe.some(verifactuResult.data);
}
}
// 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 [mapToDTO]", 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!,
description: attributes.description!,
reference: attributes.reference!,
customerId: attributes.customerId!,
recipient: recipientResult.data,
languageCode: attributes.languageCode!,
currencyCode: attributes.currencyCode!,
subtotalAmount: attributes.subtotalAmount!,
totalDiscountAmount: attributes.totalDiscountAmount!,
taxableAmount: attributes.taxableAmount!,
taxesAmount: attributes.taxesAmount!,
totalAmount: attributes.totalAmount!,
verifactu,
});
}
private _mapAttributesToReadModel(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(InvoiceStatus.create(raw.status), "status", errors);
const series = extractOrPushError(
maybeFromNullableResult(raw.series, (value) => InvoiceSerie.create(value)),
"serie",
errors
);
const invoiceNumber = extractOrPushError(
InvoiceNumber.create(raw.invoice_number),
"invoice_number",
errors
);
const invoiceDate = extractOrPushError(
UtcDate.createFromISO(raw.invoice_date),
"invoice_date",
errors
);
const operationDate = extractOrPushError(
maybeFromNullableResult(raw.operation_date, (value) => UtcDate.createFromISO(value)),
"operation_date",
errors
);
const reference = extractOrPushError(
maybeFromNullableResult(raw.reference, (value) => Result.ok(String(value))),
"description",
errors
);
const description = extractOrPushError(
maybeFromNullableResult(raw.description, (value) => Result.ok(String(value))),
"description",
errors
);
const languageCode = extractOrPushError(
LanguageCode.create(raw.language_code),
"language_code",
errors
);
const currencyCode = extractOrPushError(
CurrencyCode.create(raw.currency_code),
"currency_code",
errors
);
const subtotalAmount = extractOrPushError(
InvoiceAmount.create({
value: raw.subtotal_amount_value,
currency_code: currencyCode?.code,
}),
"subtotal_amount_value",
errors
);
const totalDiscountAmount = extractOrPushError(
InvoiceAmount.create({
value: raw.total_discount_amount_value,
currency_code: currencyCode?.code,
}),
"total_discount_amount_value",
errors
);
const taxableAmount = extractOrPushError(
InvoiceAmount.create({
value: raw.taxable_amount_value,
currency_code: currencyCode?.code,
}),
"taxable_amount_value",
errors
);
const taxesAmount = extractOrPushError(
InvoiceAmount.create({
value: raw.taxes_amount_value,
currency_code: currencyCode?.code,
}),
"taxes_amount_value",
errors
);
const totalAmount = extractOrPushError(
InvoiceAmount.create({
value: raw.total_amount_value,
currency_code: currencyCode?.code,
}),
"total_amount_value",
errors
);
return {
invoiceId,
companyId,
customerId,
isProforma,
status,
series,
invoiceNumber,
invoiceDate,
operationDate,
reference,
description,
languageCode,
currencyCode,
subtotalAmount,
totalDiscountAmount,
taxableAmount,
taxesAmount,
totalAmount,
};
}
}

View File

@ -0,0 +1,2 @@
export * from "./supplier-invoice.model";
export * from "./supplier-invoice-tax.model";

View File

@ -0,0 +1,241 @@
import {
type CreationOptional,
DataTypes,
type InferAttributes,
type InferCreationAttributes,
Model,
type NonAttribute,
type Sequelize,
} from "sequelize";
import type { SupplierInvoiceModel } from "./supplier-invoice.model";
export type SupplierInvoiceTaxCreationAttributes = InferCreationAttributes<
SupplierInvoiceTaxModel,
{ omit: "invoice" }
>;
export class SupplierInvoiceTaxModel extends Model<
InferAttributes<SupplierInvoiceTaxModel>,
InferCreationAttributes<SupplierInvoiceTaxModel, { omit: "invoice" }>
> {
declare tax_id: string;
declare invoice_id: string;
// Taxable amount (base imponible)
declare taxable_amount_value: number;
declare taxable_amount_scale: number;
declare iva_code: string;
declare iva_percentage_value: number;
declare iva_percentage_scale: number;
declare iva_amount_value: number;
declare iva_amount_scale: number;
declare rec_code: CreationOptional<string | null>;
declare rec_percentage_value: CreationOptional<number | null>;
declare rec_percentage_scale: number;
declare rec_amount_value: number;
declare rec_amount_scale: number;
declare retention_code: CreationOptional<string | null>;
declare retention_percentage_value: CreationOptional<number | null>;
declare retention_percentage_scale: number;
declare retention_amount_value: number;
declare retention_amount_scale: number;
// Total taxes amount / taxes total
declare taxes_amount_value: number;
declare taxes_amount_scale: number;
declare invoice: NonAttribute<SupplierInvoiceModel>;
static associate(database: Sequelize) {
const models = database.models;
const requiredModels = ["SupplierInvoiceModel", "SupplierInvoiceTaxModel"];
for (const name of requiredModels) {
if (!models[name]) {
throw new Error(`[SupplierInvoiceTaxModel.associate] Missing model: ${name}`);
}
}
const { SupplierInvoiceModel, SupplierInvoiceTaxModel } = models;
SupplierInvoiceTaxModel.belongsTo(SupplierInvoiceModel, {
as: "invoice",
foreignKey: "invoice_id",
targetKey: "id",
onDelete: "CASCADE",
onUpdate: "CASCADE",
});
}
static hooks(_database: Sequelize) {
//
}
}
export default (database: Sequelize) => {
SupplierInvoiceTaxModel.init(
{
tax_id: {
type: DataTypes.UUID,
primaryKey: true,
},
invoice_id: {
type: DataTypes.UUID,
allowNull: false,
},
taxable_amount_value: {
type: DataTypes.BIGINT,
allowNull: false,
defaultValue: 0,
},
taxable_amount_scale: {
type: DataTypes.SMALLINT,
allowNull: false,
defaultValue: 4,
},
iva_code: {
type: DataTypes.STRING(40),
allowNull: false,
},
iva_percentage_value: {
type: DataTypes.SMALLINT,
allowNull: false,
},
iva_percentage_scale: {
type: DataTypes.SMALLINT,
allowNull: false,
defaultValue: 2,
},
iva_amount_value: {
type: DataTypes.BIGINT,
allowNull: false,
defaultValue: 0,
},
iva_amount_scale: {
type: DataTypes.SMALLINT,
allowNull: false,
defaultValue: 4,
},
rec_code: {
type: DataTypes.STRING(40),
allowNull: true,
defaultValue: null,
},
rec_percentage_value: {
type: DataTypes.SMALLINT,
allowNull: true,
defaultValue: null,
},
rec_percentage_scale: {
type: DataTypes.SMALLINT,
allowNull: false,
defaultValue: 2,
},
rec_amount_value: {
type: DataTypes.BIGINT,
allowNull: false,
defaultValue: 0,
},
rec_amount_scale: {
type: DataTypes.SMALLINT,
allowNull: false,
defaultValue: 4,
},
retention_code: {
type: DataTypes.STRING(40),
allowNull: true,
defaultValue: null,
},
retention_percentage_value: {
type: DataTypes.SMALLINT,
allowNull: true,
defaultValue: null,
},
retention_percentage_scale: {
type: DataTypes.SMALLINT,
allowNull: false,
defaultValue: 2,
},
retention_amount_value: {
type: DataTypes.BIGINT,
allowNull: false,
defaultValue: 0,
},
retention_amount_scale: {
type: DataTypes.SMALLINT,
allowNull: false,
defaultValue: 4,
},
taxes_amount_value: {
type: DataTypes.BIGINT,
allowNull: false,
defaultValue: 0,
},
taxes_amount_scale: {
type: DataTypes.SMALLINT,
allowNull: false,
defaultValue: 4,
},
},
{
sequelize: database,
modelName: "SupplierInvoiceTaxModel",
tableName: "supplier_invoice_taxes",
underscored: true,
indexes: [
{
name: "idx_supplier_invoice_tax_invoice_id",
fields: ["invoice_id"],
},
{
name: "uq_supplier_invoice_tax_signature",
fields: [
"invoice_id",
"iva_code",
"iva_percentage_value",
"iva_percentage_scale",
"rec_code",
"rec_percentage_value",
"rec_percentage_scale",
"retention_code",
"retention_percentage_value",
"retention_percentage_scale",
],
unique: true,
},
],
whereMergeStrategy: "and",
defaultScope: {},
scopes: {},
}
);
return SupplierInvoiceTaxModel;
};

View File

@ -0,0 +1,333 @@
import {
type CreationOptional,
DataTypes,
type InferAttributes,
type InferCreationAttributes,
Model,
type NonAttribute,
type Sequelize,
} from "sequelize";
import type {
SupplierInvoiceTaxCreationAttributes,
SupplierInvoiceTaxModel,
} from "./supplier-invoice-tax.model";
export type SupplierInvoiceCreationAttributes = InferCreationAttributes<
SupplierInvoiceModel,
{ omit: "taxes" }
> & {
taxes?: SupplierInvoiceTaxCreationAttributes[];
};
export class SupplierInvoiceModel extends Model<
InferAttributes<SupplierInvoiceModel>,
InferCreationAttributes<SupplierInvoiceModel, { omit: "taxes" }>
> {
declare id: string;
declare company_id: string;
declare supplier_id: string;
declare status: string;
declare source_type: string;
declare invoice_number: string;
declare invoice_date: string;
declare due_date: CreationOptional<string | null>;
declare currency_code: string;
// Método de pago
declare payment_method_id: CreationOptional<string | null>;
declare payment_method_description: CreationOptional<string | null>;
declare description: CreationOptional<string | null>;
declare notes: CreationOptional<string | null>;
declare supplier_invoice_category_id: CreationOptional<string | null>;
declare document_id: CreationOptional<string | null>;
declare taxable_amount_value: number;
declare taxable_amount_scale: number;
declare iva_amount_value: number;
declare iva_amount_scale: number;
declare rec_amount_value: number;
declare rec_amount_scale: number;
declare retention_amount_value: number;
declare retention_amount_scale: number;
declare taxes_amount_value: number;
declare taxes_amount_scale: number;
declare total_amount_value: number;
declare total_amount_scale: number;
declare version: number;
declare taxes: NonAttribute<SupplierInvoiceTaxModel[]>;
static associate(database: Sequelize) {
const models = database.models;
const requiredModels = ["SupplierInvoiceModel", "SupplierInvoiceTaxModel"];
for (const name of requiredModels) {
if (!models[name]) {
throw new Error(`[SupplierInvoiceModel.associate] Missing model: ${name}`);
}
}
const { SupplierInvoiceModel, SupplierInvoiceTaxModel } = models;
SupplierInvoiceModel.hasMany(SupplierInvoiceTaxModel, {
as: "taxes",
foreignKey: "invoice_id",
sourceKey: "id",
constraints: true,
onDelete: "CASCADE",
onUpdate: "CASCADE",
});
}
static hooks(_database: Sequelize) {
//
}
}
export default (database: Sequelize) => {
SupplierInvoiceModel.init(
{
id: {
type: DataTypes.UUID,
primaryKey: true,
},
company_id: {
type: DataTypes.UUID,
allowNull: false,
},
supplier_id: {
type: DataTypes.UUID,
allowNull: false,
},
status: {
type: DataTypes.STRING(40),
allowNull: false,
},
source_type: {
type: DataTypes.STRING(40),
allowNull: false,
},
invoice_number: {
type: DataTypes.STRING(80),
allowNull: false,
},
invoice_date: {
type: DataTypes.DATEONLY,
allowNull: false,
},
due_date: {
type: DataTypes.DATEONLY,
allowNull: true,
defaultValue: null,
},
currency_code: {
type: DataTypes.STRING(3),
allowNull: false,
defaultValue: "EUR",
},
payment_method_id: {
type: DataTypes.UUID,
allowNull: true,
defaultValue: null,
},
payment_method_description: {
type: new DataTypes.STRING(),
allowNull: true,
defaultValue: null,
},
description: {
type: DataTypes.STRING(500),
allowNull: true,
defaultValue: null,
},
notes: {
type: DataTypes.TEXT,
allowNull: true,
defaultValue: null,
},
supplier_invoice_category_id: {
type: DataTypes.UUID,
allowNull: true,
defaultValue: null,
},
document_id: {
type: DataTypes.UUID,
allowNull: true,
defaultValue: null,
},
taxable_amount_value: {
type: DataTypes.BIGINT,
allowNull: false,
defaultValue: 0,
},
taxable_amount_scale: {
type: DataTypes.SMALLINT,
allowNull: false,
defaultValue: 4,
},
iva_amount_value: {
type: DataTypes.BIGINT,
allowNull: false,
defaultValue: 0,
},
iva_amount_scale: {
type: DataTypes.SMALLINT,
allowNull: false,
defaultValue: 4,
},
rec_amount_value: {
type: DataTypes.BIGINT,
allowNull: false,
defaultValue: 0,
},
rec_amount_scale: {
type: DataTypes.SMALLINT,
allowNull: false,
defaultValue: 4,
},
retention_amount_value: {
type: DataTypes.BIGINT,
allowNull: false,
defaultValue: 0,
},
retention_amount_scale: {
type: DataTypes.SMALLINT,
allowNull: false,
defaultValue: 4,
},
taxes_amount_value: {
type: DataTypes.BIGINT,
allowNull: false,
defaultValue: 0,
},
taxes_amount_scale: {
type: DataTypes.SMALLINT,
allowNull: false,
defaultValue: 4,
},
total_amount_value: {
type: DataTypes.BIGINT,
allowNull: false,
defaultValue: 0,
},
total_amount_scale: {
type: DataTypes.SMALLINT,
allowNull: false,
defaultValue: 4,
},
version: {
type: DataTypes.INTEGER.UNSIGNED,
allowNull: false,
defaultValue: 1,
},
},
{
sequelize: database,
modelName: "SupplierInvoiceModel",
tableName: "supplier_invoices",
underscored: true,
paranoid: true,
timestamps: true,
createdAt: "created_at",
updatedAt: "updated_at",
deletedAt: "deleted_at",
indexes: [
{
name: "uq_supplier_invoice_company_supplier_number",
fields: ["company_id", "supplier_id", "invoice_number"],
unique: true,
},
{
name: "idx_supplier_invoice_company_date",
fields: ["company_id", "deleted_at", { name: "invoice_date", order: "DESC" }],
},
{
name: "idx_supplier_invoice_company_status",
fields: ["company_id", "status"],
},
{
name: "idx_supplier_invoice_company_supplier",
fields: ["company_id", "supplier_id"],
},
{
name: "idx_supplier_invoice_due_date",
fields: ["due_date"],
},
{
name: "idx_supplier_invoice_document_id",
fields: ["document_id"],
},
{
name: "ft_supplier_invoice",
type: "FULLTEXT",
fields: ["invoice_number", "description", "notes"],
},
],
whereMergeStrategy: "and",
defaultScope: {},
scopes: {},
hooks: {
/**
* Incrementa la versión en cada update exitoso.
*
* Nota:
* - Si aplicas OCC en el repositorio con cláusula WHERE version = x,
* este hook sigue siendo útil.
* - Si prefieres controlar la versión solo desde dominio/repositorio,
* elimínalo para evitar dobles incrementos.
*/
beforeUpdate: (instance) => {
const current = instance.get("version") as number;
instance.set("version", current + 1);
},
},
}
);
return SupplierInvoiceModel;
};

View File

@ -0,0 +1,372 @@
import { EntityNotFoundError, SequelizeRepository, translateSequelizeError } from "@erp/core/api";
import { type Criteria, CriteriaToSequelizeConverter } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
import { type Collection, Result } from "@repo/rdx-utils";
import type { FindOptions, InferAttributes, OrderItem, Sequelize, Transaction } from "sequelize";
import type {
ISupplierInvoiceRepository,
SupplierInvoiceSummary,
} from "../../../../../application";
import type { SupplierInvoice } from "../../../../../domain";
import type {
SequelizeSupplierInvoiceDomainMapper,
SequelizeSupplierInvoiceSummaryMapper,
} from "../mappers";
import { SupplierInvoiceModel, SupplierInvoiceTaxModel } from "../models";
export class SupplierInvoiceRepository
extends SequelizeRepository<SupplierInvoice>
implements ISupplierInvoiceRepository
{
constructor(
private readonly domainMapper: SequelizeSupplierInvoiceDomainMapper,
private readonly summaryMapper: SequelizeSupplierInvoiceSummaryMapper,
database: Sequelize
) {
super({ database });
}
/**
* Crea una nueva factura de proveedor junto con su desglose fiscal.
*/
public async create(
invoice: SupplierInvoice,
transaction?: Transaction
): Promise<Result<void, Error>> {
try {
const dtoResult = this.domainMapper.mapToPersistence(invoice);
if (dtoResult.isFailure()) {
return Result.fail(dtoResult.error);
}
const dto = dtoResult.value;
const { id, taxes, ...createPayload } = dto;
await SupplierInvoiceModel.create(
{
...createPayload,
id,
},
{ transaction }
);
if (Array.isArray(taxes) && taxes.length > 0) {
await SupplierInvoiceTaxModel.bulkCreate(taxes, { transaction });
}
return Result.ok();
} catch (error: unknown) {
return Result.fail(translateSequelizeError(error));
}
}
/**
* Actualiza la cabecera y reemplaza completamente el desglose fiscal.
*
* Nota:
* - Este enfoque es simple y robusto para MVP.
* - Si más adelante necesitas actualización parcial de taxes,
* se puede optimizar.
*/
public async update(
invoice: SupplierInvoice,
transaction?: Transaction
): Promise<Result<void, Error>> {
try {
const dtoResult = this.domainMapper.mapToPersistence(invoice);
if (dtoResult.isFailure()) {
return Result.fail(dtoResult.error);
}
const dto = dtoResult.value;
const { id, company_id, version, taxes, ...updatePayload } = dto;
const [updatedRows] = await SupplierInvoiceModel.update(
{
...updatePayload,
version,
},
{
where: {
id,
company_id,
},
transaction,
}
);
if (updatedRows === 0) {
return Result.fail(new EntityNotFoundError("SupplierInvoice", "id", id));
}
await SupplierInvoiceTaxModel.destroy({
where: { invoice_id: id },
transaction,
});
if (Array.isArray(taxes) && taxes.length > 0) {
await SupplierInvoiceTaxModel.bulkCreate(taxes, { transaction });
}
return Result.ok();
} catch (error: unknown) {
return Result.fail(translateSequelizeError(error));
}
}
/**
* Guarda la factura creando o actualizando según exista previamente.
*
* Nota:
* - Mantengo save() como conveniencia.
* - La política sigue siendo explícita: primero existencia, luego create/update.
*/
public async save(
invoice: SupplierInvoice,
transaction?: Transaction
): Promise<Result<void, Error>> {
const existsResult = await this.existsByIdInCompany(invoice.companyId, invoice.id, transaction);
if (existsResult.isFailure) {
return Result.fail(existsResult.error);
}
if (existsResult.data) {
return this.update(invoice, transaction);
}
return this.create(invoice, transaction);
}
/**
* Comprueba si existe una factura por id dentro de una empresa.
*/
public async existsByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction?: Transaction,
options: FindOptions<InferAttributes<SupplierInvoiceModel>> = {}
): Promise<Result<boolean, Error>> {
try {
const count = await SupplierInvoiceModel.count({
...options,
where: {
id: id.toString(),
company_id: companyId.toString(),
...(options.where ?? {}),
},
transaction,
});
return Result.ok(count > 0);
} catch (error: unknown) {
return Result.fail(translateSequelizeError(error));
}
}
/**
* Busca una factura por id dentro de una empresa.
*/
public async findByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction?: Transaction,
options: FindOptions<InferAttributes<SupplierInvoiceModel>> = {}
): Promise<Result<SupplierInvoice, Error>> {
try {
const normalizedOrder = Array.isArray(options.order)
? options.order
: options.order
? [options.order]
: [];
const normalizedInclude = Array.isArray(options.include)
? options.include
: options.include
? [options.include]
: [];
const mergedOptions: FindOptions<InferAttributes<SupplierInvoiceModel>> = {
...options,
where: {
...(options.where ?? {}),
id: id.toString(),
company_id: companyId.toString(),
},
order: [...normalizedOrder],
include: [
...normalizedInclude,
{
model: SupplierInvoiceTaxModel,
as: "taxes",
required: false,
},
],
transaction,
};
const row = await SupplierInvoiceModel.findOne(mergedOptions);
if (!row) {
return Result.fail(new EntityNotFoundError("SupplierInvoice", "id", id.toString()));
}
return this.domainMapper.mapToDomain(row);
} catch (error: unknown) {
return Result.fail(translateSequelizeError(error));
}
}
/**
* Busca una factura por proveedor + número dentro de una empresa.
*/
public async findBySupplierAndNumberInCompany(
companyId: UniqueID,
supplierId: UniqueID,
invoiceNumber: string,
transaction?: Transaction,
options: FindOptions<InferAttributes<SupplierInvoiceModel>> = {}
): Promise<Result<SupplierInvoice, Error>> {
try {
const normalizedInclude = Array.isArray(options.include)
? options.include
: options.include
? [options.include]
: [];
const row = await SupplierInvoiceModel.findOne({
...options,
where: {
...(options.where ?? {}),
company_id: companyId.toString(),
supplier_id: supplierId.toString(),
invoice_number: invoiceNumber,
},
include: [
...normalizedInclude,
{
model: SupplierInvoiceTaxModel,
as: "taxes",
required: false,
},
],
transaction,
});
if (!row) {
return Result.fail(
new EntityNotFoundError(
"SupplierInvoice",
"invoice_number",
`${supplierId.toString()}:${invoiceNumber}`
)
);
}
return this.domainMapper.mapToDomain(row);
} catch (error: unknown) {
return Result.fail(translateSequelizeError(error));
}
}
/**
* Comprueba si existe una factura por proveedor + número dentro de una empresa.
*/
public async existsBySupplierAndNumberInCompany(
companyId: UniqueID,
supplierId: UniqueID,
invoiceNumber: string,
transaction?: Transaction,
options: FindOptions<InferAttributes<SupplierInvoiceModel>> = {}
): Promise<Result<boolean, Error>> {
try {
const count = await SupplierInvoiceModel.count({
...options,
where: {
...(options.where ?? {}),
company_id: companyId.toString(),
supplier_id: supplierId.toString(),
invoice_number: invoiceNumber,
},
transaction,
});
return Result.ok(count > 0);
} catch (error: unknown) {
return Result.fail(translateSequelizeError(error));
}
}
/**
* Busca facturas de proveedor mediante Criteria.
*/
public async findByCriteriaInCompany(
companyId: UniqueID,
criteria: Criteria,
transaction?: Transaction,
options: FindOptions<InferAttributes<SupplierInvoiceModel>> = {}
): Promise<Result<Collection<SupplierInvoiceSummary>, Error>> {
try {
const criteriaConverter = new CriteriaToSequelizeConverter();
const query = criteriaConverter.convert(criteria, {
searchableFields: ["invoice_number", "description", "internal_notes"],
mappings: {},
allowedFields: ["invoice_date", "due_date", "id", "created_at"],
enableFullText: true,
database: this.database,
strictMode: true,
});
const normalizedOrder = Array.isArray(options.order)
? options.order
: options.order
? [options.order]
: [];
const normalizedInclude = Array.isArray(options.include)
? options.include
: options.include
? [options.include]
: [];
query.where = {
...query.where,
...(options.where ?? {}),
company_id: companyId.toString(),
deleted_at: null,
};
query.order = [...(query.order as OrderItem[]), ...normalizedOrder];
query.include = [
...normalizedInclude,
{
model: SupplierInvoiceTaxModel,
as: "taxes",
required: false,
separate: true,
},
];
const [rows, count] = await Promise.all([
SupplierInvoiceModel.findAll({
...query,
transaction,
}),
SupplierInvoiceModel.count({
where: query.where,
distinct: true,
transaction,
}),
]);
return this.summaryMapper.mapToReadModelCollection(rows, count);
} catch (error: unknown) {
return Result.fail(translateSequelizeError(error));
}
}
}

View File

@ -0,0 +1,33 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@erp/supplier-invoices/*": ["./src/*"]
},
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@ -691,6 +691,46 @@ importers:
specifier: ^5.9.3 specifier: ^5.9.3
version: 5.9.3 version: 5.9.3
modules/supplier-invoices:
dependencies:
'@erp/auth':
specifier: workspace:*
version: link:../auth
'@erp/core':
specifier: workspace:*
version: link:../core
'@repo/i18next':
specifier: workspace:*
version: link:../../packages/i18n
'@repo/rdx-criteria':
specifier: workspace:*
version: link:../../packages/rdx-criteria
'@repo/rdx-ddd':
specifier: workspace:*
version: link:../../packages/rdx-ddd
'@repo/rdx-logger':
specifier: workspace:*
version: link:../../packages/rdx-logger
'@repo/rdx-utils':
specifier: workspace:*
version: link:../../packages/rdx-utils
express:
specifier: ^4.18.2
version: 4.21.2
sequelize:
specifier: ^6.37.5
version: 6.37.7(mysql2@3.15.3)(pg-hstore@2.3.4)
zod:
specifier: ^4.1.11
version: 4.1.12
devDependencies:
'@types/express':
specifier: ^4.17.21
version: 4.17.25
typescript:
specifier: ^5.9.3
version: 5.9.3
packages/i18n: packages/i18n:
dependencies: dependencies:
i18next: i18next: