Supplier invoices (incompleto)
This commit is contained in:
parent
936c440cf3
commit
96ddb559b2
33
modules/supplier-invoices/package.json
Normal file
33
modules/supplier-invoices/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
6
modules/supplier-invoices/src/api/application/index.ts
Normal file
6
modules/supplier-invoices/src/api/application/index.ts
Normal 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";
|
||||
@ -0,0 +1 @@
|
||||
export * from "./supplier-invoice-summary";
|
||||
@ -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;
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export * from "./supplier-invoice-repository.interface";
|
||||
@ -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>>;
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export * from "./supplier-invoice.aggregate";
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./invoice-payment-method";
|
||||
export * from "./supplier-invoice-taxes";
|
||||
@ -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),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./supplier-invoice-tax.entity";
|
||||
export * from "./supplier-invoice-taxes.collection";
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
1
modules/supplier-invoices/src/api/domain/enums/index.ts
Normal file
1
modules/supplier-invoices/src/api/domain/enums/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./supplier-invoice-source-type.enum";
|
||||
@ -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",
|
||||
}
|
||||
3
modules/supplier-invoices/src/api/domain/index.ts
Normal file
3
modules/supplier-invoices/src/api/domain/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./aggregates";
|
||||
export * from "./entities";
|
||||
export * from "./value-objects";
|
||||
@ -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";
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
70
modules/supplier-invoices/src/api/infrastucture/index.ts
Normal file
70
modules/supplier-invoices/src/api/infrastucture/index.ts
Normal 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;
|
||||
@ -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];
|
||||
@ -0,0 +1 @@
|
||||
export * from "./sequelize-issued-invoice-domain.mapper";
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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()),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./domain";
|
||||
export * from "./summary";
|
||||
@ -0,0 +1 @@
|
||||
export * from "./sequelize-issued-invoice-summary.mapper";
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./supplier-invoice.model";
|
||||
export * from "./supplier-invoice-tax.model";
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
};
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
33
modules/supplier-invoices/tsconfig.json
Normal file
33
modules/supplier-invoices/tsconfig.json
Normal 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"]
|
||||
}
|
||||
@ -691,6 +691,46 @@ importers:
|
||||
specifier: ^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:
|
||||
dependencies:
|
||||
i18next:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user