This commit is contained in:
David Arranz 2026-06-14 14:11:17 +02:00
parent 5bc9920538
commit 3a0262cbf7
30 changed files with 477 additions and 257 deletions

View File

@ -2,6 +2,9 @@ import { type ModuleParams, type SetupParams, buildTransactionManager } from "@e
import type { Sequelize } from "sequelize"; import type { Sequelize } from "sequelize";
import { import {
type IPaymentMethodPublicFinder,
PaymentMethodPublicFinder,
PaymentMethodPublicModelMapper,
buildPaymentMethodCreator, buildPaymentMethodCreator,
buildPaymentMethodDeleter, buildPaymentMethodDeleter,
buildPaymentMethodFinder, buildPaymentMethodFinder,
@ -11,8 +14,6 @@ import {
buildPaymentMethodUpdater, buildPaymentMethodUpdater,
} from "../../../application"; } from "../../../application";
import type { IPaymentMethodRepository } from "../../../application/payment-methods/repositories"; import type { IPaymentMethodRepository } from "../../../application/payment-methods/repositories";
import type { IPaymentMethodFinder } from "../../../application/payment-methods/services";
import { PaymentMethodFinder } from "../../../application/payment-methods/services";
import { import {
CreatePaymentMethodUseCase, CreatePaymentMethodUseCase,
DeletePaymentMethodByIdUseCase, DeletePaymentMethodByIdUseCase,
@ -116,8 +117,13 @@ export const buildPaymentMethodsDependencies = (
export const buildPaymentMethodsPublicServices = ( export const buildPaymentMethodsPublicServices = (
_params: SetupParams, _params: SetupParams,
deps: PaymentMethodsInternalDeps deps: PaymentMethodsInternalDeps
): { finder: IPaymentMethodFinder } => { ): { finder: IPaymentMethodPublicFinder } => {
const mapper = new PaymentMethodPublicModelMapper();
return { return {
finder: new PaymentMethodFinder(deps.repository), finder: new PaymentMethodPublicFinder({
repository: deps.repository,
mapper,
}),
}; };
}; };

View File

@ -12,7 +12,7 @@ export abstract class SequelizeDomainMapper<TModel extends Model, TModelAttribut
public abstract mapToPersistence( public abstract mapToPersistence(
domain: TEntity, domain: TEntity,
params?: MapperParamsType params?: MapperParamsType
): Result<TModelAttributes, Error> | Promise<Result<TModelAttributes, Error>>; ): Result<TModelAttributes, Error>;
public mapToDomainCollection( public mapToDomainCollection(
raws: (TModel | TModelAttributes)[], raws: (TModel | TModelAttributes)[],

View File

@ -0,0 +1,2 @@
export * from "./invoice-payment-method-read.model";
export * from "./invoice-tax-regime-full-read.model";

View File

@ -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>;
}

View File

@ -0,0 +1,8 @@
/**
* Datos del régimen de pago que completan a la proforma / issued-invoice
*/
export interface InvocieTaxRegimeFullReadModel {
code: string;
description: string;
}

View File

@ -1,10 +1,8 @@
import { buildCatalogs } from "@erp/core/api";
import { import {
type IProformaToIssuedInvoiceConverter, type IProformaToIssuedInvoiceConverter,
ProformaToIssuedInvoiceConverter, ProformaToIssuedInvoiceConverter,
} from "../services"; } from "../services";
export function buildProformaToIssuedInvoicePropsConverter(): IProformaToIssuedInvoiceConverter { export const buildProformaToIssuedInvoicePropsConverter = (): IProformaToIssuedInvoiceConverter => {
return new ProformaToIssuedInvoiceConverter(buildCatalogs()); return new ProformaToIssuedInvoiceConverter();
} };

View File

@ -1 +1,2 @@
export * from "./issued-invoice-summary"; export * from "./issued-invoice-summary";
export * from "./proforma-issue-read.model";

View File

@ -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;
}

View File

@ -1,8 +1,6 @@
// modules/customer-invoices/src/api/application/issued-invoices/services/proforma-to-issued-invoice-props-converter.ts // modules/customer-invoices/src/api/application/issued-invoices/services/proforma-to-issued-invoice-props-converter.ts
import type { JsonPaymentCatalogProvider } from "@erp/core"; import { UtcDate } from "@repo/rdx-ddd";
import type { ICatalogs } from "@erp/core/api";
import { DomainError, UtcDate } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import { import {
@ -14,9 +12,10 @@ import {
IssuedInvoiceTaxes, IssuedInvoiceTaxes,
type Proforma, type Proforma,
} from "../../../domain"; } from "../../../domain";
import type { ProformaIssueReadModel } from "../models";
export interface IProformaToIssuedInvoiceConverter { 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 { export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoiceConverter {
private readonly paymentCatalog: JsonPaymentCatalogProvider; public toCreateProps(source: ProformaIssueReadModel): Result<IIssuedInvoiceCreateProps, Error> {
const { proforma } = source;
constructor(catalogs: ICatalogs) { const itemsResult = this.resolveItems(proforma);
this.paymentCatalog = catalogs.paymentCatalog;
}
public toCreateProps(proforma: Proforma): Result<IIssuedInvoiceCreateProps, Error> { if (itemsResult.isFailure) {
const itemsOrResult = this.resolveItems(proforma); return Result.fail(itemsResult.error);
if (itemsOrResult.isFailure) {
return Result.fail(itemsOrResult.error);
} }
const taxesOrResult = this.resolveTaxes(proforma); const taxesResult = this.resolveTaxes(proforma);
if (taxesOrResult.isFailure) { if (taxesResult.isFailure) {
return Result.fail(taxesOrResult.error); return Result.fail(taxesResult.error);
} }
const paymentOrResult = this.resolvePayment(proforma); const paymentResult = this.resolvePayment(source);
if (paymentOrResult.isFailure) { if (paymentResult.isFailure) {
return Result.fail(paymentOrResult.error); return Result.fail(paymentResult.error);
} }
const proformaTotals = proforma.totals(); const proformaTotals = proforma.totals();
@ -74,17 +69,17 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic
notes: proforma.notes, notes: proforma.notes,
reference: proforma.reference, reference: proforma.reference,
paymentMethod: paymentOrResult.data, paymentMethod: paymentResult.data,
customerId: proforma.customerId, customerId: proforma.customerId,
recipient: proforma.recipient.getOrUndefined()!, recipient: proforma.recipient.getOrUndefined()!,
items: itemsOrResult.data, items: itemsResult.data,
taxes: IssuedInvoiceTaxes.create({ taxes: IssuedInvoiceTaxes.create({
currencyCode: proforma.currencyCode, currencyCode: proforma.currencyCode,
languageCode: proforma.languageCode, languageCode: proforma.languageCode,
taxes: taxesOrResult.data, taxes: taxesResult.data,
}), }),
subtotalAmount: proformaTotals.subtotalAmount, subtotalAmount: proformaTotals.subtotalAmount,
@ -111,13 +106,13 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic
const issuedItems: IssuedInvoiceItem[] = []; const issuedItems: IssuedInvoiceItem[] = [];
for (const item of proforma.items.getAll()) { for (const item of proforma.items.getAll()) {
const itemOrResult = this.resolveItem(proforma, item); const itemResult = this.resolveItem(proforma, item);
if (itemOrResult.isFailure) { if (itemResult.isFailure) {
return Result.fail(itemOrResult.error); return Result.fail(itemResult.error);
} }
issuedItems.push(itemOrResult.data); issuedItems.push(itemResult.data);
} }
return Result.ok(issuedItems); return Result.ok(issuedItems);
@ -173,13 +168,13 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic
const issuedTaxes: IssuedInvoiceTax[] = []; const issuedTaxes: IssuedInvoiceTax[] = [];
for (const tax of proforma.taxes().getAll()) { for (const tax of proforma.taxes().getAll()) {
const taxOrResult = this.resolveTax(tax); const taxResult = this.resolveTax(tax);
if (taxOrResult.isFailure) { if (taxResult.isFailure) {
return Result.fail(taxOrResult.error); return Result.fail(taxResult.error);
} }
issuedTaxes.push(taxOrResult.data); issuedTaxes.push(taxResult.data);
} }
return Result.ok(issuedTaxes); return Result.ok(issuedTaxes);
@ -203,34 +198,25 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic
recAmount: tax.recAmount, recAmount: tax.recAmount,
retentionCode: tax.retentionCode, retentionCode: tax.retentionCode,
retentionAmount: tax.retentionAmount,
retentionPercentage: tax.retentionPercentage, retentionPercentage: tax.retentionPercentage,
retentionAmount: tax.retentionAmount,
taxesAmount: tax.taxesAmount, taxesAmount: tax.taxesAmount,
}); });
} }
private resolvePayment(proforma: Proforma): Result<InvoicePaymentMethod, Error> { private resolvePayment(source: ProformaIssueReadModel): Result<InvoicePaymentMethod, Error> {
const paymentId = proforma.paymentMethodId.unwrap(); const paymentMethodResult = InvoicePaymentMethod.create(
const existingPaymentResult = this.paymentCatalog.findById(paymentId.toString());
if (existingPaymentResult.isNone()) {
return Result.fail(
new DomainError("Missing payment method [ProformaToIssuedInvoiceConverter]")
);
}
const paymentMethodOrError = InvoicePaymentMethod.create(
{ {
name: existingPaymentResult.unwrap().description, name: source.paymentMethod.name,
}, },
paymentId source.paymentMethod.id
); );
if (paymentMethodOrError.isFailure) { if (paymentMethodResult.isFailure) {
return Result.fail(paymentMethodOrError.error); return Result.fail(paymentMethodResult.error);
} }
return Result.ok(paymentMethodOrError.data); return Result.ok(paymentMethodResult.data);
} }
} }

View File

@ -1,15 +1,20 @@
import type { IPaymentMethodPublicFinder } from "@erp/catalogs/api";
import type { IProformaToIssuedInvoiceConverter } from "../../issued-invoices"; import type { IProformaToIssuedInvoiceConverter } from "../../issued-invoices";
import type { IProformaRepository } from "../repositories"; import { type IProformaIssuer, ProformaIssueReadModelAssembler, ProformaIssuer } from "../services";
import { type IProformaIssuer, ProformaIssuer } from "../services";
export const buildProformaIssuer = (params: { export const buildProformaIssuer = (params: {
proformaConverter: IProformaToIssuedInvoiceConverter; proformaConverter: IProformaToIssuedInvoiceConverter;
repository: IProformaRepository;
}): IProformaIssuer => { }): IProformaIssuer => {
const { proformaConverter, repository } = params;
return new ProformaIssuer({ return new ProformaIssuer({
proformaConverter, proformaConverter: params.proformaConverter,
repository, });
};
export const buildProformaIssueReadModelAssembler = (params: {
paymentMethodFinder: IPaymentMethodPublicFinder;
}) => {
return new ProformaIssueReadModelAssembler({
paymentMethodFinder: params.paymentMethodFinder,
}); });
}; };

View File

@ -2,10 +2,12 @@ import type { ITransactionManager } from "@erp/core/api";
import type { IIssuedInvoicePublicServices } from "../../issued-invoices"; import type { IIssuedInvoicePublicServices } from "../../issued-invoices";
import type { ICreateProformaInputMapper, IUpdateProformaInputMapper } from "../mappers"; import type { ICreateProformaInputMapper, IUpdateProformaInputMapper } from "../mappers";
import type { IProformaRepository } from "../repositories";
import type { import type {
IProformaCreator, IProformaCreator,
IProformaFinder, IProformaFinder,
IProformaFullReadModelAssembler, IProformaFullReadModelAssembler,
IProformaIssueReadModelAssembler,
IProformaIssuer, IProformaIssuer,
IProformaStatusChanger, IProformaStatusChanger,
IProformaUpdater, IProformaUpdater,
@ -92,18 +94,25 @@ export function buildIssueProformaUseCase(deps: {
}; };
finder: IProformaFinder; finder: IProformaFinder;
issuer: IProformaIssuer; issuer: IProformaIssuer;
issueReadModelAssembler: IProformaIssueReadModelAssembler;
repository: IProformaRepository;
transactionManager: ITransactionManager; transactionManager: ITransactionManager;
}) { }) {
const { const {
finder, finder,
issuer, issuer,
issueReadModelAssembler,
repository,
transactionManager, transactionManager,
publicServices: { issuedInvoiceServices }, publicServices: { issuedInvoiceServices },
} = deps; } = deps;
return new IssueProformaUseCase({ return new IssueProformaUseCase({
issuedInvoiceServices, issuedInvoiceServices,
finder, finder,
issuer, issuer,
issueReadModelAssembler,
repository,
transactionManager, transactionManager,
}); });
} }

View File

@ -1,23 +1,10 @@
import type { Maybe } from "@repo/rdx-utils"; import type { Maybe } from "@repo/rdx-utils";
import type { Proforma } from "../../../domain"; import type { Proforma } from "../../../domain";
import type {
/** InvocieTaxRegimeFullReadModel,
* Datos del método de pago que completan a la proforma. InvoicePaymentMethodReadModel,
*/ } from "../../common/models";
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;
}
/** /**
* Modelo de una proforma con datos accesorios. * Modelo de una proforma con datos accesorios.
@ -27,6 +14,6 @@ export interface ProformaTaxRegimeFullReadModel {
*/ */
export interface ProformaFullReadModel { export interface ProformaFullReadModel {
proforma: Proforma; proforma: Proforma;
paymentMethod: Maybe<ProformaPaymentMethodReadModel>; paymentMethod: Maybe<InvoicePaymentMethodReadModel>;
taxRegime: Maybe<ProformaTaxRegimeFullReadModel>; taxRegime: Maybe<InvocieTaxRegimeFullReadModel>;
} }

View File

@ -40,4 +40,10 @@ export interface IProformaRepository {
newStatus: InvoiceStatus, newStatus: InvoiceStatus,
transaction: unknown transaction: unknown
): Promise<Result<boolean, Error>>; ): Promise<Result<boolean, Error>>;
markAsIssuedByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction: unknown
): Promise<Result<void, Error>>;
} }

View File

@ -1 +1,2 @@
export * from "./proforma-full-read-model.assembler"; export * from "./proforma-full-read-model.assembler";
export * from "./proforma-issue-read-model.assembler";

View File

@ -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,
},
});
}
}

View File

@ -8,5 +8,5 @@ export * from "./proforma-finder";
export * from "./proforma-issuer"; export * from "./proforma-issuer";
export * from "./proforma-number-generator.interface"; export * from "./proforma-number-generator.interface";
export * from "./proforma-public-services.interface"; export * from "./proforma-public-services.interface";
export * from "./proforma-status-charger"; export * from "./proforma-status-changer";
export * from "./proforma-updater"; export * from "./proforma-updater";

View File

@ -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 { Result } from "@repo/rdx-utils";
import type { IIssuedInvoiceCreateProps, Proforma } from "../../../domain"; import type { IIssuedInvoiceCreateProps } from "../../../domain";
import type { IProformaToIssuedInvoiceConverter } from "../../issued-invoices"; import type {
import type { IProformaRepository } from "../repositories"; IProformaToIssuedInvoiceConverter,
ProformaIssueReadModel,
} from "../../issued-invoices";
export interface IProformaIssuerParams { export interface IProformaIssuerParams {
companyId: UniqueID; source: ProformaIssueReadModel;
proforma: Proforma;
issuedInvoiceId: UniqueID;
transaction: unknown;
} }
export interface IProformaIssuer { 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 { export class ProformaIssuer implements IProformaIssuer {
private readonly proformaConverter: IProformaToIssuedInvoiceConverter; public constructor(
private readonly repository: IProformaRepository; private readonly deps: {
proformaConverter: IProformaToIssuedInvoiceConverter;
}
) {}
constructor(deps: ProformaIssuerDeps) { public issueProforma(params: IProformaIssuerParams): Result<IIssuedInvoiceCreateProps, Error> {
this.proformaConverter = deps.proformaConverter; const issueResult = params.source.proforma.markAsIssued();
this.repository = deps.repository;
}
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) { if (issueResult.isFailure) {
return Result.fail(issueResult.error); return Result.fail(issueResult.error);
} }
// Persistir return this.deps.proformaConverter.toCreateProps(params.source);
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);
} }
} }

View File

@ -1,8 +1,7 @@
import type { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { Proforma } from "../../../domain"; import { InvoiceStatus, type Proforma } from "../../../domain";
import { InvoiceStatus } from "../../../domain";
import type { IProformaRepository } from "../repositories"; import type { IProformaRepository } from "../repositories";
export interface IProformaStatusChanger { export interface IProformaStatusChanger {

View File

@ -35,13 +35,11 @@ export class ChangeStatusProformaUseCase {
return Result.fail(proformaIdResult.error); return Result.fail(proformaIdResult.error);
} }
const proformaId = proformaIdResult.data;
return this.deps.transactionManager.complete(async (transaction) => { return this.deps.transactionManager.complete(async (transaction) => {
try { try {
const changeResult = await this.deps.statusChanger.changeStatus({ const changeResult = await this.deps.statusChanger.changeStatus({
companyId, companyId,
id: proformaId, id: proformaIdResult.data,
newStatus: new_status!, newStatus: new_status!,
transaction, transaction,
}); });

View File

@ -3,7 +3,12 @@ import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { IIssuedInvoicePublicServices } from "../../issued-invoices"; 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 = { type IssueProformaUseCaseInput = {
companyId: UniqueID; 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 * - Recupera la proforma.
* - Valida su estado ("approved") * - Valida su emisión mediante dominio.
* - Genera la factura definitiva (nueva entidad) * - Crea la factura definitiva.
* - Marca la proforma como "issued" * - Marca la proforma como emitida.
* - Persiste ambas dentro de la misma transacción * - Ejecuta todo dentro de una única transacción.
*/ */
export class IssueProformaUseCase { export class IssueProformaUseCase {
public constructor( public constructor(
@ -25,6 +30,8 @@ export class IssueProformaUseCase {
issuedInvoiceServices: IIssuedInvoicePublicServices; issuedInvoiceServices: IIssuedInvoicePublicServices;
finder: IProformaFinder; finder: IProformaFinder;
issuer: IProformaIssuer; issuer: IProformaIssuer;
issueReadModelAssembler: IProformaIssueReadModelAssembler;
repository: IProformaRepository;
transactionManager: ITransactionManager; transactionManager: ITransactionManager;
} }
) {} ) {}
@ -32,10 +39,13 @@ export class IssueProformaUseCase {
public execute(params: IssueProformaUseCaseInput) { public execute(params: IssueProformaUseCaseInput) {
const { proforma_id, companyId } = params; const { proforma_id, companyId } = params;
const proformaIdOrError = UniqueID.create(proforma_id); const proformaIdResult = UniqueID.create(proforma_id);
if (proformaIdOrError.isFailure) return Result.fail(proformaIdOrError.error);
const proformaId = proformaIdOrError.data; if (proformaIdResult.isFailure) {
return Result.fail(proformaIdResult.error);
}
const proformaId = proformaIdResult.data;
return this.deps.transactionManager.complete(async (transaction) => { return this.deps.transactionManager.complete(async (transaction) => {
try { try {
@ -46,28 +56,36 @@ export class IssueProformaUseCase {
transaction transaction
); );
if (proformaResult.isFailure) return Result.fail(proformaResult.error); if (proformaResult.isFailure) {
return Result.fail(proformaResult.error);
}
const proforma = proformaResult.data; const proforma = proformaResult.data;
// 2. Generamos la factura definitiva y la guardamos // 2. Generamos la factura definitiva
const issuedInvoiceId = UniqueID.generateNewID(); const issuedInvoiceId = UniqueID.generateNewID();
const createPropsOrError = await this.deps.issuer.issueProforma({
const issueReadModelResult = await this.deps.issueReadModelAssembler.assemble({
companyId, companyId,
issuedInvoiceId,
proforma, proforma,
transaction, transaction,
}); });
if (createPropsOrError.isFailure) { if (issueReadModelResult.isFailure) {
return Result.fail(createPropsOrError.error); 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( const invoiceResult = await this.deps.issuedInvoiceServices.createIssuedInvoice(
issuedInvoiceId, issuedInvoiceId,
createProps, createPropsResult.data,
{ {
companyId, companyId,
transaction, transaction,
@ -78,12 +96,21 @@ export class IssueProformaUseCase {
return Result.fail(invoiceResult.error); 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(), issuedinvoice_id: issuedInvoiceId.toString(),
proforma_id: proformaId.toString(), proforma_id: proformaId.toString(),
customer_id: proforma.customerId.toString(), customer_id: proforma.customerId.toString(),
}; });
return Result.ok(dto);
} catch (error: unknown) { } catch (error: unknown) {
return Result.fail(error as Error); return Result.fail(error as Error);
} }

View File

@ -25,29 +25,19 @@ const INVOICE_TRANSITIONS: Record<string, string[]> = {
}; };
export class InvoiceStatus extends ValueObject<IInvoiceStatusProps> { 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 FIELD = "invoiceStatus";
private static readonly ERROR_CODE = "INVALID_INVOICE_STATUS"; private static readonly ERROR_CODE = "INVALID_INVOICE_STATUS";
static create(value: string): Result<InvoiceStatus, Error> { 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}`; const detail = `Estado de la factura no válido: ${value}`;
return Result.fail( return Result.fail(
new DomainValidationError(InvoiceStatus.ERROR_CODE, InvoiceStatus.FIELD, detail) new DomainValidationError(InvoiceStatus.ERROR_CODE, InvoiceStatus.FIELD, detail)
); );
} }
return Result.ok( return Result.ok(new InvoiceStatus({ value: value as INVOICE_STATUS }));
value === "rejected"
? InvoiceStatus.rejected()
: value === "sent"
? InvoiceStatus.sent()
: value === "issued"
? InvoiceStatus.issued()
: value === "approved"
? InvoiceStatus.approved()
: InvoiceStatus.draft()
);
} }
public static draft(): InvoiceStatus { public static draft(): InvoiceStatus {
@ -94,10 +84,18 @@ export class InvoiceStatus extends ValueObject<IInvoiceStatusProps> {
return INVOICE_TRANSITIONS[this.props.value].includes(nextStatus); return INVOICE_TRANSITIONS[this.props.value].includes(nextStatus);
} }
public isIssued(): boolean { isIssued(): boolean {
return this.props.value === INVOICE_STATUS.ISSUED; 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() { toString() {
return String(this.props.value); return String(this.props.value);
} }

View File

@ -29,6 +29,7 @@ import {
} from "../entities"; } from "../entities";
import { InvalidProformaTransitionError, ProformaItemMismatch } from "../errors"; import { InvalidProformaTransitionError, ProformaItemMismatch } from "../errors";
import type { IProformaTaxTotals, ProformaCalculationContext } from "../services"; import type { IProformaTaxTotals, ProformaCalculationContext } from "../services";
import { canManuallyTransitionProformaStatus } from "../services";
import { ProformaItemTaxes } from "../value-objects"; import { ProformaItemTaxes } from "../value-objects";
export interface IProformaCreateProps { export interface IProformaCreateProps {
@ -301,6 +302,12 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
return this.taxRegimeCode.isSome(); 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> { public changeStatus(nextStatus: InvoiceStatus): Result<boolean, Error> {
const currentStatus = this.status; const currentStatus = this.status;
@ -308,17 +315,7 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
return Result.ok(false); return Result.ok(false);
} }
if (nextStatus.toPrimitive() === INVOICE_STATUS.ISSUED) { if (!canManuallyTransitionProformaStatus({ currentStatus, nextStatus })) {
return Result.fail(
new InvalidProformaTransitionError(
currentStatus.toPrimitive(),
nextStatus.toPrimitive(),
this.id.toString()
)
);
}
if (!currentStatus.canTransitionTo(nextStatus)) {
return Result.fail( return Result.fail(
new InvalidProformaTransitionError( new InvalidProformaTransitionError(
currentStatus.toPrimitive(), currentStatus.toPrimitive(),
@ -333,21 +330,39 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
return Result.ok(true); 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, // Antes de cambiar el estado de la proforma,
// comprobamos que se cumplen las condiciones // comprobamos que se cumplen las condiciones
// necesarias. // necesarias.
if (!this.props.status.canTransitionTo("issued")) { if (!currentStatus.isApproved()) {
return Result.fail( return Result.fail(
new DomainValidationError( new InvalidProformaTransitionError(
"INVALID_STATE", currentStatus.toPrimitive(),
"status", INVOICE_STATUS.ISSUED,
"Proforma cannot be issued from current state" 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()) { if (this.series.isNone()) {
return Result.fail( return Result.fail(
new DomainValidationError( new DomainValidationError(
@ -444,12 +459,11 @@ export class Proforma extends AggregateRoot<ProformaInternalProps> implements IP
new DomainValidationError( new DomainValidationError(
"LINKED_INVOICE_NOT_ALLOWED", "LINKED_INVOICE_NOT_ALLOWED",
"linkedInvoiceId", "linkedInvoiceId",
"Proforma cannot be linked to an invoice" "Proforma is already linked to an invoice"
) )
); );
} }
this.props.status = InvoiceStatus.issued();
return Result.ok(); return Result.ok();
} }

View File

@ -1,4 +1,5 @@
export * from "./proforma-compare-tax-totals"; export * from "./proforma-compare-tax-totals";
export * from "./proforma-compute-tax-groups"; export * from "./proforma-compute-tax-groups";
export * from "./proforma-items-totals-calculator"; export * from "./proforma-items-totals-calculator";
export * from "./proforma-manual-status-transitions";
export * from "./proforma-taxes-calculator"; export * from "./proforma-taxes-calculator";

View File

@ -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);
}

View File

@ -14,11 +14,11 @@ import {
} from "../../../application"; } from "../../../application";
import type { Proforma } from "../../../domain"; import type { Proforma } from "../../../domain";
import { resolveProformaCatalogsDeps } from "./proforma-catalog-deps.di";
import { buildProformaNumberGenerator } from "./proforma-number-generator.di"; import { buildProformaNumberGenerator } from "./proforma-number-generator.di";
import { buildProformaPersistenceMappers } from "./proforma-persistence-mappers.di"; import { buildProformaPersistenceMappers } from "./proforma-persistence-mappers.di";
import { buildProformaRepository } from "./proforma-repositories.di"; import { buildProformaRepository } from "./proforma-repositories.di";
import type { ProformasInternalDeps } from "./proformas.di"; import type { ProformasInternalDeps } from "./proformas.di";
import { resolveProformaCatalogsDeps } from "./proforrma-catalog-deps.di";
type ProformaServicesContext = { type ProformaServicesContext = {
transaction: Transaction; transaction: Transaction;

View File

@ -18,6 +18,7 @@ import {
buildProformaCreator, buildProformaCreator,
buildProformaFinder, buildProformaFinder,
buildProformaInputMappers, buildProformaInputMappers,
buildProformaIssueReadModelAssembler,
buildProformaIssuer, buildProformaIssuer,
buildProformaReadModelAssemblers, buildProformaReadModelAssemblers,
buildProformaSnapshotBuilders, buildProformaSnapshotBuilders,
@ -104,7 +105,10 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
const issuer = buildProformaIssuer({ const issuer = buildProformaIssuer({
proformaConverter: proformaToIssuedInvoiceConverter, proformaConverter: proformaToIssuedInvoiceConverter,
repository, });
const issueReadModelAssembler = buildProformaIssueReadModelAssembler({
paymentMethodFinder: catalogs.paymentMethod.finder,
}); });
const documentGeneratorPipeline = buildProformaDocumentService(params); const documentGeneratorPipeline = buildProformaDocumentService(params);
@ -159,6 +163,8 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
publicServices, publicServices,
finder, finder,
issuer, issuer,
issueReadModelAssembler,
repository,
transactionManager, transactionManager,
}), }),

View File

@ -15,24 +15,24 @@ type ProformaCatalogsDeps = {
export function resolveProformaCatalogsDeps(params: ModuleParams): ProformaCatalogsDeps { export function resolveProformaCatalogsDeps(params: ModuleParams): ProformaCatalogsDeps {
const taxDefinition = const taxDefinition =
params.getService<CatalogsPublicServicesType["taxDefinitions"]>("catalogs:taxDefinition"); params.getService<CatalogsPublicServicesType["taxDefinitions"]>("catalogs:taxDefinitions");
if (!taxDefinition?.finder) { if (!taxDefinition?.finder) {
throw new Error("Missing public service: catalogs:taxDefinition.finder"); throw new Error("Missing public service: catalogs:taxDefinitions.finder");
} }
const taxRegime = const taxRegime =
params.getService<CatalogsPublicServicesType["taxRegimes"]>("catalogs:taxRegime"); params.getService<CatalogsPublicServicesType["taxRegimes"]>("catalogs:taxRegimes");
if (!taxRegime?.finder) { if (!taxRegime?.finder) {
throw new Error("Missing public service: catalogs:taxRegime.finder"); throw new Error("Missing public service: catalogs:taxRegimes.finder");
} }
const paymentMethod = const paymentMethod =
params.getService<CatalogsPublicServicesType["paymentMethods"]>("catalogs:paymentMethod"); params.getService<CatalogsPublicServicesType["paymentMethods"]>("catalogs:paymentMethods");
if (!paymentMethod?.finder) { if (!paymentMethod?.finder) {
throw new Error("Missing public service: catalogs:paymentMethod.finder"); throw new Error("Missing public service: catalogs:paymentMethods.finder");
} }
return { return {

View File

@ -275,7 +275,7 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
} }
} }
public async mapToPersistence( public mapToPersistence(
source: Proforma, source: Proforma,
params?: MapperParamsType params?: MapperParamsType
): Result<CustomerInvoiceCreationAttributes, Error> { ): Result<CustomerInvoiceCreationAttributes, Error> {
@ -350,7 +350,10 @@ export class SequelizeProformaDomainMapper extends SequelizeDomainMapper<
notes: maybeToNullable(source.notes, (v) => v.toPrimitive()), notes: maybeToNullable(source.notes, (v) => v.toPrimitive()),
payment_method_id: maybeToNullable(source.paymentMethodId, (value) => value.toPrimitive()), payment_method_id: maybeToNullable(source.paymentMethodId, (value) => value.toPrimitive()),
payment_method_description: null,
tax_regime_code: maybeToNullable(source.taxRegimeCode, (value) => value), tax_regime_code: maybeToNullable(source.taxRegimeCode, (value) => value),
tax_regime_description: null,
subtotal_amount_value: allAmounts.subtotalAmount.value, subtotal_amount_value: allAmounts.subtotalAmount.value,
subtotal_amount_scale: allAmounts.subtotalAmount.scale, subtotal_amount_scale: allAmounts.subtotalAmount.scale,

View File

@ -1,8 +1,12 @@
// modules/customer-invoices/src/api/infrastructure/proformas/persistence/sequelize/mappers/domain/sequelize-proforma-item-domain.mapper.ts
import { import {
DiscountPercentage, DiscountPercentage,
type MapperParamsType, type MapperParamsType,
SequelizeDomainMapper, SequelizeDomainMapper,
Tax, Tax,
type TaxCalculationBehavior,
type TaxGroup,
TaxPercentage,
} from "@erp/core/api"; } from "@erp/core/api";
import { import {
UniqueID, UniqueID,
@ -12,7 +16,7 @@ import {
maybeFromNullableResult, maybeFromNullableResult,
maybeToNullable, maybeToNullable,
} from "@repo/rdx-ddd"; } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import { import {
type IProformaCreateProps, type IProformaCreateProps,
@ -54,14 +58,14 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
); );
const description = extractOrPushError( const description = extractOrPushError(
maybeFromNullableResult(raw.description, (v) => ItemDescription.create(v)), maybeFromNullableResult(raw.description, (value) => ItemDescription.create(value)),
`items[${index}].description`, `items[${index}].description`,
errors errors
); );
const quantity = extractOrPushError( const quantity = extractOrPushError(
maybeFromNullableResult(raw.quantity_value, (v) => maybeFromNullableResult(raw.quantity_value, (value) =>
ItemQuantity.create({ value: v, scale: raw.quantity_scale }) ItemQuantity.create({ value, scale: raw.quantity_scale })
), ),
`items[${index}].quantity_value`, `items[${index}].quantity_value`,
errors errors
@ -80,9 +84,9 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
); );
const itemDiscountPercentage = extractOrPushError( const itemDiscountPercentage = extractOrPushError(
maybeFromNullableResult(raw.item_discount_percentage_value, (v) => maybeFromNullableResult(raw.item_discount_percentage_value, (value) =>
DiscountPercentage.create({ DiscountPercentage.create({
value: v, value,
scale: raw.item_discount_percentage_scale, scale: raw.item_discount_percentage_scale,
}) })
), ),
@ -91,22 +95,41 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
); );
const iva = extractOrPushError( const iva = extractOrPushError(
maybeFromNullableResult(raw.iva_code, (code) => Tax.createFromCode(code, this._taxCatalog)), this.mapTaxToDomain({
`items[${index}].iva_code`, 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 errors
); );
const rec = extractOrPushError( const rec = extractOrPushError(
maybeFromNullableResult(raw.rec_code, (code) => Tax.createFromCode(code, this._taxCatalog)), this.mapTaxToDomain({
`items[${index}].rec_code`, 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 errors
); );
const retention = extractOrPushError( const retention = extractOrPushError(
maybeFromNullableResult(raw.retention_code, (code) => this.mapTaxToDomain({
Tax.createFromCode(code, this._taxCatalog) code: raw.retention_code,
), percentageValue: raw.retention_percentage_value,
`items[${index}].retention_code`, percentageScale: raw.retention_percentage_scale,
group: "retention",
calculationBehavior: "subtractive",
fieldPath: `items[${index}].retention`,
}),
`items[${index}].retention`,
errors 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( public mapToDomain(
raw: CustomerInvoiceItemModel, raw: CustomerInvoiceItemModel,
params?: MapperParamsType params?: MapperParamsType
): Result<ProformaItem, Error> { ): Result<ProformaItem, Error> {
const { errors, index } = params as { const { errors } = params as {
index: number; index: number;
errors: ValidationErrorDetail[]; errors: ValidationErrorDetail[];
parent: Partial<IProformaCreateProps>; parent: Partial<IProformaCreateProps>;
}; };
// 1) Valores escalares (atributos generales)
const attributes = this.mapAttributesToDomain(raw, params); const attributes = this.mapAttributesToDomain(raw, params);
// Si hubo errores de mapeo, devolvemos colección de validación
if (errors.length > 0) { if (errors.length > 0) {
return Result.fail( return Result.fail(
new ValidationErrorCollection("Customer invoice item mapping failed [mapToDomain]", errors) new ValidationErrorCollection("Customer invoice item mapping failed [mapToDomain]", errors)
); );
} }
// 2) Construcción del elemento de taxes
const taxesResult = ProformaItemTaxes.create({ const taxesResult = ProformaItemTaxes.create({
iva: attributes.iva!, iva: attributes.iva!,
rec: attributes.rec!, rec: attributes.rec!,
retention: attributes.retention!, retention: attributes.retention!,
}); });
// 2) Construcción del elemento de dominio if (taxesResult.isFailure) {
const itemId = attributes.itemId!; return Result.fail(taxesResult.error);
const newItem = ProformaItem.rehydrate( }
const item = ProformaItem.rehydrate(
{ {
description: attributes.description!, description: attributes.description!,
quantity: attributes.quantity!, quantity: attributes.quantity!,
unitAmount: attributes.unitAmount!, unitAmount: attributes.unitAmount!,
itemDiscountPercentage: attributes.itemDiscountPercentage!, itemDiscountPercentage: attributes.itemDiscountPercentage!,
taxes: taxesResult.data, taxes: taxesResult.data,
}, },
itemId attributes.itemId!
); );
return Result.ok(newItem); return Result.ok(item);
} }
public mapToPersistence( public mapToPersistence(
source: ProformaItem, source: ProformaItem,
params?: MapperParamsType params?: MapperParamsType
): Result<CustomerInvoiceItemCreationAttributes, Error> { ): Result<CustomerInvoiceItemCreationAttributes, Error> {
const { errors, index, parent } = params as { const { index, parent } = params as {
index: number; index: number;
parent: Proforma; parent: Proforma;
errors: ValidationErrorDetail[]; errors: ValidationErrorDetail[];
@ -187,34 +249,32 @@ export class SequelizeProformaItemDomainMapper extends SequelizeDomainMapper<
invoice_id: parent.id.toPrimitive(), invoice_id: parent.id.toPrimitive(),
position: index, 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: quantity_scale:
maybeToNullable(source.quantity, (v) => v.toPrimitive().scale) ?? maybeToNullable(source.quantity, (value) => value.toPrimitive().scale) ??
ItemQuantity.DEFAULT_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: unit_amount_scale:
maybeToNullable(source.unitAmount, (v) => v.toPrimitive().scale) ?? maybeToNullable(source.unitAmount, (value) => value.toPrimitive().scale) ??
ItemAmount.DEFAULT_SCALE, ItemAmount.DEFAULT_SCALE,
subtotal_amount_value: allAmounts.subtotalAmount.value, subtotal_amount_value: allAmounts.subtotalAmount.value,
subtotal_amount_scale: allAmounts.subtotalAmount.scale, subtotal_amount_scale: allAmounts.subtotalAmount.scale,
//
item_discount_percentage_value: maybeToNullable( item_discount_percentage_value: maybeToNullable(
source.itemDiscountPercentage, source.itemDiscountPercentage,
(v) => v.toPrimitive().value (value) => value.toPrimitive().value
), ),
item_discount_percentage_scale: item_discount_percentage_scale:
maybeToNullable(source.itemDiscountPercentage, (v) => v.toPrimitive().scale) ?? maybeToNullable(source.itemDiscountPercentage, (value) => value.toPrimitive().scale) ??
DiscountPercentage.DEFAULT_SCALE, DiscountPercentage.DEFAULT_SCALE,
item_discount_amount_value: allAmounts.itemDiscountAmount.value, item_discount_amount_value: allAmounts.itemDiscountAmount.value,
item_discount_amount_scale: allAmounts.itemDiscountAmount.scale, item_discount_amount_scale: allAmounts.itemDiscountAmount.scale,
//
global_discount_percentage_value: parent.globalDiscountPercentage.toPrimitive().value, global_discount_percentage_value: parent.globalDiscountPercentage.toPrimitive().value,
global_discount_percentage_scale: global_discount_percentage_scale:
parent.globalDiscountPercentage.toPrimitive().scale ?? DiscountPercentage.DEFAULT_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_value: allAmounts.globalDiscountAmount.value,
global_discount_amount_scale: allAmounts.globalDiscountAmount.scale, global_discount_amount_scale: allAmounts.globalDiscountAmount.scale,
//
total_discount_amount_value: allAmounts.totalDiscountAmount.value, total_discount_amount_value: allAmounts.totalDiscountAmount.value,
total_discount_amount_scale: allAmounts.totalDiscountAmount.scale, total_discount_amount_scale: allAmounts.totalDiscountAmount.scale,
//
taxable_amount_value: allAmounts.taxableAmount.value, taxable_amount_value: allAmounts.taxableAmount.value,
taxable_amount_scale: allAmounts.taxableAmount.scale, taxable_amount_scale: allAmounts.taxableAmount.scale,
// IVA iva_code: maybeToNullable(source.taxes.iva, (value) => value.code),
iva_code: maybeToNullable(source.taxes.iva, (v) => v.code), iva_percentage_value: maybeToNullable(source.taxes.iva, (value) => value.percentage.value),
iva_percentage_value: maybeToNullable(source.taxes.iva, (v) => v.percentage.value),
iva_percentage_scale: 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_value: allAmounts.ivaAmount.value,
iva_amount_scale: allAmounts.ivaAmount.scale, iva_amount_scale: allAmounts.ivaAmount.scale,
// REC rec_code: maybeToNullable(source.taxes.rec, (value) => value.code),
rec_code: maybeToNullable(source.taxes.rec, (v) => v.code), rec_percentage_value: maybeToNullable(source.taxes.rec, (value) => value.percentage.value),
rec_percentage_value: maybeToNullable(source.taxes.rec, (v) => v.percentage.value),
rec_percentage_scale: 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_value: allAmounts.recAmount.value,
rec_amount_scale: allAmounts.recAmount.scale, rec_amount_scale: allAmounts.recAmount.scale,
// RET retention_code: maybeToNullable(source.taxes.retention, (value) => value.code),
retention_code: maybeToNullable(source.taxes.retention, (v) => v.code),
retention_percentage_value: maybeToNullable( retention_percentage_value: maybeToNullable(
source.taxes.retention, source.taxes.retention,
(v) => v.percentage.value (value) => value.percentage.value
), ),
retention_percentage_scale: 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_value: allAmounts.retentionAmount.value,
retention_amount_scale: allAmounts.retentionAmount.scale, retention_amount_scale: allAmounts.retentionAmount.scale,
//
taxes_amount_value: allAmounts.taxesAmount.value, taxes_amount_value: allAmounts.taxesAmount.value,
taxes_amount_scale: allAmounts.taxesAmount.scale, taxes_amount_scale: allAmounts.taxesAmount.scale,
//
total_amount_value: allAmounts.totalAmount.value, total_amount_value: allAmounts.totalAmount.value,
total_amount_scale: allAmounts.totalAmount.scale, total_amount_scale: allAmounts.totalAmount.scale,
}); });

View File

@ -10,7 +10,7 @@ import { type Collection, Result } from "@repo/rdx-utils";
import type { FindOptions, InferAttributes, OrderItem, Sequelize, Transaction } from "sequelize"; import type { FindOptions, InferAttributes, OrderItem, Sequelize, Transaction } from "sequelize";
import type { IProformaRepository, ProformaSummary } from "../../../../../application"; import type { IProformaRepository, ProformaSummary } from "../../../../../application";
import type { InvoiceStatus, Proforma } from "../../../../../domain"; import { INVOICE_STATUS, type InvoiceStatus, type Proforma } from "../../../../../domain";
import { import {
CustomerInvoiceItemModel, CustomerInvoiceItemModel,
CustomerInvoiceModel, 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. * Busca una factura por su identificador único.