.
This commit is contained in:
parent
5bc9920538
commit
3a0262cbf7
@ -2,6 +2,9 @@ import { type ModuleParams, type SetupParams, buildTransactionManager } from "@e
|
||||
import type { Sequelize } from "sequelize";
|
||||
|
||||
import {
|
||||
type IPaymentMethodPublicFinder,
|
||||
PaymentMethodPublicFinder,
|
||||
PaymentMethodPublicModelMapper,
|
||||
buildPaymentMethodCreator,
|
||||
buildPaymentMethodDeleter,
|
||||
buildPaymentMethodFinder,
|
||||
@ -11,8 +14,6 @@ import {
|
||||
buildPaymentMethodUpdater,
|
||||
} from "../../../application";
|
||||
import type { IPaymentMethodRepository } from "../../../application/payment-methods/repositories";
|
||||
import type { IPaymentMethodFinder } from "../../../application/payment-methods/services";
|
||||
import { PaymentMethodFinder } from "../../../application/payment-methods/services";
|
||||
import {
|
||||
CreatePaymentMethodUseCase,
|
||||
DeletePaymentMethodByIdUseCase,
|
||||
@ -116,8 +117,13 @@ export const buildPaymentMethodsDependencies = (
|
||||
export const buildPaymentMethodsPublicServices = (
|
||||
_params: SetupParams,
|
||||
deps: PaymentMethodsInternalDeps
|
||||
): { finder: IPaymentMethodFinder } => {
|
||||
): { finder: IPaymentMethodPublicFinder } => {
|
||||
const mapper = new PaymentMethodPublicModelMapper();
|
||||
|
||||
return {
|
||||
finder: new PaymentMethodFinder(deps.repository),
|
||||
finder: new PaymentMethodPublicFinder({
|
||||
repository: deps.repository,
|
||||
mapper,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@ -12,7 +12,7 @@ export abstract class SequelizeDomainMapper<TModel extends Model, TModelAttribut
|
||||
public abstract mapToPersistence(
|
||||
domain: TEntity,
|
||||
params?: MapperParamsType
|
||||
): Result<TModelAttributes, Error> | Promise<Result<TModelAttributes, Error>>;
|
||||
): Result<TModelAttributes, Error>;
|
||||
|
||||
public mapToDomainCollection(
|
||||
raws: (TModel | TModelAttributes)[],
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
export * from "./invoice-payment-method-read.model";
|
||||
export * from "./invoice-tax-regime-full-read.model";
|
||||
@ -0,0 +1,12 @@
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import type { Maybe } from "@repo/rdx-utils";
|
||||
|
||||
/**
|
||||
* Datos del método de pago que completan a la proforma / issued-invoice
|
||||
*/
|
||||
|
||||
export interface InvoicePaymentMethodReadModel {
|
||||
id: UniqueID;
|
||||
name: string;
|
||||
description: Maybe<string>;
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Datos del régimen de pago que completan a la proforma / issued-invoice
|
||||
*/
|
||||
|
||||
export interface InvocieTaxRegimeFullReadModel {
|
||||
code: string;
|
||||
description: string;
|
||||
}
|
||||
@ -1,10 +1,8 @@
|
||||
import { buildCatalogs } from "@erp/core/api";
|
||||
|
||||
import {
|
||||
type IProformaToIssuedInvoiceConverter,
|
||||
ProformaToIssuedInvoiceConverter,
|
||||
} from "../services";
|
||||
|
||||
export function buildProformaToIssuedInvoicePropsConverter(): IProformaToIssuedInvoiceConverter {
|
||||
return new ProformaToIssuedInvoiceConverter(buildCatalogs());
|
||||
}
|
||||
export const buildProformaToIssuedInvoicePropsConverter = (): IProformaToIssuedInvoiceConverter => {
|
||||
return new ProformaToIssuedInvoiceConverter();
|
||||
};
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export * from "./issued-invoice-summary";
|
||||
export * from "./proforma-issue-read.model";
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
import type { Proforma } from "../../../domain";
|
||||
import type { InvoicePaymentMethodReadModel } from "../../common/models";
|
||||
|
||||
/**
|
||||
* Modelo de lectura usado exclusivamente durante la emisión de una proforma.
|
||||
*
|
||||
* Combina la proforma validada con los datos externos que deben materializarse
|
||||
* como snapshot histórico dentro de la factura emitida.
|
||||
*/
|
||||
export interface ProformaIssueReadModel {
|
||||
proforma: Proforma;
|
||||
paymentMethod: InvoicePaymentMethodReadModel;
|
||||
}
|
||||
@ -1,8 +1,6 @@
|
||||
// modules/customer-invoices/src/api/application/issued-invoices/services/proforma-to-issued-invoice-props-converter.ts
|
||||
|
||||
import type { JsonPaymentCatalogProvider } from "@erp/core";
|
||||
import type { ICatalogs } from "@erp/core/api";
|
||||
import { DomainError, UtcDate } from "@repo/rdx-ddd";
|
||||
import { UtcDate } from "@repo/rdx-ddd";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
@ -14,9 +12,10 @@ import {
|
||||
IssuedInvoiceTaxes,
|
||||
type Proforma,
|
||||
} from "../../../domain";
|
||||
import type { ProformaIssueReadModel } from "../models";
|
||||
|
||||
export interface IProformaToIssuedInvoiceConverter {
|
||||
toCreateProps(proforma: Proforma): Result<IIssuedInvoiceCreateProps, Error>;
|
||||
toCreateProps(source: ProformaIssueReadModel): Result<IIssuedInvoiceCreateProps, Error>;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -27,29 +26,25 @@ export interface IProformaToIssuedInvoiceConverter {
|
||||
*/
|
||||
|
||||
export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoiceConverter {
|
||||
private readonly paymentCatalog: JsonPaymentCatalogProvider;
|
||||
public toCreateProps(source: ProformaIssueReadModel): Result<IIssuedInvoiceCreateProps, Error> {
|
||||
const { proforma } = source;
|
||||
|
||||
constructor(catalogs: ICatalogs) {
|
||||
this.paymentCatalog = catalogs.paymentCatalog;
|
||||
}
|
||||
const itemsResult = this.resolveItems(proforma);
|
||||
|
||||
public toCreateProps(proforma: Proforma): Result<IIssuedInvoiceCreateProps, Error> {
|
||||
const itemsOrResult = this.resolveItems(proforma);
|
||||
|
||||
if (itemsOrResult.isFailure) {
|
||||
return Result.fail(itemsOrResult.error);
|
||||
if (itemsResult.isFailure) {
|
||||
return Result.fail(itemsResult.error);
|
||||
}
|
||||
|
||||
const taxesOrResult = this.resolveTaxes(proforma);
|
||||
const taxesResult = this.resolveTaxes(proforma);
|
||||
|
||||
if (taxesOrResult.isFailure) {
|
||||
return Result.fail(taxesOrResult.error);
|
||||
if (taxesResult.isFailure) {
|
||||
return Result.fail(taxesResult.error);
|
||||
}
|
||||
|
||||
const paymentOrResult = this.resolvePayment(proforma);
|
||||
const paymentResult = this.resolvePayment(source);
|
||||
|
||||
if (paymentOrResult.isFailure) {
|
||||
return Result.fail(paymentOrResult.error);
|
||||
if (paymentResult.isFailure) {
|
||||
return Result.fail(paymentResult.error);
|
||||
}
|
||||
|
||||
const proformaTotals = proforma.totals();
|
||||
@ -74,17 +69,17 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic
|
||||
notes: proforma.notes,
|
||||
reference: proforma.reference,
|
||||
|
||||
paymentMethod: paymentOrResult.data,
|
||||
paymentMethod: paymentResult.data,
|
||||
|
||||
customerId: proforma.customerId,
|
||||
recipient: proforma.recipient.getOrUndefined()!,
|
||||
|
||||
items: itemsOrResult.data,
|
||||
items: itemsResult.data,
|
||||
|
||||
taxes: IssuedInvoiceTaxes.create({
|
||||
currencyCode: proforma.currencyCode,
|
||||
languageCode: proforma.languageCode,
|
||||
taxes: taxesOrResult.data,
|
||||
taxes: taxesResult.data,
|
||||
}),
|
||||
|
||||
subtotalAmount: proformaTotals.subtotalAmount,
|
||||
@ -111,13 +106,13 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic
|
||||
const issuedItems: IssuedInvoiceItem[] = [];
|
||||
|
||||
for (const item of proforma.items.getAll()) {
|
||||
const itemOrResult = this.resolveItem(proforma, item);
|
||||
const itemResult = this.resolveItem(proforma, item);
|
||||
|
||||
if (itemOrResult.isFailure) {
|
||||
return Result.fail(itemOrResult.error);
|
||||
if (itemResult.isFailure) {
|
||||
return Result.fail(itemResult.error);
|
||||
}
|
||||
|
||||
issuedItems.push(itemOrResult.data);
|
||||
issuedItems.push(itemResult.data);
|
||||
}
|
||||
|
||||
return Result.ok(issuedItems);
|
||||
@ -173,13 +168,13 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic
|
||||
const issuedTaxes: IssuedInvoiceTax[] = [];
|
||||
|
||||
for (const tax of proforma.taxes().getAll()) {
|
||||
const taxOrResult = this.resolveTax(tax);
|
||||
const taxResult = this.resolveTax(tax);
|
||||
|
||||
if (taxOrResult.isFailure) {
|
||||
return Result.fail(taxOrResult.error);
|
||||
if (taxResult.isFailure) {
|
||||
return Result.fail(taxResult.error);
|
||||
}
|
||||
|
||||
issuedTaxes.push(taxOrResult.data);
|
||||
issuedTaxes.push(taxResult.data);
|
||||
}
|
||||
|
||||
return Result.ok(issuedTaxes);
|
||||
@ -203,34 +198,25 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic
|
||||
recAmount: tax.recAmount,
|
||||
|
||||
retentionCode: tax.retentionCode,
|
||||
retentionAmount: tax.retentionAmount,
|
||||
retentionPercentage: tax.retentionPercentage,
|
||||
retentionAmount: tax.retentionAmount,
|
||||
|
||||
taxesAmount: tax.taxesAmount,
|
||||
});
|
||||
}
|
||||
|
||||
private resolvePayment(proforma: Proforma): Result<InvoicePaymentMethod, Error> {
|
||||
const paymentId = proforma.paymentMethodId.unwrap();
|
||||
|
||||
const existingPaymentResult = this.paymentCatalog.findById(paymentId.toString());
|
||||
if (existingPaymentResult.isNone()) {
|
||||
return Result.fail(
|
||||
new DomainError("Missing payment method [ProformaToIssuedInvoiceConverter]")
|
||||
);
|
||||
}
|
||||
|
||||
const paymentMethodOrError = InvoicePaymentMethod.create(
|
||||
private resolvePayment(source: ProformaIssueReadModel): Result<InvoicePaymentMethod, Error> {
|
||||
const paymentMethodResult = InvoicePaymentMethod.create(
|
||||
{
|
||||
name: existingPaymentResult.unwrap().description,
|
||||
name: source.paymentMethod.name,
|
||||
},
|
||||
paymentId
|
||||
source.paymentMethod.id
|
||||
);
|
||||
|
||||
if (paymentMethodOrError.isFailure) {
|
||||
return Result.fail(paymentMethodOrError.error);
|
||||
if (paymentMethodResult.isFailure) {
|
||||
return Result.fail(paymentMethodResult.error);
|
||||
}
|
||||
|
||||
return Result.ok(paymentMethodOrError.data);
|
||||
return Result.ok(paymentMethodResult.data);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +1,20 @@
|
||||
import type { IPaymentMethodPublicFinder } from "@erp/catalogs/api";
|
||||
|
||||
import type { IProformaToIssuedInvoiceConverter } from "../../issued-invoices";
|
||||
import type { IProformaRepository } from "../repositories";
|
||||
import { type IProformaIssuer, ProformaIssuer } from "../services";
|
||||
import { type IProformaIssuer, ProformaIssueReadModelAssembler, ProformaIssuer } from "../services";
|
||||
|
||||
export const buildProformaIssuer = (params: {
|
||||
proformaConverter: IProformaToIssuedInvoiceConverter;
|
||||
repository: IProformaRepository;
|
||||
}): IProformaIssuer => {
|
||||
const { proformaConverter, repository } = params;
|
||||
|
||||
return new ProformaIssuer({
|
||||
proformaConverter,
|
||||
repository,
|
||||
proformaConverter: params.proformaConverter,
|
||||
});
|
||||
};
|
||||
|
||||
export const buildProformaIssueReadModelAssembler = (params: {
|
||||
paymentMethodFinder: IPaymentMethodPublicFinder;
|
||||
}) => {
|
||||
return new ProformaIssueReadModelAssembler({
|
||||
paymentMethodFinder: params.paymentMethodFinder,
|
||||
});
|
||||
};
|
||||
|
||||
@ -2,10 +2,12 @@ import type { ITransactionManager } from "@erp/core/api";
|
||||
|
||||
import type { IIssuedInvoicePublicServices } from "../../issued-invoices";
|
||||
import type { ICreateProformaInputMapper, IUpdateProformaInputMapper } from "../mappers";
|
||||
import type { IProformaRepository } from "../repositories";
|
||||
import type {
|
||||
IProformaCreator,
|
||||
IProformaFinder,
|
||||
IProformaFullReadModelAssembler,
|
||||
IProformaIssueReadModelAssembler,
|
||||
IProformaIssuer,
|
||||
IProformaStatusChanger,
|
||||
IProformaUpdater,
|
||||
@ -92,18 +94,25 @@ export function buildIssueProformaUseCase(deps: {
|
||||
};
|
||||
finder: IProformaFinder;
|
||||
issuer: IProformaIssuer;
|
||||
issueReadModelAssembler: IProformaIssueReadModelAssembler;
|
||||
repository: IProformaRepository;
|
||||
transactionManager: ITransactionManager;
|
||||
}) {
|
||||
const {
|
||||
finder,
|
||||
issuer,
|
||||
issueReadModelAssembler,
|
||||
repository,
|
||||
transactionManager,
|
||||
publicServices: { issuedInvoiceServices },
|
||||
} = deps;
|
||||
|
||||
return new IssueProformaUseCase({
|
||||
issuedInvoiceServices,
|
||||
finder,
|
||||
issuer,
|
||||
issueReadModelAssembler,
|
||||
repository,
|
||||
transactionManager,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,23 +1,10 @@
|
||||
import type { Maybe } from "@repo/rdx-utils";
|
||||
|
||||
import type { Proforma } from "../../../domain";
|
||||
|
||||
/**
|
||||
* Datos del método de pago que completan a la proforma.
|
||||
*/
|
||||
export interface ProformaPaymentMethodReadModel {
|
||||
id: string;
|
||||
name: string;
|
||||
description: Maybe<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Datos del régimen de pago que completan a la proforma.
|
||||
*/
|
||||
export interface ProformaTaxRegimeFullReadModel {
|
||||
code: string;
|
||||
description: string;
|
||||
}
|
||||
import type {
|
||||
InvocieTaxRegimeFullReadModel,
|
||||
InvoicePaymentMethodReadModel,
|
||||
} from "../../common/models";
|
||||
|
||||
/**
|
||||
* Modelo de una proforma con datos accesorios.
|
||||
@ -27,6 +14,6 @@ export interface ProformaTaxRegimeFullReadModel {
|
||||
*/
|
||||
export interface ProformaFullReadModel {
|
||||
proforma: Proforma;
|
||||
paymentMethod: Maybe<ProformaPaymentMethodReadModel>;
|
||||
taxRegime: Maybe<ProformaTaxRegimeFullReadModel>;
|
||||
paymentMethod: Maybe<InvoicePaymentMethodReadModel>;
|
||||
taxRegime: Maybe<InvocieTaxRegimeFullReadModel>;
|
||||
}
|
||||
|
||||
@ -40,4 +40,10 @@ export interface IProformaRepository {
|
||||
newStatus: InvoiceStatus,
|
||||
transaction: unknown
|
||||
): Promise<Result<boolean, Error>>;
|
||||
|
||||
markAsIssuedByIdInCompany(
|
||||
companyId: UniqueID,
|
||||
id: UniqueID,
|
||||
transaction: unknown
|
||||
): Promise<Result<void, Error>>;
|
||||
}
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export * from "./proforma-full-read-model.assembler";
|
||||
export * from "./proforma-issue-read-model.assembler";
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
import type { IPaymentMethodPublicFinder } from "@erp/catalogs/api";
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { Proforma } from "../../../../domain";
|
||||
import type { ProformaIssueReadModel } from "../../../issued-invoices";
|
||||
|
||||
export interface IProformaIssueReadModelAssembler {
|
||||
assemble(params: {
|
||||
companyId: UniqueID;
|
||||
proforma: Proforma;
|
||||
transaction?: unknown;
|
||||
}): Promise<Result<ProformaIssueReadModel, Error>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepara los datos externos necesarios para emitir una proforma.
|
||||
*
|
||||
* Esta pieza materializa snapshots externos que pasarán a formar parte histórica
|
||||
* de la factura emitida.
|
||||
*/
|
||||
export class ProformaIssueReadModelAssembler implements IProformaIssueReadModelAssembler {
|
||||
public constructor(
|
||||
private readonly deps: {
|
||||
paymentMethodFinder: IPaymentMethodPublicFinder;
|
||||
}
|
||||
) {}
|
||||
|
||||
public async assemble(params: {
|
||||
companyId: UniqueID;
|
||||
proforma: Proforma;
|
||||
transaction?: unknown;
|
||||
}): Promise<Result<ProformaIssueReadModel, Error>> {
|
||||
if (params.proforma.paymentMethodId.isNone()) {
|
||||
return Result.fail(new Error("Payment method is required to issue proforma"));
|
||||
}
|
||||
|
||||
const paymentMethodId = params.proforma.paymentMethodId.unwrap();
|
||||
|
||||
const paymentMethodResult = await this.deps.paymentMethodFinder.getByIdInCompany({
|
||||
companyId: params.companyId,
|
||||
id: paymentMethodId,
|
||||
transaction: params.transaction,
|
||||
});
|
||||
|
||||
if (paymentMethodResult.isFailure) {
|
||||
return Result.fail(paymentMethodResult.error);
|
||||
}
|
||||
|
||||
const paymentMethod = paymentMethodResult.data;
|
||||
|
||||
return Result.ok({
|
||||
proforma: params.proforma,
|
||||
paymentMethod: {
|
||||
id: paymentMethod.id,
|
||||
name: paymentMethod.name,
|
||||
description: paymentMethod.description,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -8,5 +8,5 @@ export * from "./proforma-finder";
|
||||
export * from "./proforma-issuer";
|
||||
export * from "./proforma-number-generator.interface";
|
||||
export * from "./proforma-public-services.interface";
|
||||
export * from "./proforma-status-charger";
|
||||
export * from "./proforma-status-changer";
|
||||
export * from "./proforma-updater";
|
||||
|
||||
@ -1,65 +1,34 @@
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
// modules/customer-invoices/src/api/application/proformas/services/proforma-issuer.ts
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { IIssuedInvoiceCreateProps, Proforma } from "../../../domain";
|
||||
import type { IProformaToIssuedInvoiceConverter } from "../../issued-invoices";
|
||||
import type { IProformaRepository } from "../repositories";
|
||||
import type { IIssuedInvoiceCreateProps } from "../../../domain";
|
||||
import type {
|
||||
IProformaToIssuedInvoiceConverter,
|
||||
ProformaIssueReadModel,
|
||||
} from "../../issued-invoices";
|
||||
|
||||
export interface IProformaIssuerParams {
|
||||
companyId: UniqueID;
|
||||
proforma: Proforma;
|
||||
issuedInvoiceId: UniqueID;
|
||||
transaction: unknown;
|
||||
source: ProformaIssueReadModel;
|
||||
}
|
||||
|
||||
export interface IProformaIssuer {
|
||||
issueProforma(params: IProformaIssuerParams): Promise<Result<IIssuedInvoiceCreateProps, Error>>;
|
||||
issueProforma(params: IProformaIssuerParams): Result<IIssuedInvoiceCreateProps, Error>;
|
||||
}
|
||||
|
||||
type ProformaIssuerDeps = {
|
||||
proformaConverter: IProformaToIssuedInvoiceConverter;
|
||||
repository: IProformaRepository;
|
||||
};
|
||||
|
||||
export class ProformaIssuer implements IProformaIssuer {
|
||||
private readonly proformaConverter: IProformaToIssuedInvoiceConverter;
|
||||
private readonly repository: IProformaRepository;
|
||||
public constructor(
|
||||
private readonly deps: {
|
||||
proformaConverter: IProformaToIssuedInvoiceConverter;
|
||||
}
|
||||
) {}
|
||||
|
||||
constructor(deps: ProformaIssuerDeps) {
|
||||
this.proformaConverter = deps.proformaConverter;
|
||||
this.repository = deps.repository;
|
||||
}
|
||||
public issueProforma(params: IProformaIssuerParams): Result<IIssuedInvoiceCreateProps, Error> {
|
||||
const issueResult = params.source.proforma.markAsIssued();
|
||||
|
||||
public async issueProforma(
|
||||
params: IProformaIssuerParams
|
||||
): Promise<Result<IIssuedInvoiceCreateProps, Error>> {
|
||||
const { proforma, companyId, transaction } = params;
|
||||
|
||||
// Cambiamos el estado de la proforma a 'issued'
|
||||
const issueResult = proforma.issue();
|
||||
if (issueResult.isFailure) {
|
||||
return Result.fail(issueResult.error);
|
||||
}
|
||||
|
||||
// Persistir
|
||||
const updateStatusResult = await this.repository.updateStatusByIdInCompany(
|
||||
companyId,
|
||||
proforma.id,
|
||||
proforma.status,
|
||||
transaction
|
||||
);
|
||||
|
||||
if (updateStatusResult.isFailure) {
|
||||
return Result.fail(updateStatusResult.error);
|
||||
}
|
||||
|
||||
// Generamos las propiedades de la factura a partir de la proforma
|
||||
const propsResult = this.proformaConverter.toCreateProps(proforma);
|
||||
|
||||
if (propsResult.isFailure) {
|
||||
return Result.fail(propsResult.error);
|
||||
}
|
||||
|
||||
return Result.ok(propsResult.data);
|
||||
return this.deps.proformaConverter.toCreateProps(params.source);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { Proforma } from "../../../domain";
|
||||
import { InvoiceStatus } from "../../../domain";
|
||||
import { InvoiceStatus, type Proforma } from "../../../domain";
|
||||
import type { IProformaRepository } from "../repositories";
|
||||
|
||||
export interface IProformaStatusChanger {
|
||||
@ -35,13 +35,11 @@ export class ChangeStatusProformaUseCase {
|
||||
return Result.fail(proformaIdResult.error);
|
||||
}
|
||||
|
||||
const proformaId = proformaIdResult.data;
|
||||
|
||||
return this.deps.transactionManager.complete(async (transaction) => {
|
||||
try {
|
||||
const changeResult = await this.deps.statusChanger.changeStatus({
|
||||
companyId,
|
||||
id: proformaId,
|
||||
id: proformaIdResult.data,
|
||||
newStatus: new_status!,
|
||||
transaction,
|
||||
});
|
||||
|
||||
@ -3,7 +3,12 @@ import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { IIssuedInvoicePublicServices } from "../../issued-invoices";
|
||||
import type { IProformaFinder, IProformaIssuer } from "../services";
|
||||
import type { IProformaRepository } from "../repositories";
|
||||
import type {
|
||||
IProformaFinder,
|
||||
IProformaIssueReadModelAssembler,
|
||||
IProformaIssuer,
|
||||
} from "../services";
|
||||
|
||||
type IssueProformaUseCaseInput = {
|
||||
companyId: UniqueID;
|
||||
@ -11,13 +16,13 @@ type IssueProformaUseCaseInput = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Caso de uso: Conversión de una issuedinvoice a factura definitiva.
|
||||
* Caso de uso: conversión de una proforma en factura definitiva.
|
||||
*
|
||||
* - Recupera la proforma
|
||||
* - Valida su estado ("approved")
|
||||
* - Genera la factura definitiva (nueva entidad)
|
||||
* - Marca la proforma como "issued"
|
||||
* - Persiste ambas dentro de la misma transacción
|
||||
* - Recupera la proforma.
|
||||
* - Valida su emisión mediante dominio.
|
||||
* - Crea la factura definitiva.
|
||||
* - Marca la proforma como emitida.
|
||||
* - Ejecuta todo dentro de una única transacción.
|
||||
*/
|
||||
export class IssueProformaUseCase {
|
||||
public constructor(
|
||||
@ -25,6 +30,8 @@ export class IssueProformaUseCase {
|
||||
issuedInvoiceServices: IIssuedInvoicePublicServices;
|
||||
finder: IProformaFinder;
|
||||
issuer: IProformaIssuer;
|
||||
issueReadModelAssembler: IProformaIssueReadModelAssembler;
|
||||
repository: IProformaRepository;
|
||||
transactionManager: ITransactionManager;
|
||||
}
|
||||
) {}
|
||||
@ -32,10 +39,13 @@ export class IssueProformaUseCase {
|
||||
public execute(params: IssueProformaUseCaseInput) {
|
||||
const { proforma_id, companyId } = params;
|
||||
|
||||
const proformaIdOrError = UniqueID.create(proforma_id);
|
||||
if (proformaIdOrError.isFailure) return Result.fail(proformaIdOrError.error);
|
||||
const proformaIdResult = UniqueID.create(proforma_id);
|
||||
|
||||
const proformaId = proformaIdOrError.data;
|
||||
if (proformaIdResult.isFailure) {
|
||||
return Result.fail(proformaIdResult.error);
|
||||
}
|
||||
|
||||
const proformaId = proformaIdResult.data;
|
||||
|
||||
return this.deps.transactionManager.complete(async (transaction) => {
|
||||
try {
|
||||
@ -46,28 +56,36 @@ export class IssueProformaUseCase {
|
||||
transaction
|
||||
);
|
||||
|
||||
if (proformaResult.isFailure) return Result.fail(proformaResult.error);
|
||||
if (proformaResult.isFailure) {
|
||||
return Result.fail(proformaResult.error);
|
||||
}
|
||||
|
||||
const proforma = proformaResult.data;
|
||||
|
||||
// 2. Generamos la factura definitiva y la guardamos
|
||||
// 2. Generamos la factura definitiva
|
||||
const issuedInvoiceId = UniqueID.generateNewID();
|
||||
const createPropsOrError = await this.deps.issuer.issueProforma({
|
||||
|
||||
const issueReadModelResult = await this.deps.issueReadModelAssembler.assemble({
|
||||
companyId,
|
||||
issuedInvoiceId,
|
||||
proforma,
|
||||
transaction,
|
||||
});
|
||||
|
||||
if (createPropsOrError.isFailure) {
|
||||
return Result.fail(createPropsOrError.error);
|
||||
if (issueReadModelResult.isFailure) {
|
||||
return Result.fail(issueReadModelResult.error);
|
||||
}
|
||||
|
||||
const createProps = createPropsOrError.data;
|
||||
const createPropsResult = this.deps.issuer.issueProforma({
|
||||
source: issueReadModelResult.data,
|
||||
});
|
||||
|
||||
if (createPropsResult.isFailure) {
|
||||
return Result.fail(createPropsResult.error);
|
||||
}
|
||||
|
||||
// Creamos y guardamos en persistencia la factura definitiva
|
||||
const invoiceResult = await this.deps.issuedInvoiceServices.createIssuedInvoice(
|
||||
issuedInvoiceId,
|
||||
createProps,
|
||||
createPropsResult.data,
|
||||
{
|
||||
companyId,
|
||||
transaction,
|
||||
@ -78,12 +96,21 @@ export class IssueProformaUseCase {
|
||||
return Result.fail(invoiceResult.error);
|
||||
}
|
||||
|
||||
const dto = {
|
||||
const markAsIssuedResult = await this.deps.repository.markAsIssuedByIdInCompany(
|
||||
companyId,
|
||||
proformaId,
|
||||
transaction
|
||||
);
|
||||
|
||||
if (markAsIssuedResult.isFailure) {
|
||||
return Result.fail(markAsIssuedResult.error);
|
||||
}
|
||||
|
||||
return Result.ok({
|
||||
issuedinvoice_id: issuedInvoiceId.toString(),
|
||||
proforma_id: proformaId.toString(),
|
||||
customer_id: proforma.customerId.toString(),
|
||||
};
|
||||
return Result.ok(dto);
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(error as Error);
|
||||
}
|
||||
|
||||
@ -25,29 +25,19 @@ const INVOICE_TRANSITIONS: Record<string, string[]> = {
|
||||
};
|
||||
|
||||
export class InvoiceStatus extends ValueObject<IInvoiceStatusProps> {
|
||||
private static readonly ALLOWED_STATUSES = ["draft", "sent", "approved", "rejected", "issued"];
|
||||
private static readonly ALLOWED_STATUSES = Object.values(INVOICE_STATUS);
|
||||
private static readonly FIELD = "invoiceStatus";
|
||||
private static readonly ERROR_CODE = "INVALID_INVOICE_STATUS";
|
||||
|
||||
static create(value: string): Result<InvoiceStatus, Error> {
|
||||
if (!InvoiceStatus.ALLOWED_STATUSES.includes(value)) {
|
||||
if (!InvoiceStatus.ALLOWED_STATUSES.includes(value as INVOICE_STATUS)) {
|
||||
const detail = `Estado de la factura no válido: ${value}`;
|
||||
return Result.fail(
|
||||
new DomainValidationError(InvoiceStatus.ERROR_CODE, InvoiceStatus.FIELD, detail)
|
||||
);
|
||||
}
|
||||
|
||||
return Result.ok(
|
||||
value === "rejected"
|
||||
? InvoiceStatus.rejected()
|
||||
: value === "sent"
|
||||
? InvoiceStatus.sent()
|
||||
: value === "issued"
|
||||
? InvoiceStatus.issued()
|
||||
: value === "approved"
|
||||
? InvoiceStatus.approved()
|
||||
: InvoiceStatus.draft()
|
||||
);
|
||||
return Result.ok(new InvoiceStatus({ value: value as INVOICE_STATUS }));
|
||||
}
|
||||
|
||||
public static draft(): InvoiceStatus {
|
||||
@ -94,10 +84,18 @@ export class InvoiceStatus extends ValueObject<IInvoiceStatusProps> {
|
||||
return INVOICE_TRANSITIONS[this.props.value].includes(nextStatus);
|
||||
}
|
||||
|
||||
public isIssued(): boolean {
|
||||
isIssued(): boolean {
|
||||
return this.props.value === INVOICE_STATUS.ISSUED;
|
||||
}
|
||||
|
||||
isRejected(): boolean {
|
||||
return this.props.value === INVOICE_STATUS.REJECTED;
|
||||
}
|
||||
|
||||
isSent(): boolean {
|
||||
return this.props.value === INVOICE_STATUS.SENT;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return String(this.props.value);
|
||||
}
|
||||
|
||||
@ -29,6 +29,7 @@ import {
|
||||
} from "../entities";
|
||||
import { InvalidProformaTransitionError, ProformaItemMismatch } from "../errors";
|
||||
import type { IProformaTaxTotals, ProformaCalculationContext } from "../services";
|
||||
import { canManuallyTransitionProformaStatus } from "../services";
|
||||
import { ProformaItemTaxes } from "../value-objects";
|
||||
|
||||
export interface IProformaCreateProps {
|
||||
@ -301,6 +302,12 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
|
||||
return this.taxRegimeCode.isSome();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cambia manualmente el estado de la proforma.
|
||||
*
|
||||
* No permite marcar una proforma como `issued`, porque esa transición implica
|
||||
* crear una factura emitida y debe ejecutarse mediante el caso de uso de emisión.
|
||||
*/
|
||||
public changeStatus(nextStatus: InvoiceStatus): Result<boolean, Error> {
|
||||
const currentStatus = this.status;
|
||||
|
||||
@ -308,17 +315,7 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
|
||||
return Result.ok(false);
|
||||
}
|
||||
|
||||
if (nextStatus.toPrimitive() === INVOICE_STATUS.ISSUED) {
|
||||
return Result.fail(
|
||||
new InvalidProformaTransitionError(
|
||||
currentStatus.toPrimitive(),
|
||||
nextStatus.toPrimitive(),
|
||||
this.id.toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (!currentStatus.canTransitionTo(nextStatus)) {
|
||||
if (!canManuallyTransitionProformaStatus({ currentStatus, nextStatus })) {
|
||||
return Result.fail(
|
||||
new InvalidProformaTransitionError(
|
||||
currentStatus.toPrimitive(),
|
||||
@ -333,21 +330,39 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
|
||||
return Result.ok(true);
|
||||
}
|
||||
|
||||
public issue(): Result<void, Error> {
|
||||
public markAsIssued(): Result<void, Error> {
|
||||
const currentStatus = this.status;
|
||||
|
||||
if (currentStatus.isIssued()) {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
// Antes de cambiar el estado de la proforma,
|
||||
// comprobamos que se cumplen las condiciones
|
||||
// necesarias.
|
||||
|
||||
if (!this.props.status.canTransitionTo("issued")) {
|
||||
if (!currentStatus.isApproved()) {
|
||||
return Result.fail(
|
||||
new DomainValidationError(
|
||||
"INVALID_STATE",
|
||||
"status",
|
||||
"Proforma cannot be issued from current state"
|
||||
new InvalidProformaTransitionError(
|
||||
currentStatus.toPrimitive(),
|
||||
INVOICE_STATUS.ISSUED,
|
||||
this.id.toString()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const validationResult = this.validateCanBeIssued();
|
||||
|
||||
if (validationResult.isFailure) {
|
||||
return Result.fail(validationResult.error);
|
||||
}
|
||||
|
||||
this.props.status = InvoiceStatus.issued();
|
||||
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
private validateCanBeIssued(): Result<void, Error> {
|
||||
if (this.series.isNone()) {
|
||||
return Result.fail(
|
||||
new DomainValidationError(
|
||||
@ -444,12 +459,11 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
|
||||
new DomainValidationError(
|
||||
"LINKED_INVOICE_NOT_ALLOWED",
|
||||
"linkedInvoiceId",
|
||||
"Proforma cannot be linked to an invoice"
|
||||
"Proforma is already linked to an invoice"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
this.props.status = InvoiceStatus.issued();
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
export * from "./proforma-compare-tax-totals";
|
||||
export * from "./proforma-compute-tax-groups";
|
||||
export * from "./proforma-items-totals-calculator";
|
||||
export * from "./proforma-manual-status-transitions";
|
||||
export * from "./proforma-taxes-calculator";
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
import { INVOICE_STATUS, type InvoiceStatus } from "../..";
|
||||
|
||||
/**
|
||||
* Transiciones manuales permitidas para una proforma.
|
||||
*
|
||||
* No incluye `approved -> issued` porque emitir una proforma crea una factura emitida
|
||||
* y debe pasar siempre por el caso de uso específico de emisión.
|
||||
*/
|
||||
const PROFORMA_MANUAL_STATUS_TRANSITIONS: Readonly<
|
||||
Record<INVOICE_STATUS, readonly INVOICE_STATUS[]>
|
||||
> = {
|
||||
[INVOICE_STATUS.DRAFT]: [INVOICE_STATUS.SENT],
|
||||
[INVOICE_STATUS.SENT]: [INVOICE_STATUS.APPROVED, INVOICE_STATUS.REJECTED],
|
||||
[INVOICE_STATUS.APPROVED]: [INVOICE_STATUS.REJECTED, INVOICE_STATUS.DRAFT],
|
||||
[INVOICE_STATUS.REJECTED]: [INVOICE_STATUS.DRAFT],
|
||||
[INVOICE_STATUS.ISSUED]: [],
|
||||
};
|
||||
|
||||
export function canManuallyTransitionProformaStatus(params: {
|
||||
currentStatus: InvoiceStatus;
|
||||
nextStatus: InvoiceStatus;
|
||||
}): boolean {
|
||||
const current = params.currentStatus.toPrimitive() as INVOICE_STATUS;
|
||||
const next = params.nextStatus.toPrimitive() as INVOICE_STATUS;
|
||||
|
||||
return PROFORMA_MANUAL_STATUS_TRANSITIONS[current].includes(next);
|
||||
}
|
||||
@ -14,11 +14,11 @@ import {
|
||||
} from "../../../application";
|
||||
import type { Proforma } from "../../../domain";
|
||||
|
||||
import { resolveProformaCatalogsDeps } from "./proforma-catalog-deps.di";
|
||||
import { buildProformaNumberGenerator } from "./proforma-number-generator.di";
|
||||
import { buildProformaPersistenceMappers } from "./proforma-persistence-mappers.di";
|
||||
import { buildProformaRepository } from "./proforma-repositories.di";
|
||||
import type { ProformasInternalDeps } from "./proformas.di";
|
||||
import { resolveProformaCatalogsDeps } from "./proforrma-catalog-deps.di";
|
||||
|
||||
type ProformaServicesContext = {
|
||||
transaction: Transaction;
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
buildProformaCreator,
|
||||
buildProformaFinder,
|
||||
buildProformaInputMappers,
|
||||
buildProformaIssueReadModelAssembler,
|
||||
buildProformaIssuer,
|
||||
buildProformaReadModelAssemblers,
|
||||
buildProformaSnapshotBuilders,
|
||||
@ -104,7 +105,10 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
||||
|
||||
const issuer = buildProformaIssuer({
|
||||
proformaConverter: proformaToIssuedInvoiceConverter,
|
||||
repository,
|
||||
});
|
||||
|
||||
const issueReadModelAssembler = buildProformaIssueReadModelAssembler({
|
||||
paymentMethodFinder: catalogs.paymentMethod.finder,
|
||||
});
|
||||
|
||||
const documentGeneratorPipeline = buildProformaDocumentService(params);
|
||||
@ -159,6 +163,8 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
||||
publicServices,
|
||||
finder,
|
||||
issuer,
|
||||
issueReadModelAssembler,
|
||||
repository,
|
||||
transactionManager,
|
||||
}),
|
||||
|
||||
|
||||
@ -15,24 +15,24 @@ type ProformaCatalogsDeps = {
|
||||
|
||||
export function resolveProformaCatalogsDeps(params: ModuleParams): ProformaCatalogsDeps {
|
||||
const taxDefinition =
|
||||
params.getService<CatalogsPublicServicesType["taxDefinitions"]>("catalogs:taxDefinition");
|
||||
params.getService<CatalogsPublicServicesType["taxDefinitions"]>("catalogs:taxDefinitions");
|
||||
|
||||
if (!taxDefinition?.finder) {
|
||||
throw new Error("Missing public service: catalogs:taxDefinition.finder");
|
||||
throw new Error("Missing public service: catalogs:taxDefinitions.finder");
|
||||
}
|
||||
|
||||
const taxRegime =
|
||||
params.getService<CatalogsPublicServicesType["taxRegimes"]>("catalogs:taxRegime");
|
||||
params.getService<CatalogsPublicServicesType["taxRegimes"]>("catalogs:taxRegimes");
|
||||
|
||||
if (!taxRegime?.finder) {
|
||||
throw new Error("Missing public service: catalogs:taxRegime.finder");
|
||||
throw new Error("Missing public service: catalogs:taxRegimes.finder");
|
||||
}
|
||||
|
||||
const paymentMethod =
|
||||
params.getService<CatalogsPublicServicesType["paymentMethods"]>("catalogs:paymentMethod");
|
||||
params.getService<CatalogsPublicServicesType["paymentMethods"]>("catalogs:paymentMethods");
|
||||
|
||||
if (!paymentMethod?.finder) {
|
||||
throw new Error("Missing public service: catalogs:paymentMethod.finder");
|
||||
throw new Error("Missing public service: catalogs:paymentMethods.finder");
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@ -275,7 +275,7 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
|
||||
}
|
||||
}
|
||||
|
||||
public async mapToPersistence(
|
||||
public mapToPersistence(
|
||||
source: Proforma,
|
||||
params?: MapperParamsType
|
||||
): Result<CustomerInvoiceCreationAttributes, Error> {
|
||||
@ -350,7 +350,10 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
|
||||
notes: maybeToNullable(source.notes, (v) => v.toPrimitive()),
|
||||
|
||||
payment_method_id: maybeToNullable(source.paymentMethodId, (value) => value.toPrimitive()),
|
||||
payment_method_description: null,
|
||||
|
||||
tax_regime_code: maybeToNullable(source.taxRegimeCode, (value) => value),
|
||||
tax_regime_description: null,
|
||||
|
||||
subtotal_amount_value: allAmounts.subtotalAmount.value,
|
||||
subtotal_amount_scale: allAmounts.subtotalAmount.scale,
|
||||
|
||||
@ -1,8 +1,12 @@
|
||||
// modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-item-domain.mapper.ts
|
||||
import {
|
||||
DiscountPercentage,
|
||||
type MapperParamsType,
|
||||
SequelizeDomainMapper,
|
||||
Tax,
|
||||
type TaxCalculationBehavior,
|
||||
type TaxGroup,
|
||||
TaxPercentage,
|
||||
} from "@erp/core/api";
|
||||
import {
|
||||
UniqueID,
|
||||
@ -12,7 +16,7 @@ import {
|
||||
maybeFromNullableResult,
|
||||
maybeToNullable,
|
||||
} from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
|
||||
import {
|
||||
type IProformaCreateProps,
|
||||
@ -54,14 +58,14 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
|
||||
);
|
||||
|
||||
const description = extractOrPushError(
|
||||
maybeFromNullableResult(raw.description, (v) => ItemDescription.create(v)),
|
||||
maybeFromNullableResult(raw.description, (value) => ItemDescription.create(value)),
|
||||
`items[${index}].description`,
|
||||
errors
|
||||
);
|
||||
|
||||
const quantity = extractOrPushError(
|
||||
maybeFromNullableResult(raw.quantity_value, (v) =>
|
||||
ItemQuantity.create({ value: v, scale: raw.quantity_scale })
|
||||
maybeFromNullableResult(raw.quantity_value, (value) =>
|
||||
ItemQuantity.create({ value, scale: raw.quantity_scale })
|
||||
),
|
||||
`items[${index}].quantity_value`,
|
||||
errors
|
||||
@ -80,9 +84,9 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
|
||||
);
|
||||
|
||||
const itemDiscountPercentage = extractOrPushError(
|
||||
maybeFromNullableResult(raw.item_discount_percentage_value, (v) =>
|
||||
maybeFromNullableResult(raw.item_discount_percentage_value, (value) =>
|
||||
DiscountPercentage.create({
|
||||
value: v,
|
||||
value,
|
||||
scale: raw.item_discount_percentage_scale,
|
||||
})
|
||||
),
|
||||
@ -91,22 +95,41 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
|
||||
);
|
||||
|
||||
const iva = extractOrPushError(
|
||||
maybeFromNullableResult(raw.iva_code, (code) => Tax.createFromCode(code, this._taxCatalog)),
|
||||
`items[${index}].iva_code`,
|
||||
this.mapTaxToDomain({
|
||||
code: raw.iva_code,
|
||||
percentageValue: raw.iva_percentage_value,
|
||||
percentageScale: raw.iva_percentage_scale,
|
||||
group: "iva",
|
||||
calculationBehavior: "additive",
|
||||
fieldPath: `items[${index}].iva`,
|
||||
}),
|
||||
`items[${index}].iva`,
|
||||
errors
|
||||
);
|
||||
|
||||
const rec = extractOrPushError(
|
||||
maybeFromNullableResult(raw.rec_code, (code) => Tax.createFromCode(code, this._taxCatalog)),
|
||||
`items[${index}].rec_code`,
|
||||
this.mapTaxToDomain({
|
||||
code: raw.rec_code,
|
||||
percentageValue: raw.rec_percentage_value,
|
||||
percentageScale: raw.rec_percentage_scale,
|
||||
group: "surcharge",
|
||||
calculationBehavior: "additive",
|
||||
fieldPath: `items[${index}].rec`,
|
||||
}),
|
||||
`items[${index}].rec`,
|
||||
errors
|
||||
);
|
||||
|
||||
const retention = extractOrPushError(
|
||||
maybeFromNullableResult(raw.retention_code, (code) =>
|
||||
Tax.createFromCode(code, this._taxCatalog)
|
||||
),
|
||||
`items[${index}].retention_code`,
|
||||
this.mapTaxToDomain({
|
||||
code: raw.retention_code,
|
||||
percentageValue: raw.retention_percentage_value,
|
||||
percentageScale: raw.retention_percentage_scale,
|
||||
group: "retention",
|
||||
calculationBehavior: "subtractive",
|
||||
fieldPath: `items[${index}].retention`,
|
||||
}),
|
||||
`items[${index}].retention`,
|
||||
errors
|
||||
);
|
||||
|
||||
@ -122,55 +145,94 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
|
||||
};
|
||||
}
|
||||
|
||||
private mapTaxToDomain(params: {
|
||||
code: string | null;
|
||||
percentageValue: number | null;
|
||||
percentageScale?: number;
|
||||
group: TaxGroup;
|
||||
calculationBehavior: TaxCalculationBehavior;
|
||||
fieldPath: string;
|
||||
}): Result<Maybe<Tax>, Error> {
|
||||
if (params.code === null || params.code.trim() === "") {
|
||||
return Result.ok(Maybe.none());
|
||||
}
|
||||
|
||||
if (params.percentageValue === null) {
|
||||
return Result.fail(
|
||||
new Error(`${params.fieldPath}.percentage_value is required when tax code is present`)
|
||||
);
|
||||
}
|
||||
|
||||
const percentageResult = TaxPercentage.create({
|
||||
value: params.percentageValue,
|
||||
});
|
||||
|
||||
if (percentageResult.isFailure) {
|
||||
return Result.fail(percentageResult.error);
|
||||
}
|
||||
|
||||
const taxResult = Tax.create({
|
||||
code: params.code,
|
||||
name: params.code,
|
||||
rate: percentageResult.data,
|
||||
group: params.group,
|
||||
calculationBehavior: params.calculationBehavior,
|
||||
});
|
||||
|
||||
if (taxResult.isFailure) {
|
||||
return Result.fail(taxResult.error);
|
||||
}
|
||||
|
||||
return Result.ok(Maybe.some(taxResult.data));
|
||||
}
|
||||
|
||||
public mapToDomain(
|
||||
raw: CustomerInvoiceItemModel,
|
||||
params?: MapperParamsType
|
||||
): Result<ProformaItem, Error> {
|
||||
const { errors, index } = params as {
|
||||
const { errors } = params as {
|
||||
index: number;
|
||||
errors: ValidationErrorDetail[];
|
||||
parent: Partial<IProformaCreateProps>;
|
||||
};
|
||||
|
||||
// 1) Valores escalares (atributos generales)
|
||||
const attributes = this.mapAttributesToDomain(raw, 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 taxes
|
||||
const taxesResult = ProformaItemTaxes.create({
|
||||
iva: attributes.iva!,
|
||||
rec: attributes.rec!,
|
||||
retention: attributes.retention!,
|
||||
});
|
||||
|
||||
// 2) Construcción del elemento de dominio
|
||||
const itemId = attributes.itemId!;
|
||||
const newItem = ProformaItem.rehydrate(
|
||||
if (taxesResult.isFailure) {
|
||||
return Result.fail(taxesResult.error);
|
||||
}
|
||||
|
||||
const item = ProformaItem.rehydrate(
|
||||
{
|
||||
description: attributes.description!,
|
||||
quantity: attributes.quantity!,
|
||||
unitAmount: attributes.unitAmount!,
|
||||
|
||||
itemDiscountPercentage: attributes.itemDiscountPercentage!,
|
||||
taxes: taxesResult.data,
|
||||
},
|
||||
itemId
|
||||
attributes.itemId!
|
||||
);
|
||||
|
||||
return Result.ok(newItem);
|
||||
return Result.ok(item);
|
||||
}
|
||||
|
||||
public mapToPersistence(
|
||||
source: ProformaItem,
|
||||
params?: MapperParamsType
|
||||
): Result<CustomerInvoiceItemCreationAttributes, Error> {
|
||||
const { errors, index, parent } = params as {
|
||||
const { index, parent } = params as {
|
||||
index: number;
|
||||
parent: Proforma;
|
||||
errors: ValidationErrorDetail[];
|
||||
@ -187,34 +249,32 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
|
||||
invoice_id: parent.id.toPrimitive(),
|
||||
position: index,
|
||||
|
||||
description: maybeToNullable(source.description, (v) => v.toPrimitive()),
|
||||
description: maybeToNullable(source.description, (value) => value.toPrimitive()),
|
||||
|
||||
quantity_value: maybeToNullable(source.quantity, (v) => v.toPrimitive().value),
|
||||
quantity_value: maybeToNullable(source.quantity, (value) => value.toPrimitive().value),
|
||||
quantity_scale:
|
||||
maybeToNullable(source.quantity, (v) => v.toPrimitive().scale) ??
|
||||
maybeToNullable(source.quantity, (value) => value.toPrimitive().scale) ??
|
||||
ItemQuantity.DEFAULT_SCALE,
|
||||
|
||||
unit_amount_value: maybeToNullable(source.unitAmount, (v) => v.toPrimitive().value),
|
||||
unit_amount_value: maybeToNullable(source.unitAmount, (value) => value.toPrimitive().value),
|
||||
unit_amount_scale:
|
||||
maybeToNullable(source.unitAmount, (v) => v.toPrimitive().scale) ??
|
||||
maybeToNullable(source.unitAmount, (value) => value.toPrimitive().scale) ??
|
||||
ItemAmount.DEFAULT_SCALE,
|
||||
|
||||
subtotal_amount_value: allAmounts.subtotalAmount.value,
|
||||
subtotal_amount_scale: allAmounts.subtotalAmount.scale,
|
||||
|
||||
//
|
||||
item_discount_percentage_value: maybeToNullable(
|
||||
source.itemDiscountPercentage,
|
||||
(v) => v.toPrimitive().value
|
||||
(value) => value.toPrimitive().value
|
||||
),
|
||||
item_discount_percentage_scale:
|
||||
maybeToNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ??
|
||||
maybeToNullable(source.itemDiscountPercentage, (value) => value.toPrimitive().scale) ??
|
||||
DiscountPercentage.DEFAULT_SCALE,
|
||||
|
||||
item_discount_amount_value: allAmounts.itemDiscountAmount.value,
|
||||
item_discount_amount_scale: allAmounts.itemDiscountAmount.scale,
|
||||
|
||||
//
|
||||
global_discount_percentage_value: parent.globalDiscountPercentage.toPrimitive().value,
|
||||
global_discount_percentage_scale:
|
||||
parent.globalDiscountPercentage.toPrimitive().scale ?? DiscountPercentage.DEFAULT_SCALE,
|
||||
@ -222,52 +282,40 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
|
||||
global_discount_amount_value: allAmounts.globalDiscountAmount.value,
|
||||
global_discount_amount_scale: allAmounts.globalDiscountAmount.scale,
|
||||
|
||||
//
|
||||
total_discount_amount_value: allAmounts.totalDiscountAmount.value,
|
||||
total_discount_amount_scale: allAmounts.totalDiscountAmount.scale,
|
||||
|
||||
//
|
||||
taxable_amount_value: allAmounts.taxableAmount.value,
|
||||
taxable_amount_scale: allAmounts.taxableAmount.scale,
|
||||
|
||||
// IVA
|
||||
iva_code: maybeToNullable(source.taxes.iva, (v) => v.code),
|
||||
|
||||
iva_percentage_value: maybeToNullable(source.taxes.iva, (v) => v.percentage.value),
|
||||
iva_code: maybeToNullable(source.taxes.iva, (value) => value.code),
|
||||
iva_percentage_value: maybeToNullable(source.taxes.iva, (value) => value.percentage.value),
|
||||
iva_percentage_scale:
|
||||
maybeToNullable(source.taxes.iva, (v) => v.percentage.scale) ?? Tax.DEFAULT_SCALE,
|
||||
|
||||
maybeToNullable(source.taxes.iva, (value) => value.percentage.scale) ?? Tax.DEFAULT_SCALE,
|
||||
iva_amount_value: allAmounts.ivaAmount.value,
|
||||
iva_amount_scale: allAmounts.ivaAmount.scale,
|
||||
|
||||
// REC
|
||||
rec_code: maybeToNullable(source.taxes.rec, (v) => v.code),
|
||||
|
||||
rec_percentage_value: maybeToNullable(source.taxes.rec, (v) => v.percentage.value),
|
||||
rec_code: maybeToNullable(source.taxes.rec, (value) => value.code),
|
||||
rec_percentage_value: maybeToNullable(source.taxes.rec, (value) => value.percentage.value),
|
||||
rec_percentage_scale:
|
||||
maybeToNullable(source.taxes.rec, (v) => v.percentage.scale) ?? Tax.DEFAULT_SCALE,
|
||||
|
||||
maybeToNullable(source.taxes.rec, (value) => value.percentage.scale) ?? Tax.DEFAULT_SCALE,
|
||||
rec_amount_value: allAmounts.recAmount.value,
|
||||
rec_amount_scale: allAmounts.recAmount.scale,
|
||||
|
||||
// RET
|
||||
retention_code: maybeToNullable(source.taxes.retention, (v) => v.code),
|
||||
|
||||
retention_code: maybeToNullable(source.taxes.retention, (value) => value.code),
|
||||
retention_percentage_value: maybeToNullable(
|
||||
source.taxes.retention,
|
||||
(v) => v.percentage.value
|
||||
(value) => value.percentage.value
|
||||
),
|
||||
retention_percentage_scale:
|
||||
maybeToNullable(source.taxes.retention, (v) => v.percentage.scale) ?? Tax.DEFAULT_SCALE,
|
||||
|
||||
maybeToNullable(source.taxes.retention, (value) => value.percentage.scale) ??
|
||||
Tax.DEFAULT_SCALE,
|
||||
retention_amount_value: allAmounts.retentionAmount.value,
|
||||
retention_amount_scale: allAmounts.retentionAmount.scale,
|
||||
|
||||
//
|
||||
taxes_amount_value: allAmounts.taxesAmount.value,
|
||||
taxes_amount_scale: allAmounts.taxesAmount.scale,
|
||||
|
||||
//
|
||||
total_amount_value: allAmounts.totalAmount.value,
|
||||
total_amount_scale: allAmounts.totalAmount.scale,
|
||||
});
|
||||
|
||||
@ -10,7 +10,7 @@ import { type Collection, Result } from "@repo/rdx-utils";
|
||||
import type { FindOptions, InferAttributes, OrderItem, Sequelize, Transaction } from "sequelize";
|
||||
|
||||
import type { IProformaRepository, ProformaSummary } from "../../../../../application";
|
||||
import type { InvoiceStatus, Proforma } from "../../../../../domain";
|
||||
import { INVOICE_STATUS, type InvoiceStatus, type Proforma } from "../../../../../domain";
|
||||
import {
|
||||
CustomerInvoiceItemModel,
|
||||
CustomerInvoiceModel,
|
||||
@ -241,6 +241,41 @@ export class ProformaRepository
|
||||
}
|
||||
}
|
||||
|
||||
public async markAsIssuedByIdInCompany(
|
||||
companyId: UniqueID,
|
||||
id: UniqueID,
|
||||
transaction?: unknown
|
||||
): Promise<Result<void, Error>> {
|
||||
try {
|
||||
const [affectedRows] = await CustomerInvoiceModel.update(
|
||||
{
|
||||
status: INVOICE_STATUS.ISSUED,
|
||||
},
|
||||
{
|
||||
where: {
|
||||
id: id.toString(),
|
||||
company_id: companyId.toString(),
|
||||
is_proforma: true,
|
||||
status: INVOICE_STATUS.APPROVED,
|
||||
},
|
||||
transaction: transaction as Transaction,
|
||||
}
|
||||
);
|
||||
|
||||
if (affectedRows !== 1) {
|
||||
return Result.fail(
|
||||
new Error(
|
||||
`Proforma ${id.toString()} could not be marked as issued because it is not approved or does not exist`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return Result.ok();
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Busca una factura por su identificador único.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user