Compare commits
4 Commits
936c440cf3
...
366f90d403
| Author | SHA1 | Date | |
|---|---|---|---|
| 366f90d403 | |||
| 5ac24e161d | |||
| ee70526376 | |||
| 96ddb559b2 |
@ -38,6 +38,7 @@
|
||||
"@erp/customers": "workspace:*",
|
||||
"@erp/customer-invoices": "workspace:*",
|
||||
"@erp/factuges": "workspace:*",
|
||||
"@erp/suppliers": "workspace:*",
|
||||
"@repo/rdx-logger": "workspace:*",
|
||||
"@repo/rdx-utils": "workspace:*",
|
||||
"bcrypt": "^5.1.1",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import customerInvoicesAPIModule from "@erp/customer-invoices/api";
|
||||
import customersAPIModule from "@erp/customers/api";
|
||||
import factuGESAPIModule from "@erp/factuges/api";
|
||||
import suppliersAPIModule from "@erp/suppliers/api";
|
||||
|
||||
import { registerModule } from "./lib";
|
||||
|
||||
@ -9,4 +10,5 @@ export const registerModules = () => {
|
||||
registerModule(customersAPIModule);
|
||||
registerModule(customerInvoicesAPIModule);
|
||||
registerModule(factuGESAPIModule);
|
||||
registerModule(suppliersAPIModule);
|
||||
};
|
||||
|
||||
@ -81,9 +81,10 @@ export interface ICustomer {
|
||||
readonly phoneSecondary: Maybe<PhoneNumber>;
|
||||
readonly mobilePrimary: Maybe<PhoneNumber>;
|
||||
readonly mobileSecondary: Maybe<PhoneNumber>;
|
||||
readonly fax: Maybe<PhoneNumber>;
|
||||
|
||||
readonly fax: Maybe<PhoneNumber>;
|
||||
readonly website: Maybe<URLAddress>;
|
||||
|
||||
readonly legalRecord: Maybe<TextValue>;
|
||||
|
||||
readonly defaultTaxes: CustomerTaxes;
|
||||
|
||||
@ -4,7 +4,7 @@ import type { ICustomerPublicServices } from "./application";
|
||||
import { customersRouter, models } from "./infrastructure";
|
||||
import { buildCustomerPublicServices, buildCustomersDependencies } from "./infrastructure/di";
|
||||
|
||||
export * from "./infrastructure/sequelize";
|
||||
export * from "./infrastructure/persistence/sequelize";
|
||||
|
||||
export const customersAPIModule: IModuleServer = {
|
||||
name: "customers",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { ICatalogs } from "@erp/core/api";
|
||||
|
||||
import { SequelizeCustomerDomainMapper, SequelizeCustomerSummaryMapper } from "../mappers";
|
||||
import { SequelizeCustomerDomainMapper, SequelizeCustomerSummaryMapper } from "../persistence";
|
||||
|
||||
export interface ICustomerPersistenceMappers {
|
||||
domainMapper: SequelizeCustomerDomainMapper;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { Sequelize } from "sequelize";
|
||||
|
||||
import { CustomerRepository } from "../sequelize";
|
||||
import { CustomerRepository } from "../persistence/sequelize";
|
||||
|
||||
import type { ICustomerPersistenceMappers } from "./customer-persistence-mappers.di";
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { ApiErrorMapper, ErrorToApiRule, NotFoundApiError } from "@erp/core/api";
|
||||
import { CustomerNotFoundError, isCustomerNotFoundError } from "../../domain";
|
||||
import { ApiErrorMapper, type ErrorToApiRule, NotFoundApiError } from "@erp/core/api";
|
||||
|
||||
import { type CustomerNotFoundError, isCustomerNotFoundError } from "../../domain";
|
||||
|
||||
// Crea una regla específica (prioridad alta para sobreescribir mensajes)
|
||||
const customerNotFoundRule: ErrorToApiRule = {
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
export * from "./di";
|
||||
export * from "./express";
|
||||
export * from "./mappers";
|
||||
export * from "./sequelize";
|
||||
export * from "./persistence";
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from "./sequelize";
|
||||
@ -1,5 +1,6 @@
|
||||
import customerModelInit from "./models/customer.model";
|
||||
import customerModelInit from "./models/sequelize-customer.model";
|
||||
|
||||
export * from "./mappers";
|
||||
export * from "./models";
|
||||
export * from "./repositories";
|
||||
|
||||
@ -29,8 +29,8 @@ import {
|
||||
CustomerStatus,
|
||||
CustomerTaxes,
|
||||
type ICustomerCreateProps,
|
||||
} from "../../../domain";
|
||||
import type { CustomerCreationAttributes, CustomerModel } from "../../sequelize";
|
||||
} from "../../../../../domain";
|
||||
import type { CustomerCreationAttributes, CustomerModel } from "../../models";
|
||||
|
||||
export class SequelizeCustomerDomainMapper extends SequelizeDomainMapper<
|
||||
CustomerModel,
|
||||
@ -22,9 +22,9 @@ import {
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { CustomerSummary } from "../../../application";
|
||||
import { CustomerStatus } from "../../../domain";
|
||||
import type { CustomerModel } from "../../sequelize";
|
||||
import type { CustomerSummary } from "../../../../../application";
|
||||
import { CustomerStatus } from "../../../../../domain";
|
||||
import type { CustomerModel } from "../../models";
|
||||
|
||||
export class SequelizeCustomerSummaryMapper extends SequelizeQueryMapper<
|
||||
CustomerModel,
|
||||
@ -0,0 +1 @@
|
||||
export * from "./sequelize-customer.model";
|
||||
@ -9,10 +9,10 @@ import type { TINNumber, UniqueID } from "@repo/rdx-ddd";
|
||||
import { type Collection, Result } from "@repo/rdx-utils";
|
||||
import type { FindOptions, InferAttributes, OrderItem, Sequelize, Transaction } from "sequelize";
|
||||
|
||||
import type { CustomerSummary, ICustomerRepository } from "../../../application";
|
||||
import type { Customer } from "../../../domain";
|
||||
import type { CustomerSummary, ICustomerRepository } from "../../../../application";
|
||||
import type { Customer } from "../../../../domain";
|
||||
import type { SequelizeCustomerDomainMapper, SequelizeCustomerSummaryMapper } from "../../mappers";
|
||||
import { CustomerModel } from "../models/customer.model";
|
||||
import { CustomerModel } from "../models/sequelize-customer.model";
|
||||
|
||||
export class CustomerRepository
|
||||
extends SequelizeRepository<Customer>
|
||||
@ -1 +0,0 @@
|
||||
export * from "./customer.model";
|
||||
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,369 @@
|
||||
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"]
|
||||
}
|
||||
33
modules/supplier/package.json
Normal file
33
modules/supplier/package.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "@erp/suppliers",
|
||||
"description": "Suppliers",
|
||||
"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/src/api/application/di/index.ts
Normal file
6
modules/supplier/src/api/application/di/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export * from "./supplier-creator.di";
|
||||
export * from "./supplier-finder.di";
|
||||
export * from "./supplier-input-mappers.di";
|
||||
export * from "./supplier-snapshot-builders.di";
|
||||
export * from "./supplier-updater.di";
|
||||
export * from "./supplier-use-cases.di";
|
||||
@ -0,0 +1,12 @@
|
||||
import type { ISupplierRepository } from "../repositories";
|
||||
import { type ISupplierCreator, SupplierCreator } from "../services";
|
||||
|
||||
export const buildSupplierCreator = (params: {
|
||||
repository: ISupplierRepository;
|
||||
}): ISupplierCreator => {
|
||||
const { repository } = params;
|
||||
|
||||
return new SupplierCreator({
|
||||
repository,
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,8 @@
|
||||
import type { ISupplierRepository } from "../repositories";
|
||||
import { type ISupplierFinder, SupplierFinder } from "../services";
|
||||
|
||||
export function buildSupplierFinder(params: { repository: ISupplierRepository }): ISupplierFinder {
|
||||
const { repository } = params;
|
||||
|
||||
return new SupplierFinder(repository);
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
import type { ICatalogs } from "@erp/core/api";
|
||||
|
||||
import {
|
||||
CreateSupplierInputMapper,
|
||||
type ICreateSupplierInputMapper,
|
||||
type IUpdateSupplierInputMapper,
|
||||
UpdateSupplierInputMapper,
|
||||
} from "../mappers";
|
||||
|
||||
export interface ISupplierInputMappers {
|
||||
createInputMapper: ICreateSupplierInputMapper;
|
||||
updateInputMapper: IUpdateSupplierInputMapper;
|
||||
}
|
||||
|
||||
export const buildSupplierInputMappers = (catalogs: ICatalogs): ISupplierInputMappers => {
|
||||
const { taxCatalog } = catalogs;
|
||||
|
||||
// Mappers el DTO a las props validadas (SupplierProps) y luego construir agregado
|
||||
const createInputMapper = new CreateSupplierInputMapper({ taxCatalog });
|
||||
const updateInputMapper = new UpdateSupplierInputMapper();
|
||||
|
||||
return {
|
||||
createInputMapper,
|
||||
updateInputMapper,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,11 @@
|
||||
import { SupplierFullSnapshotBuilder, SupplierSummarySnapshotBuilder } from "../snapshot-builders";
|
||||
|
||||
export function buildSupplierSnapshotBuilders() {
|
||||
const fullSnapshotBuilder = new SupplierFullSnapshotBuilder();
|
||||
const summarySnapshotBuilder = new SupplierSummarySnapshotBuilder();
|
||||
|
||||
return {
|
||||
full: fullSnapshotBuilder,
|
||||
summary: summarySnapshotBuilder,
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import type { ISupplierRepository } from "../repositories";
|
||||
import { type ISupplierUpdater, SupplierUpdater } from "../services";
|
||||
|
||||
export const buildSupplierUpdater = (params: {
|
||||
repository: ISupplierRepository;
|
||||
}): ISupplierUpdater => {
|
||||
const { repository } = params;
|
||||
|
||||
return new SupplierUpdater({
|
||||
repository,
|
||||
});
|
||||
};
|
||||
@ -0,0 +1,95 @@
|
||||
import type { ITransactionManager } from "@erp/core/api";
|
||||
|
||||
import type { ICreateSupplierInputMapper, IUpdateSupplierInputMapper } from "../mappers";
|
||||
import type { ISupplierCreator, ISupplierFinder, ISupplierUpdater } from "../services";
|
||||
import type {
|
||||
ISupplierFullSnapshotBuilder,
|
||||
ISupplierSummarySnapshotBuilder,
|
||||
} from "../snapshot-builders";
|
||||
import {
|
||||
CreateSupplierUseCase,
|
||||
GetSupplierByIdUseCase,
|
||||
ListSuppliersUseCase,
|
||||
UpdateSupplierUseCase,
|
||||
} from "../use-cases";
|
||||
|
||||
export function buildGetSupplierByIdUseCase(deps: {
|
||||
finder: ISupplierFinder;
|
||||
fullSnapshotBuilder: ISupplierFullSnapshotBuilder;
|
||||
transactionManager: ITransactionManager;
|
||||
}) {
|
||||
return new GetSupplierByIdUseCase(deps.finder, deps.fullSnapshotBuilder, deps.transactionManager);
|
||||
}
|
||||
|
||||
export function buildListSuppliersUseCase(deps: {
|
||||
finder: ISupplierFinder;
|
||||
summarySnapshotBuilder: ISupplierSummarySnapshotBuilder;
|
||||
transactionManager: ITransactionManager;
|
||||
}) {
|
||||
return new ListSuppliersUseCase(
|
||||
deps.finder,
|
||||
deps.summarySnapshotBuilder,
|
||||
deps.transactionManager
|
||||
);
|
||||
}
|
||||
|
||||
export function buildCreateSupplierUseCase(deps: {
|
||||
creator: ISupplierCreator;
|
||||
dtoMapper: ICreateSupplierInputMapper;
|
||||
fullSnapshotBuilder: ISupplierFullSnapshotBuilder;
|
||||
transactionManager: ITransactionManager;
|
||||
}) {
|
||||
return new CreateSupplierUseCase({
|
||||
dtoMapper: deps.dtoMapper,
|
||||
creator: deps.creator,
|
||||
fullSnapshotBuilder: deps.fullSnapshotBuilder,
|
||||
transactionManager: deps.transactionManager,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildUpdateSupplierUseCase(deps: {
|
||||
updater: ISupplierUpdater;
|
||||
dtoMapper: IUpdateSupplierInputMapper;
|
||||
fullSnapshotBuilder: ISupplierFullSnapshotBuilder;
|
||||
transactionManager: ITransactionManager;
|
||||
}) {
|
||||
return new UpdateSupplierUseCase({
|
||||
dtoMapper: deps.dtoMapper,
|
||||
updater: deps.updater,
|
||||
fullSnapshotBuilder: deps.fullSnapshotBuilder,
|
||||
transactionManager: deps.transactionManager,
|
||||
});
|
||||
}
|
||||
|
||||
/*export function buildReportSupplierUseCase(deps: {
|
||||
finder: ISupplierFinder;
|
||||
fullSnapshotBuilder: ISupplierFullSnapshotBuilder;
|
||||
reportSnapshotBuilder: ISupplierReportSnapshotBuilder;
|
||||
documentService: SupplierDocumentGeneratorService;
|
||||
transactionManager: ITransactionManager;
|
||||
}) {
|
||||
return new ReportSupplierUseCase(
|
||||
deps.finder,
|
||||
deps.fullSnapshotBuilder,
|
||||
deps.reportSnapshotBuilder,
|
||||
deps.documentService,
|
||||
deps.transactionManager
|
||||
);
|
||||
}*/
|
||||
|
||||
/*
|
||||
|
||||
export function buildDeleteSupplierUseCase(deps: { finder: ISupplierFinder }) {
|
||||
return new DeleteSupplierUseCase(deps.finder);
|
||||
}
|
||||
|
||||
export function buildIssueSupplierUseCase(deps: { finder: ISupplierFinder }) {
|
||||
return new IssueSupplierUseCase(deps.finder);
|
||||
}
|
||||
|
||||
export function buildChangeStatusSupplierUseCase(deps: {
|
||||
finder: ISupplierFinder;
|
||||
transactionManager: ITransactionManager;
|
||||
}) {
|
||||
return new ChangeStatusSupplierUseCase(deps.finder, deps.transactionManager);
|
||||
}*/
|
||||
6
modules/supplier/src/api/application/index.ts
Normal file
6
modules/supplier/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,217 @@
|
||||
import type { JsonTaxCatalogProvider } from "@erp/core";
|
||||
import {
|
||||
City,
|
||||
Country,
|
||||
CurrencyCode,
|
||||
DomainError,
|
||||
EmailAddress,
|
||||
LanguageCode,
|
||||
Name,
|
||||
PhoneNumber,
|
||||
type PostalAddressProps,
|
||||
PostalCode,
|
||||
Province,
|
||||
Street,
|
||||
TINNumber,
|
||||
URLAddress,
|
||||
UniqueID,
|
||||
ValidationErrorCollection,
|
||||
type ValidationErrorDetail,
|
||||
extractOrPushError,
|
||||
maybeFromNullableResult,
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { CreateSupplierRequestDTO } from "../../../common";
|
||||
import { type ISupplierCreateProps, SupplierStatus } from "../../domain";
|
||||
|
||||
export interface ICreateSupplierInputMapper {
|
||||
map(
|
||||
dto: CreateSupplierRequestDTO,
|
||||
params: { companyId: UniqueID }
|
||||
): Result<{ id: UniqueID; props: ISupplierCreateProps }>;
|
||||
}
|
||||
|
||||
export class CreateSupplierInputMapper implements ICreateSupplierInputMapper {
|
||||
private readonly taxCatalog: JsonTaxCatalogProvider;
|
||||
|
||||
constructor(params: { taxCatalog: JsonTaxCatalogProvider }) {
|
||||
this.taxCatalog = params.taxCatalog;
|
||||
}
|
||||
|
||||
public map(
|
||||
dto: CreateSupplierRequestDTO,
|
||||
params: { companyId: UniqueID }
|
||||
): Result<{ id: UniqueID; props: ISupplierCreateProps }> {
|
||||
try {
|
||||
const errors: ValidationErrorDetail[] = [];
|
||||
const { companyId } = params;
|
||||
|
||||
const supplierId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
|
||||
|
||||
const status = SupplierStatus.active();
|
||||
const isCompany = dto.is_company === "true";
|
||||
|
||||
const reference = extractOrPushError(
|
||||
maybeFromNullableResult(dto.reference, (value) => Name.create(value)),
|
||||
"reference",
|
||||
errors
|
||||
);
|
||||
|
||||
const name = extractOrPushError(Name.create(dto.name), "name", errors);
|
||||
|
||||
const tradeName = extractOrPushError(
|
||||
maybeFromNullableResult(dto.trade_name, (value) => Name.create(value)),
|
||||
"trade_name",
|
||||
errors
|
||||
);
|
||||
|
||||
const tinNumber = extractOrPushError(
|
||||
maybeFromNullableResult(dto.tin, (value) => TINNumber.create(value)),
|
||||
"tin",
|
||||
errors
|
||||
);
|
||||
|
||||
const street = extractOrPushError(
|
||||
maybeFromNullableResult(dto.street, (value) => Street.create(value)),
|
||||
"street",
|
||||
errors
|
||||
);
|
||||
|
||||
const street2 = extractOrPushError(
|
||||
maybeFromNullableResult(dto.street2, (value) => Street.create(value)),
|
||||
"street2",
|
||||
errors
|
||||
);
|
||||
|
||||
const city = extractOrPushError(
|
||||
maybeFromNullableResult(dto.city, (value) => City.create(value)),
|
||||
"city",
|
||||
errors
|
||||
);
|
||||
|
||||
const province = extractOrPushError(
|
||||
maybeFromNullableResult(dto.province, (value) => Province.create(value)),
|
||||
"province",
|
||||
errors
|
||||
);
|
||||
|
||||
const postalCode = extractOrPushError(
|
||||
maybeFromNullableResult(dto.postal_code, (value) => PostalCode.create(value)),
|
||||
"postal_code",
|
||||
errors
|
||||
);
|
||||
|
||||
const country = extractOrPushError(
|
||||
maybeFromNullableResult(dto.country, (value) => Country.create(value)),
|
||||
"country",
|
||||
errors
|
||||
);
|
||||
|
||||
const primaryEmailAddress = extractOrPushError(
|
||||
maybeFromNullableResult(dto.email_primary, (value) => EmailAddress.create(value)),
|
||||
"email_primary",
|
||||
errors
|
||||
);
|
||||
|
||||
const secondaryEmailAddress = extractOrPushError(
|
||||
maybeFromNullableResult(dto.email_secondary, (value) => EmailAddress.create(value)),
|
||||
"email_secondary",
|
||||
errors
|
||||
);
|
||||
|
||||
const primaryPhoneNumber = extractOrPushError(
|
||||
maybeFromNullableResult(dto.phone_primary, (value) => PhoneNumber.create(value)),
|
||||
"phone_primary",
|
||||
errors
|
||||
);
|
||||
|
||||
const secondaryPhoneNumber = extractOrPushError(
|
||||
maybeFromNullableResult(dto.phone_secondary, (value) => PhoneNumber.create(value)),
|
||||
"phone_secondary",
|
||||
errors
|
||||
);
|
||||
|
||||
const primaryMobileNumber = extractOrPushError(
|
||||
maybeFromNullableResult(dto.mobile_primary, (value) => PhoneNumber.create(value)),
|
||||
"mobile_primary",
|
||||
errors
|
||||
);
|
||||
|
||||
const secondaryMobileNumber = extractOrPushError(
|
||||
maybeFromNullableResult(dto.mobile_secondary, (value) => PhoneNumber.create(value)),
|
||||
"mobile_secondary",
|
||||
errors
|
||||
);
|
||||
|
||||
const faxNumber = extractOrPushError(
|
||||
maybeFromNullableResult(dto.fax, (value) => PhoneNumber.create(value)),
|
||||
"fax",
|
||||
errors
|
||||
);
|
||||
|
||||
const website = extractOrPushError(
|
||||
maybeFromNullableResult(dto.website, (value) => URLAddress.create(value)),
|
||||
"website",
|
||||
errors
|
||||
);
|
||||
|
||||
const languageCode = extractOrPushError(
|
||||
LanguageCode.create(dto.language_code),
|
||||
"language_code",
|
||||
errors
|
||||
);
|
||||
|
||||
const currencyCode = extractOrPushError(
|
||||
CurrencyCode.create(dto.currency_code),
|
||||
"currency_code",
|
||||
errors
|
||||
);
|
||||
|
||||
if (errors.length > 0) {
|
||||
return Result.fail(new ValidationErrorCollection("Supplier props mapping failed", errors));
|
||||
}
|
||||
|
||||
const postalAddressProps: PostalAddressProps = {
|
||||
street: street!,
|
||||
street2: street2!,
|
||||
city: city!,
|
||||
postalCode: postalCode!,
|
||||
province: province!,
|
||||
country: country!,
|
||||
};
|
||||
|
||||
const supplierProps: ISupplierCreateProps = {
|
||||
companyId,
|
||||
status: status!,
|
||||
reference: reference!,
|
||||
|
||||
isCompany: isCompany,
|
||||
name: name!,
|
||||
tradeName: tradeName!,
|
||||
tin: tinNumber!,
|
||||
|
||||
address: postalAddressProps!,
|
||||
|
||||
emailPrimary: primaryEmailAddress!,
|
||||
emailSecondary: secondaryEmailAddress!,
|
||||
|
||||
phonePrimary: primaryPhoneNumber!,
|
||||
phoneSecondary: secondaryPhoneNumber!,
|
||||
|
||||
mobilePrimary: primaryMobileNumber!,
|
||||
mobileSecondary: secondaryMobileNumber!,
|
||||
|
||||
fax: faxNumber!,
|
||||
website: website!,
|
||||
|
||||
languageCode: languageCode!,
|
||||
currencyCode: currencyCode!,
|
||||
};
|
||||
|
||||
return Result.ok({ id: supplierId!, props: supplierProps });
|
||||
} catch (err: unknown) {
|
||||
return Result.fail(new DomainError("Supplier props mapping failed", { cause: err }));
|
||||
}
|
||||
}
|
||||
}
|
||||
2
modules/supplier/src/api/application/mappers/index.ts
Normal file
2
modules/supplier/src/api/application/mappers/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from "./create-supplier-input.mapper";
|
||||
export * from "./update-supplier-input.mapper";
|
||||
@ -0,0 +1,262 @@
|
||||
import {
|
||||
City,
|
||||
Country,
|
||||
CurrencyCode,
|
||||
DomainError,
|
||||
EmailAddress,
|
||||
LanguageCode,
|
||||
Name,
|
||||
PhoneNumber,
|
||||
type PostalAddressPatchProps,
|
||||
PostalCode,
|
||||
Province,
|
||||
Street,
|
||||
TINNumber,
|
||||
URLAddress,
|
||||
type UniqueID,
|
||||
ValidationErrorCollection,
|
||||
type ValidationErrorDetail,
|
||||
extractOrPushError,
|
||||
maybeFromNullableResult,
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Result, isNullishOrEmpty, toPatchField } from "@repo/rdx-utils";
|
||||
|
||||
import type { UpdateSupplierByIdRequestDTO } from "../../../common";
|
||||
import type { SupplierPatchProps } from "../../domain";
|
||||
|
||||
/**
|
||||
* UpdateSupplierInputMapper
|
||||
* Convierte el DTO a las props validadas (SupplierProps).
|
||||
* No construye directamente el agregado.
|
||||
* Tri-estado:
|
||||
* - campo omitido → no se cambia
|
||||
* - campo con valor null/"" → se quita el valor -> set(None()),
|
||||
* - campo con valor no-vacío → se pone el nuevo valor -> set(Some(VO)).
|
||||
*
|
||||
* @param dto - DTO con los datos a cambiar en el cliente
|
||||
* @returns Cambios en las propiedades del cliente
|
||||
*
|
||||
*/
|
||||
|
||||
export interface IUpdateSupplierInputMapper {
|
||||
map(
|
||||
dto: UpdateSupplierByIdRequestDTO,
|
||||
params: { companyId: UniqueID }
|
||||
): Result<SupplierPatchProps>;
|
||||
}
|
||||
|
||||
export class UpdateSupplierInputMapper implements IUpdateSupplierInputMapper {
|
||||
public map(dto: UpdateSupplierByIdRequestDTO, params: { companyId: UniqueID }) {
|
||||
try {
|
||||
const errors: ValidationErrorDetail[] = [];
|
||||
const supplierPatchProps: SupplierPatchProps = {};
|
||||
|
||||
toPatchField(dto.reference).ifSet((reference) => {
|
||||
supplierPatchProps.reference = extractOrPushError(
|
||||
maybeFromNullableResult(reference, (value) => Name.create(value)),
|
||||
"reference",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.is_company).ifSet((is_company) => {
|
||||
if (isNullishOrEmpty(is_company)) {
|
||||
errors.push({ path: "is_company", message: "is_company cannot be empty" });
|
||||
return;
|
||||
}
|
||||
supplierPatchProps.isCompany = extractOrPushError(
|
||||
Result.ok(Boolean(is_company!)),
|
||||
"is_company",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.name).ifSet((name) => {
|
||||
if (isNullishOrEmpty(name)) {
|
||||
errors.push({ path: "name", message: "Name cannot be empty" });
|
||||
return;
|
||||
}
|
||||
supplierPatchProps.name = extractOrPushError(Name.create(name!), "name", errors);
|
||||
});
|
||||
|
||||
toPatchField(dto.trade_name).ifSet((trade_name) => {
|
||||
supplierPatchProps.tradeName = extractOrPushError(
|
||||
maybeFromNullableResult(trade_name, (value) => Name.create(value)),
|
||||
"trade_name",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.tin).ifSet((tin) => {
|
||||
supplierPatchProps.tin = extractOrPushError(
|
||||
maybeFromNullableResult(tin, (value) => TINNumber.create(value)),
|
||||
"tin",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.email_primary).ifSet((email_primary) => {
|
||||
supplierPatchProps.emailPrimary = extractOrPushError(
|
||||
maybeFromNullableResult(email_primary, (value) => EmailAddress.create(value)),
|
||||
"email_primary",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.email_secondary).ifSet((email_secondary) => {
|
||||
supplierPatchProps.emailSecondary = extractOrPushError(
|
||||
maybeFromNullableResult(email_secondary, (value) => EmailAddress.create(value)),
|
||||
"email_secondary",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.mobile_primary).ifSet((mobile_primary) => {
|
||||
supplierPatchProps.mobilePrimary = extractOrPushError(
|
||||
maybeFromNullableResult(mobile_primary, (value) => PhoneNumber.create(value)),
|
||||
"mobile_primary",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.mobile_secondary).ifSet((mobile_secondary) => {
|
||||
supplierPatchProps.mobilePrimary = extractOrPushError(
|
||||
maybeFromNullableResult(mobile_secondary, (value) => PhoneNumber.create(value)),
|
||||
"mobile_secondary",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.phone_primary).ifSet((phone_primary) => {
|
||||
supplierPatchProps.phonePrimary = extractOrPushError(
|
||||
maybeFromNullableResult(phone_primary, (value) => PhoneNumber.create(value)),
|
||||
"phone_primary",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.phone_secondary).ifSet((phone_secondary) => {
|
||||
supplierPatchProps.phoneSecondary = extractOrPushError(
|
||||
maybeFromNullableResult(phone_secondary, (value) => PhoneNumber.create(value)),
|
||||
"phone_secondary",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.fax).ifSet((fax) => {
|
||||
supplierPatchProps.fax = extractOrPushError(
|
||||
maybeFromNullableResult(fax, (value) => PhoneNumber.create(value)),
|
||||
"fax",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.website).ifSet((website) => {
|
||||
supplierPatchProps.website = extractOrPushError(
|
||||
maybeFromNullableResult(website, (value) => URLAddress.create(value)),
|
||||
"website",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.language_code).ifSet((languageCode) => {
|
||||
if (isNullishOrEmpty(languageCode)) {
|
||||
errors.push({ path: "language_code", message: "Language code cannot be empty" });
|
||||
return;
|
||||
}
|
||||
|
||||
supplierPatchProps.languageCode = extractOrPushError(
|
||||
LanguageCode.create(languageCode!),
|
||||
"language_code",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.currency_code).ifSet((currencyCode) => {
|
||||
if (isNullishOrEmpty(currencyCode)) {
|
||||
errors.push({ path: "currency_code", message: "Currency code cannot be empty" });
|
||||
return;
|
||||
}
|
||||
|
||||
supplierPatchProps.currencyCode = extractOrPushError(
|
||||
CurrencyCode.create(currencyCode!),
|
||||
"currency_code",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
// PostalAddress
|
||||
const addressPatchProps = this.mapPostalAddress(dto, errors);
|
||||
if (addressPatchProps) {
|
||||
supplierPatchProps.address = addressPatchProps;
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return Result.fail(
|
||||
new ValidationErrorCollection("Supplier props mapping failed (update)", errors)
|
||||
);
|
||||
}
|
||||
|
||||
return Result.ok(supplierPatchProps);
|
||||
} catch (err: unknown) {
|
||||
return Result.fail(new DomainError("Supplier props mapping failed", { cause: err }));
|
||||
}
|
||||
}
|
||||
|
||||
public mapPostalAddress(
|
||||
dto: UpdateSupplierByIdRequestDTO,
|
||||
errors: ValidationErrorDetail[]
|
||||
): PostalAddressPatchProps | undefined {
|
||||
const postalAddressPatchProps: PostalAddressPatchProps = {};
|
||||
|
||||
toPatchField(dto.street).ifSet((street) => {
|
||||
postalAddressPatchProps.street = extractOrPushError(
|
||||
maybeFromNullableResult(street, (value) => Street.create(value)),
|
||||
"street",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.street2).ifSet((street2) => {
|
||||
postalAddressPatchProps.street2 = extractOrPushError(
|
||||
maybeFromNullableResult(street2, (value) => Street.create(value)),
|
||||
"street2",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.city).ifSet((city) => {
|
||||
postalAddressPatchProps.city = extractOrPushError(
|
||||
maybeFromNullableResult(city, (value) => City.create(value)),
|
||||
"city",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.province).ifSet((province) => {
|
||||
postalAddressPatchProps.province = extractOrPushError(
|
||||
maybeFromNullableResult(province, (value) => Province.create(value)),
|
||||
"province",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.postal_code).ifSet((postalCode) => {
|
||||
postalAddressPatchProps.postalCode = extractOrPushError(
|
||||
maybeFromNullableResult(postalCode, (value) => PostalCode.create(value)),
|
||||
"postal_code",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
toPatchField(dto.country).ifSet((country) => {
|
||||
postalAddressPatchProps.country = extractOrPushError(
|
||||
maybeFromNullableResult(country, (value) => Country.create(value)),
|
||||
"country",
|
||||
errors
|
||||
);
|
||||
});
|
||||
|
||||
return Object.keys(postalAddressPatchProps).length > 0 ? postalAddressPatchProps : undefined;
|
||||
}
|
||||
}
|
||||
1
modules/supplier/src/api/application/models/index.ts
Normal file
1
modules/supplier/src/api/application/models/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./supplier-summary";
|
||||
@ -0,0 +1,39 @@
|
||||
import type {
|
||||
CurrencyCode,
|
||||
EmailAddress,
|
||||
LanguageCode,
|
||||
Name,
|
||||
PhoneNumber,
|
||||
PostalAddress,
|
||||
TINNumber,
|
||||
URLAddress,
|
||||
UniqueID,
|
||||
} from "@repo/rdx-ddd";
|
||||
import type { Maybe } from "@repo/rdx-utils";
|
||||
|
||||
export type SupplierSummary = {
|
||||
id: UniqueID;
|
||||
companyId: UniqueID;
|
||||
isActive: boolean;
|
||||
reference: Maybe<Name>;
|
||||
|
||||
isCompany: boolean;
|
||||
name: Name;
|
||||
tradeName: Maybe<Name>;
|
||||
tin: Maybe<TINNumber>;
|
||||
|
||||
address: PostalAddress;
|
||||
|
||||
emailPrimary: Maybe<EmailAddress>;
|
||||
emailSecondary: Maybe<EmailAddress>;
|
||||
phonePrimary: Maybe<PhoneNumber>;
|
||||
phoneSecondary: Maybe<PhoneNumber>;
|
||||
mobilePrimary: Maybe<PhoneNumber>;
|
||||
mobileSecondary: Maybe<PhoneNumber>;
|
||||
|
||||
fax: Maybe<PhoneNumber>;
|
||||
website: Maybe<URLAddress>;
|
||||
|
||||
languageCode: LanguageCode;
|
||||
currencyCode: CurrencyCode;
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export * from './supplier-repository.interface';
|
||||
@ -0,0 +1,82 @@
|
||||
import type { Criteria } from "@repo/rdx-criteria/server";
|
||||
import type { TINNumber, UniqueID } from "@repo/rdx-ddd";
|
||||
import type { Collection, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { Supplier } from "../../domain/aggregates";
|
||||
import type { SupplierSummary } from "../models";
|
||||
|
||||
/**
|
||||
* Interfaz del repositorio para el agregado `Supplier`.
|
||||
* El escopado multitenant está representado por `companyId`.
|
||||
*/
|
||||
export interface ISupplierRepository {
|
||||
/**
|
||||
*
|
||||
* Crea un nuevo proveedor
|
||||
*
|
||||
* @param supplier - El proveedor nuevo a guardar.
|
||||
* @param transaction - Transacción activa para la operación.
|
||||
* @returns Result<void, Error>
|
||||
*/
|
||||
create(supplier: Supplier, transaction: unknown): Promise<Result<void, Error>>;
|
||||
|
||||
/**
|
||||
* Actualiza un proveedor existente.
|
||||
*
|
||||
* @param supplier - El proveedor a actualizar.
|
||||
* @param transaction - Transacción activa para la operación.
|
||||
* @returns Result<void, Error>
|
||||
*/
|
||||
update(supplier: Supplier, transaction: unknown): Promise<Result<void, Error>>;
|
||||
|
||||
/**
|
||||
* Comprueba si existe un Supplier con un `id` dentro de una `company`.
|
||||
*/
|
||||
existsByIdInCompany(
|
||||
companyId: UniqueID,
|
||||
id: UniqueID,
|
||||
transaction: unknown
|
||||
): Promise<Result<boolean, Error>>;
|
||||
|
||||
/**
|
||||
* Recupera un Supplier por su ID y companyId.
|
||||
* Devuelve un `NotFoundError` si no se encuentra.
|
||||
*/
|
||||
getByIdInCompany(
|
||||
companyId: UniqueID,
|
||||
id: UniqueID,
|
||||
transaction: unknown
|
||||
): Promise<Result<Supplier, Error>>;
|
||||
|
||||
/**
|
||||
* Recupera un Supplier por su TIN y companyId.
|
||||
* Devuelve un `NotFoundError` si no se encuentra.
|
||||
*/
|
||||
getByTINInCompany(
|
||||
companyId: UniqueID,
|
||||
tin: TINNumber,
|
||||
transaction?: unknown
|
||||
): Promise<Result<Supplier, Error>>;
|
||||
|
||||
/**
|
||||
* Recupera múltiples suppliers dentro de una empresa
|
||||
* según un criterio dinámico (búsqueda, paginación, etc.).
|
||||
* El resultado está encapsulado en un objeto `Collection<T>`.
|
||||
*/
|
||||
findByCriteriaInCompany(
|
||||
companyId: UniqueID,
|
||||
criteria: Criteria,
|
||||
transaction: unknown
|
||||
): Promise<Result<Collection<SupplierSummary>, Error>>;
|
||||
|
||||
/**
|
||||
* Elimina un Supplier por su ID, dentro de una empresa.
|
||||
* Retorna `void` si se elimina correctamente, o `NotFoundError` si no existía.
|
||||
*
|
||||
*/
|
||||
deleteByIdInCompany(
|
||||
companyId: UniqueID,
|
||||
id: UniqueID,
|
||||
transaction: unknown
|
||||
): Promise<Result<boolean, Error>>;
|
||||
}
|
||||
4
modules/supplier/src/api/application/services/index.ts
Normal file
4
modules/supplier/src/api/application/services/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from "./supplier-creator.service";
|
||||
export * from "./supplier-finder.service";
|
||||
export * from "./supplier-public-services.interface";
|
||||
export * from "./supplier-updater.service";
|
||||
@ -0,0 +1,70 @@
|
||||
import { DuplicateEntityError } from "@erp/core/api";
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import { type ISupplierCreateProps, Supplier } from "../../domain";
|
||||
import type { ISupplierRepository } from "../repositories";
|
||||
import { SupplierNotExistsInCompanySpecification } from "../specs";
|
||||
|
||||
export interface ISupplierCreator {
|
||||
create(params: {
|
||||
companyId: UniqueID;
|
||||
id: UniqueID;
|
||||
props: ISupplierCreateProps;
|
||||
unknown: unknown;
|
||||
}): Promise<Result<Supplier, Error>>;
|
||||
}
|
||||
|
||||
type SupplierCreatorDeps = {
|
||||
repository: ISupplierRepository;
|
||||
};
|
||||
|
||||
export class SupplierCreator implements ISupplierCreator {
|
||||
private readonly repository: ISupplierRepository;
|
||||
|
||||
constructor(deps: SupplierCreatorDeps) {
|
||||
this.repository = deps.repository;
|
||||
}
|
||||
|
||||
async create(params: {
|
||||
companyId: UniqueID;
|
||||
id: UniqueID;
|
||||
props: ISupplierCreateProps;
|
||||
unknown: unknown;
|
||||
}): Promise<Result<Supplier, Error>> {
|
||||
const { companyId, id, props, unknown } = params;
|
||||
|
||||
// 1. Verificar unicidad
|
||||
const spec = new SupplierNotExistsInCompanySpecification(this.repository, companyId, unknown);
|
||||
|
||||
const isNew = await spec.isSatisfiedBy(id);
|
||||
|
||||
if (!isNew) {
|
||||
return Result.fail(new DuplicateEntityError("Supplier", "id", String(id)));
|
||||
}
|
||||
|
||||
// 2. Crear agregado
|
||||
const createResult = Supplier.create(
|
||||
{
|
||||
...props,
|
||||
companyId,
|
||||
},
|
||||
id
|
||||
);
|
||||
|
||||
if (createResult.isFailure) {
|
||||
return createResult;
|
||||
}
|
||||
|
||||
const newSupplier = createResult.data;
|
||||
|
||||
// 3. Persistir agregado
|
||||
const saveResult = await this.repository.create(newSupplier, unknown);
|
||||
|
||||
if (saveResult.isFailure) {
|
||||
return Result.fail(saveResult.error);
|
||||
}
|
||||
|
||||
return Result.ok(newSupplier);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,69 @@
|
||||
import type { Criteria } from "@repo/rdx-criteria/server";
|
||||
import type { TINNumber, UniqueID } from "@repo/rdx-ddd";
|
||||
import type { Collection, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { Supplier } from "../../domain";
|
||||
import type { SupplierSummary } from "../models";
|
||||
import type { ISupplierRepository } from "../repositories";
|
||||
|
||||
export interface ISupplierFinder {
|
||||
findSupplierById(
|
||||
companyId: UniqueID,
|
||||
supplierId: UniqueID,
|
||||
unknown?: unknown
|
||||
): Promise<Result<Supplier, Error>>;
|
||||
|
||||
findSupplierByTIN(
|
||||
companyId: UniqueID,
|
||||
tin: TINNumber,
|
||||
unknown?: unknown
|
||||
): Promise<Result<Supplier, Error>>;
|
||||
|
||||
supplierExists(
|
||||
companyId: UniqueID,
|
||||
invoiceId: UniqueID,
|
||||
unknown?: unknown
|
||||
): Promise<Result<boolean, Error>>;
|
||||
|
||||
findSuppliersByCriteria(
|
||||
companyId: UniqueID,
|
||||
criteria: Criteria,
|
||||
unknown?: unknown
|
||||
): Promise<Result<Collection<SupplierSummary>, Error>>;
|
||||
}
|
||||
|
||||
export class SupplierFinder implements ISupplierFinder {
|
||||
constructor(private readonly repository: ISupplierRepository) {}
|
||||
|
||||
async findSupplierById(
|
||||
companyId: UniqueID,
|
||||
supplierId: UniqueID,
|
||||
unknown?: unknown
|
||||
): Promise<Result<Supplier, Error>> {
|
||||
return this.repository.getByIdInCompany(companyId, supplierId, unknown);
|
||||
}
|
||||
|
||||
findSupplierByTIN(
|
||||
companyId: UniqueID,
|
||||
tin: TINNumber,
|
||||
unknown?: unknown
|
||||
): Promise<Result<Supplier, Error>> {
|
||||
return this.repository.getByTINInCompany(companyId, tin, unknown);
|
||||
}
|
||||
|
||||
async supplierExists(
|
||||
companyId: UniqueID,
|
||||
supplierId: UniqueID,
|
||||
unknown?: unknown
|
||||
): Promise<Result<boolean, Error>> {
|
||||
return this.repository.existsByIdInCompany(companyId, supplierId, unknown);
|
||||
}
|
||||
|
||||
async findSuppliersByCriteria(
|
||||
companyId: UniqueID,
|
||||
criteria: Criteria,
|
||||
unknown?: unknown
|
||||
): Promise<Result<Collection<SupplierSummary>, Error>> {
|
||||
return this.repository.findByCriteriaInCompany(companyId, criteria, unknown);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
import type { TINNumber, UniqueID } from "@repo/rdx-ddd";
|
||||
import type { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { ISupplierCreateProps, Supplier } from "../../domain";
|
||||
|
||||
export interface ISupplierServicesContext {
|
||||
transaction: unknown;
|
||||
companyId: UniqueID;
|
||||
}
|
||||
|
||||
export interface ISupplierPublicServices {
|
||||
findSupplierByTIN: (
|
||||
tin: TINNumber,
|
||||
context: ISupplierServicesContext
|
||||
) => Promise<Result<Supplier, Error>>;
|
||||
|
||||
createSupplier: (
|
||||
id: UniqueID,
|
||||
props: ISupplierCreateProps,
|
||||
context: ISupplierServicesContext
|
||||
) => Promise<Result<Supplier, Error>>;
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { Supplier, SupplierPatchProps } from "../../domain";
|
||||
import type { ISupplierRepository } from "../repositories";
|
||||
|
||||
export interface ISupplierUpdater {
|
||||
update(params: {
|
||||
companyId: UniqueID;
|
||||
id: UniqueID;
|
||||
props: SupplierPatchProps;
|
||||
transaction: unknown;
|
||||
}): Promise<Result<Supplier, Error>>;
|
||||
}
|
||||
|
||||
type SupplierUpdaterDeps = {
|
||||
repository: ISupplierRepository;
|
||||
};
|
||||
|
||||
export class SupplierUpdater implements ISupplierUpdater {
|
||||
private readonly repository: ISupplierRepository;
|
||||
|
||||
constructor(deps: SupplierUpdaterDeps) {
|
||||
this.repository = deps.repository;
|
||||
}
|
||||
|
||||
async update(params: {
|
||||
companyId: UniqueID;
|
||||
id: UniqueID;
|
||||
props: SupplierPatchProps;
|
||||
transaction: unknown;
|
||||
}): Promise<Result<Supplier, Error>> {
|
||||
const { companyId, id, props, transaction } = params;
|
||||
|
||||
// Recuperar agregado existente
|
||||
const existingResult = await this.repository.getByIdInCompany(companyId, id, transaction);
|
||||
|
||||
if (existingResult.isFailure) {
|
||||
return Result.fail(existingResult.error);
|
||||
}
|
||||
|
||||
const supplier = existingResult.data;
|
||||
|
||||
// Aplicar cambios en el agregado
|
||||
const updateResult = supplier.update(props);
|
||||
|
||||
if (updateResult.isFailure) {
|
||||
return Result.fail(updateResult.error);
|
||||
}
|
||||
|
||||
// Persistir cambios
|
||||
const saveResult = await this.repository.update(supplier, transaction);
|
||||
|
||||
if (saveResult.isFailure) {
|
||||
return Result.fail(saveResult.error);
|
||||
}
|
||||
|
||||
return Result.ok(supplier);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./supplier-snapshot.interface";
|
||||
export * from "./supplier-snapshot-builder";
|
||||
@ -0,0 +1,55 @@
|
||||
import type { ISnapshotBuilder } from "@erp/core/api";
|
||||
import { maybeToEmptyString } from "@repo/rdx-ddd";
|
||||
|
||||
import type { Supplier } from "../../../domain";
|
||||
|
||||
import type { ISupplierFullSnapshot } from "./supplier-snapshot.interface";
|
||||
|
||||
export interface ISupplierFullSnapshotBuilder
|
||||
extends ISnapshotBuilder<Supplier, ISupplierFullSnapshot> {}
|
||||
|
||||
export class SupplierFullSnapshotBuilder implements ISupplierFullSnapshotBuilder {
|
||||
toOutput(supplier: Supplier): ISupplierFullSnapshot {
|
||||
const address = supplier.address.toPrimitive();
|
||||
|
||||
return {
|
||||
id: supplier.id.toPrimitive(),
|
||||
company_id: supplier.companyId.toPrimitive(),
|
||||
status: supplier.isActive ? "active" : "inactive",
|
||||
reference: maybeToEmptyString(supplier.reference, (value) => value.toPrimitive()),
|
||||
|
||||
is_company: String(supplier.isCompany),
|
||||
name: supplier.name.toPrimitive(),
|
||||
trade_name: maybeToEmptyString(supplier.tradeName, (value) => value.toPrimitive()),
|
||||
tin: maybeToEmptyString(supplier.tin, (value) => value.toPrimitive()),
|
||||
|
||||
street: maybeToEmptyString(address.street, (value) => value.toPrimitive()),
|
||||
street2: maybeToEmptyString(address.street2, (value) => value.toPrimitive()),
|
||||
city: maybeToEmptyString(address.city, (value) => value.toPrimitive()),
|
||||
province: maybeToEmptyString(address.province, (value) => value.toPrimitive()),
|
||||
postal_code: maybeToEmptyString(address.postalCode, (value) => value.toPrimitive()),
|
||||
country: maybeToEmptyString(address.country, (value) => value.toPrimitive()),
|
||||
|
||||
email_primary: maybeToEmptyString(supplier.emailPrimary, (value) => value.toPrimitive()),
|
||||
email_secondary: maybeToEmptyString(supplier.emailSecondary, (value) => value.toPrimitive()),
|
||||
|
||||
phone_primary: maybeToEmptyString(supplier.phonePrimary, (value) => value.toPrimitive()),
|
||||
phone_secondary: maybeToEmptyString(supplier.phoneSecondary, (value) => value.toPrimitive()),
|
||||
|
||||
mobile_primary: maybeToEmptyString(supplier.mobilePrimary, (value) => value.toPrimitive()),
|
||||
mobile_secondary: maybeToEmptyString(supplier.mobileSecondary, (value) =>
|
||||
value.toPrimitive()
|
||||
),
|
||||
|
||||
fax: maybeToEmptyString(supplier.fax, (value) => value.toPrimitive()),
|
||||
website: maybeToEmptyString(supplier.website, (value) => value.toPrimitive()),
|
||||
|
||||
language_code: supplier.languageCode.toPrimitive(),
|
||||
currency_code: supplier.currencyCode.toPrimitive(),
|
||||
|
||||
metadata: {
|
||||
entity: "supplier",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
export interface ISupplierFullSnapshot {
|
||||
id: string;
|
||||
company_id: string;
|
||||
status: string;
|
||||
reference: string;
|
||||
|
||||
is_company: string;
|
||||
name: string;
|
||||
trade_name: string;
|
||||
tin: string;
|
||||
|
||||
street: string;
|
||||
street2: string;
|
||||
city: string;
|
||||
province: string;
|
||||
postal_code: string;
|
||||
country: string;
|
||||
|
||||
email_primary: string;
|
||||
email_secondary: string;
|
||||
|
||||
phone_primary: string;
|
||||
phone_secondary: string;
|
||||
|
||||
mobile_primary: string;
|
||||
mobile_secondary: string;
|
||||
|
||||
fax: string;
|
||||
website: string;
|
||||
|
||||
language_code: string;
|
||||
currency_code: string;
|
||||
|
||||
metadata?: Record<string, string>;
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./domain";
|
||||
export * from "./summary";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user