feat: Enhance proforma issuance with tax regime support
- Added tax regime to ProformaIssueReadModel and related services. - Implemented tax regime resolution in ProformaToIssuedInvoiceConverter. - Updated IssuedInvoiceFullSnapshotBuilder to include tax regime details. - Enhanced ProformaIssueReadModelAssembler to handle tax regime fetching. - Introduced IssueProformaResponseSnapshotBuilder for response mapping. - Updated routes and controllers to accommodate new proforma issuance logic. - Refactored request and response DTOs to include tax regime information. - Added command structure for issuing proformas.
This commit is contained in:
parent
9d7e18d73d
commit
1c0235de75
@ -1,5 +1,8 @@
|
||||
import type { Proforma } from "../../../domain";
|
||||
import type { InvoicePaymentMethodReadModel } from "../../common/models";
|
||||
import type {
|
||||
InvocieTaxRegimeFullReadModel,
|
||||
InvoicePaymentMethodReadModel,
|
||||
} from "../../common/models";
|
||||
|
||||
/**
|
||||
* Modelo de lectura usado exclusivamente durante la emisión de una proforma.
|
||||
@ -10,4 +13,5 @@ import type { InvoicePaymentMethodReadModel } from "../../common/models";
|
||||
export interface ProformaIssueReadModel {
|
||||
proforma: Proforma;
|
||||
paymentMethod: InvoicePaymentMethodReadModel;
|
||||
taxRegime: InvocieTaxRegimeFullReadModel;
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
type IIssuedInvoiceCreateProps,
|
||||
InvoicePaymentMethod,
|
||||
InvoiceStatus,
|
||||
InvoiceTaxRegime,
|
||||
IssuedInvoiceItem,
|
||||
IssuedInvoiceRecipient,
|
||||
IssuedInvoiceTax,
|
||||
@ -57,6 +58,12 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic
|
||||
return Result.fail(paymentResult.error);
|
||||
}
|
||||
|
||||
const taxRegimeResult = this.resolveTaxRegime(source);
|
||||
|
||||
if (taxRegimeResult.isFailure) {
|
||||
return Result.fail(taxRegimeResult.error);
|
||||
}
|
||||
|
||||
const proformaTotals = proforma.totals();
|
||||
|
||||
return Result.ok({
|
||||
@ -78,6 +85,7 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic
|
||||
reference: proforma.reference,
|
||||
|
||||
paymentMethod: paymentResult.data,
|
||||
taxRegime: taxRegimeResult.data,
|
||||
|
||||
customerId: proforma.customerId,
|
||||
recipient: recipientResult.data,
|
||||
@ -251,4 +259,17 @@ export class ProformaToIssuedInvoiceConverter implements IProformaToIssuedInvoic
|
||||
|
||||
return Result.ok(paymentMethodResult.data);
|
||||
}
|
||||
|
||||
private resolveTaxRegime(source: ProformaIssueReadModel): Result<InvoiceTaxRegime, Error> {
|
||||
const taxRegimeResult = InvoiceTaxRegime.create({
|
||||
code: source.taxRegime.code,
|
||||
description: source.taxRegime.description || source.taxRegime.code,
|
||||
});
|
||||
|
||||
if (taxRegimeResult.isFailure) {
|
||||
return Result.fail(taxRegimeResult.error);
|
||||
}
|
||||
|
||||
return Result.ok(taxRegimeResult.data);
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,6 +31,11 @@ export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnaps
|
||||
payment_description: invoice.paymentMethod.name.toString(),
|
||||
};
|
||||
|
||||
const tax_regime = {
|
||||
code: invoice.taxRegime.code,
|
||||
description: invoice.taxRegime.description,
|
||||
};
|
||||
|
||||
return {
|
||||
id: invoice.id.toString(),
|
||||
company_id: invoice.companyId.toString(),
|
||||
@ -58,6 +63,7 @@ export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnaps
|
||||
taxes,
|
||||
|
||||
payment_method,
|
||||
tax_regime,
|
||||
|
||||
subtotal_amount: invoice.subtotalAmount.toObjectString(),
|
||||
|
||||
|
||||
@ -44,6 +44,10 @@ export interface IIssuedInvoiceFullSnapshot {
|
||||
payment_id: string;
|
||||
payment_description: string;
|
||||
} | null;
|
||||
tax_regime: {
|
||||
code: string;
|
||||
description: string | null;
|
||||
};
|
||||
|
||||
subtotal_amount: { value: string; scale: string; currency_code: string };
|
||||
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from "./issue-proforma.command";
|
||||
@ -0,0 +1,5 @@
|
||||
export type IssueProformaCommand = {
|
||||
companyId: string;
|
||||
proformaId: string;
|
||||
actorUserId?: string;
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import type { IPaymentMethodPublicFinder } from "@erp/catalogs/api";
|
||||
import type { IPaymentMethodPublicFinder, ITaxRegimePublicFinder } from "@erp/catalogs/api";
|
||||
|
||||
import type { IProformaToIssuedInvoiceConverter } from "../../issued-invoices";
|
||||
import { type IProformaIssuer, ProformaIssueReadModelAssembler, ProformaIssuer } from "../services";
|
||||
@ -13,8 +13,10 @@ export const buildProformaIssuer = (params: {
|
||||
|
||||
export const buildProformaIssueReadModelAssembler = (params: {
|
||||
paymentMethodFinder: IPaymentMethodPublicFinder;
|
||||
taxRegimeFinder: ITaxRegimePublicFinder;
|
||||
}) => {
|
||||
return new ProformaIssueReadModelAssembler({
|
||||
paymentMethodFinder: params.paymentMethodFinder,
|
||||
taxRegimeFinder: params.taxRegimeFinder,
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
// application/issued-invoices/di/snapshot-builders.di.ts
|
||||
|
||||
import {
|
||||
IssueProformaResponseSnapshotBuilder,
|
||||
ProformaFullSnapshotBuilder,
|
||||
ProformaItemReportSnapshotBuilder,
|
||||
ProformaItemsFullSnapshotBuilder,
|
||||
@ -32,9 +33,11 @@ export function buildProformaSnapshotBuilders() {
|
||||
itemsReportBuilder,
|
||||
taxesReportBuilder
|
||||
);
|
||||
const issueResponseSnapshotBuilder = new IssueProformaResponseSnapshotBuilder();
|
||||
|
||||
return {
|
||||
full: fullSnapshotBuilder,
|
||||
issue: issueResponseSnapshotBuilder,
|
||||
summary: summarySnapshotBuilder,
|
||||
report: reportSnapshotBuilder,
|
||||
};
|
||||
|
||||
@ -16,6 +16,7 @@ import type {
|
||||
} from "../services";
|
||||
import type {
|
||||
IProformaFullSnapshotBuilder,
|
||||
IIssueProformaResponseSnapshotBuilder,
|
||||
IProformaReportSnapshotBuilder,
|
||||
IProformaSummarySnapshotBuilder,
|
||||
} from "../snapshot-builders";
|
||||
@ -121,6 +122,7 @@ export function buildIssueProformaUseCase(deps: {
|
||||
finder: IProformaFinder;
|
||||
issuer: IProformaIssuer;
|
||||
issueReadModelAssembler: IProformaIssueReadModelAssembler;
|
||||
issueResponseSnapshotBuilder: IIssueProformaResponseSnapshotBuilder;
|
||||
repository: IProformaRepository;
|
||||
transactionManager: ITransactionManager;
|
||||
}) {
|
||||
@ -128,6 +130,7 @@ export function buildIssueProformaUseCase(deps: {
|
||||
finder,
|
||||
issuer,
|
||||
issueReadModelAssembler,
|
||||
issueResponseSnapshotBuilder,
|
||||
repository,
|
||||
transactionManager,
|
||||
publicServices: { issuedInvoiceServices },
|
||||
@ -138,6 +141,7 @@ export function buildIssueProformaUseCase(deps: {
|
||||
finder,
|
||||
issuer,
|
||||
issueReadModelAssembler,
|
||||
issueResponseSnapshotBuilder,
|
||||
repository,
|
||||
transactionManager,
|
||||
});
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export * from "./commands";
|
||||
export * from "./di";
|
||||
export * from "./errors";
|
||||
export * from "./mappers";
|
||||
|
||||
@ -15,7 +15,6 @@ import { Maybe, NumberHelper, Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { CreateProformaRequestDTO } from "../../../../common";
|
||||
import {
|
||||
InvoiceNumber,
|
||||
type InvoiceRecipient,
|
||||
InvoiceSerie,
|
||||
InvoiceStatus,
|
||||
@ -59,12 +58,6 @@ export class CreateProformaInputMapper implements ICreateProformaInputMapper {
|
||||
errors
|
||||
);
|
||||
|
||||
const invoiceNumber = extractOrPushError(
|
||||
InvoiceNumber.create(dto.invoice_number),
|
||||
"invoice_number",
|
||||
errors
|
||||
);
|
||||
|
||||
const series = extractOrPushError(
|
||||
maybeFromNullableResult(dto.series, (value) => InvoiceSerie.create(value)),
|
||||
"series",
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { IPaymentMethodPublicFinder } from "@erp/catalogs/api";
|
||||
import type { IPaymentMethodPublicFinder, ITaxRegimePublicFinder } from "@erp/catalogs/api";
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
@ -23,6 +23,7 @@ export class ProformaIssueReadModelAssembler implements IProformaIssueReadModelA
|
||||
public constructor(
|
||||
private readonly deps: {
|
||||
paymentMethodFinder: IPaymentMethodPublicFinder;
|
||||
taxRegimeFinder: ITaxRegimePublicFinder;
|
||||
}
|
||||
) {}
|
||||
|
||||
@ -47,6 +48,12 @@ export class ProformaIssueReadModelAssembler implements IProformaIssueReadModelA
|
||||
return Result.fail(paymentMethodResult.error);
|
||||
}
|
||||
|
||||
const taxRegimeResult = await this.resolveTaxRegime(params);
|
||||
|
||||
if (taxRegimeResult.isFailure) {
|
||||
return Result.fail(taxRegimeResult.error);
|
||||
}
|
||||
|
||||
const paymentMethod = paymentMethodResult.data;
|
||||
|
||||
return Result.ok({
|
||||
@ -56,6 +63,43 @@ export class ProformaIssueReadModelAssembler implements IProformaIssueReadModelA
|
||||
name: paymentMethod.name,
|
||||
description: paymentMethod.description,
|
||||
},
|
||||
taxRegime: taxRegimeResult.data,
|
||||
});
|
||||
}
|
||||
|
||||
private async resolveTaxRegime(params: {
|
||||
companyId: UniqueID;
|
||||
proforma: Proforma;
|
||||
transaction?: unknown;
|
||||
}) {
|
||||
if (params.proforma.taxRegimeCode.isNone()) {
|
||||
return Result.fail(new Error("Tax regime is required to issue proforma"));
|
||||
}
|
||||
|
||||
const taxRegimeCode = params.proforma.taxRegimeCode.unwrap();
|
||||
|
||||
const taxRegimeResult = await this.deps.taxRegimeFinder.findByCodeInCompany({
|
||||
companyId: params.companyId,
|
||||
code: taxRegimeCode,
|
||||
transaction: params.transaction,
|
||||
});
|
||||
|
||||
if (taxRegimeResult.isFailure) {
|
||||
return Result.fail(taxRegimeResult.error);
|
||||
}
|
||||
|
||||
if (taxRegimeResult.data.isNone()) {
|
||||
return Result.ok({
|
||||
code: taxRegimeCode,
|
||||
description: "",
|
||||
});
|
||||
}
|
||||
|
||||
const taxRegime = taxRegimeResult.data.unwrap();
|
||||
|
||||
return Result.ok({
|
||||
code: taxRegime.code,
|
||||
description: taxRegime.description,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
export * from "./full";
|
||||
export * from "./issue";
|
||||
export * from "./report";
|
||||
export * from "./summary";
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from "./issue-proforma-response.snapshot-builder";
|
||||
@ -0,0 +1,39 @@
|
||||
import type { ISnapshotBuilder } from "@erp/core/api";
|
||||
|
||||
import type { IssueProformaByIdResponseDTO } from "../../../../../common";
|
||||
import type { IssuedInvoice } from "../../../../domain";
|
||||
|
||||
export interface IIssueProformaResponseSnapshotBuilder
|
||||
extends ISnapshotBuilder<IssuedInvoice, IssueProformaByIdResponseDTO> {}
|
||||
|
||||
export class IssueProformaResponseSnapshotBuilder
|
||||
implements IIssueProformaResponseSnapshotBuilder
|
||||
{
|
||||
toOutput(invoice: IssuedInvoice): IssueProformaByIdResponseDTO {
|
||||
return {
|
||||
issued_invoice: {
|
||||
id: invoice.id.toString(),
|
||||
proforma_id: invoice.linkedProformaId.toString(),
|
||||
series: invoice.series.toString(),
|
||||
invoice_number: invoice.invoiceNumber.toString(),
|
||||
invoice_date: invoice.invoiceDate.toDateString(),
|
||||
status: invoice.status.toPrimitive(),
|
||||
recipient: {
|
||||
name: invoice.recipient.name.toString(),
|
||||
tin: invoice.recipient.tin.toString(),
|
||||
},
|
||||
tax_regime: {
|
||||
code: invoice.taxRegime.code,
|
||||
description: invoice.taxRegime.description,
|
||||
},
|
||||
totals: {
|
||||
taxable_base: invoice.taxableAmount.toObjectString(),
|
||||
tax_total: invoice.taxesAmount.toObjectString(),
|
||||
retention_total: invoice.retentionAmount.toObjectString(),
|
||||
grand_total: invoice.totalAmount.toObjectString(),
|
||||
currency_code: invoice.currencyCode.toPrimitive(),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -3,17 +3,14 @@ import { UniqueID } from "@repo/rdx-ddd";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
|
||||
import type { IIssuedInvoicePublicServices } from "../../issued-invoices";
|
||||
import type { IssueProformaCommand } from "../commands";
|
||||
import type { IProformaRepository } from "../repositories";
|
||||
import type {
|
||||
IProformaFinder,
|
||||
IProformaIssueReadModelAssembler,
|
||||
IProformaIssuer,
|
||||
} from "../services";
|
||||
|
||||
type IssueProformaUseCaseInput = {
|
||||
companyId: UniqueID;
|
||||
proforma_id: string;
|
||||
};
|
||||
import type { IIssueProformaResponseSnapshotBuilder } from "../snapshot-builders";
|
||||
|
||||
/**
|
||||
* Caso de uso: conversión de una proforma en factura definitiva.
|
||||
@ -31,15 +28,22 @@ export class IssueProformaUseCase {
|
||||
finder: IProformaFinder;
|
||||
issuer: IProformaIssuer;
|
||||
issueReadModelAssembler: IProformaIssueReadModelAssembler;
|
||||
issueResponseSnapshotBuilder: IIssueProformaResponseSnapshotBuilder;
|
||||
repository: IProformaRepository;
|
||||
transactionManager: ITransactionManager;
|
||||
}
|
||||
) {}
|
||||
|
||||
public execute(params: IssueProformaUseCaseInput) {
|
||||
const { proforma_id, companyId } = params;
|
||||
public execute(command: IssueProformaCommand) {
|
||||
const companyIdResult = UniqueID.create(command.companyId);
|
||||
|
||||
const proformaIdResult = UniqueID.create(proforma_id);
|
||||
if (companyIdResult.isFailure) {
|
||||
return Result.fail(companyIdResult.error);
|
||||
}
|
||||
|
||||
const companyId = companyIdResult.data;
|
||||
|
||||
const proformaIdResult = UniqueID.create(command.proformaId);
|
||||
|
||||
if (proformaIdResult.isFailure) {
|
||||
return Result.fail(proformaIdResult.error);
|
||||
@ -106,11 +110,7 @@ export class IssueProformaUseCase {
|
||||
return Result.fail(markAsIssuedResult.error);
|
||||
}
|
||||
|
||||
return Result.ok({
|
||||
issuedinvoice_id: issuedInvoiceId.toString(),
|
||||
proforma_id: proformaId.toString(),
|
||||
customer_id: proforma.customerId.toString(),
|
||||
});
|
||||
return Result.ok(this.deps.issueResponseSnapshotBuilder.toOutput(invoiceResult.data));
|
||||
} catch (error: unknown) {
|
||||
return Result.fail(error as Error);
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ import type {
|
||||
InvoicePaymentMethod,
|
||||
InvoiceSerie,
|
||||
InvoiceStatus,
|
||||
InvoiceTaxRegime,
|
||||
} from "../../common";
|
||||
import {
|
||||
type IIssuedInvoiceItemCreateProps,
|
||||
@ -51,6 +52,7 @@ export interface IIssuedInvoiceCreateProps {
|
||||
currencyCode: CurrencyCode;
|
||||
|
||||
paymentMethod: InvoicePaymentMethod;
|
||||
taxRegime: InvoiceTaxRegime;
|
||||
|
||||
items: IIssuedInvoiceItemCreateProps[];
|
||||
taxes: IssuedInvoiceTaxes;
|
||||
@ -139,6 +141,16 @@ export class IssuedInvoice
|
||||
);
|
||||
}
|
||||
|
||||
if (!props.taxRegime) {
|
||||
return Result.fail(
|
||||
new DomainValidationError(
|
||||
"MISSING_TAX_REGIME",
|
||||
"taxRegime",
|
||||
"Issued invoice requires tax regime"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@ -222,6 +234,10 @@ export class IssuedInvoice
|
||||
return this.props.paymentMethod;
|
||||
}
|
||||
|
||||
public get taxRegime(): InvoiceTaxRegime {
|
||||
return this.props.taxRegime;
|
||||
}
|
||||
|
||||
public get languageCode(): LanguageCode {
|
||||
return this.props.languageCode;
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { DiscountPercentage, type MapperParamsType, SequelizeDomainMapper } from "@erp/core/api";
|
||||
import {
|
||||
CurrencyCode,
|
||||
DomainValidationError,
|
||||
LanguageCode,
|
||||
TextValue,
|
||||
UniqueID,
|
||||
@ -20,6 +21,7 @@ import {
|
||||
InvoicePaymentMethod,
|
||||
InvoiceSerie,
|
||||
InvoiceStatus,
|
||||
InvoiceTaxRegime,
|
||||
IssuedInvoice,
|
||||
IssuedInvoiceItems,
|
||||
IssuedInvoiceTaxes,
|
||||
@ -154,6 +156,23 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
|
||||
}
|
||||
}
|
||||
|
||||
const taxRegime = extractOrPushError(
|
||||
!isNullishOrEmpty(raw.tax_regime_code)
|
||||
? InvoiceTaxRegime.create({
|
||||
code: String(raw.tax_regime_code),
|
||||
description: String(raw.tax_regime_description ?? raw.tax_regime_code),
|
||||
})
|
||||
: Result.fail(
|
||||
new DomainValidationError(
|
||||
"MISSING_TAX_REGIME",
|
||||
"tax_regime",
|
||||
"Issued invoice requires persisted tax regime"
|
||||
)
|
||||
),
|
||||
"tax_regime",
|
||||
errors
|
||||
);
|
||||
|
||||
const subtotalAmount = extractOrPushError(
|
||||
InvoiceAmount.create({
|
||||
value: raw.subtotal_amount_value,
|
||||
@ -270,6 +289,7 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
|
||||
languageCode,
|
||||
currencyCode,
|
||||
paymentMethod,
|
||||
taxRegime,
|
||||
|
||||
subtotalAmount,
|
||||
itemsDiscountAmount,
|
||||
@ -384,6 +404,7 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
|
||||
totalAmount: attributes.totalAmount!,
|
||||
|
||||
paymentMethod: attributes.paymentMethod!,
|
||||
taxRegime: attributes.taxRegime!,
|
||||
|
||||
taxes,
|
||||
verifactu,
|
||||
@ -483,6 +504,8 @@ export class SequelizeIssuedInvoiceDomainMapper extends SequelizeDomainMapper<
|
||||
|
||||
payment_method_id: source.paymentMethod.toObjectString().id,
|
||||
payment_method_description: source.paymentMethod.toObjectString().name,
|
||||
tax_regime_code: source.taxRegime.code,
|
||||
tax_regime_description: source.taxRegime.description,
|
||||
|
||||
subtotal_amount_value: source.subtotalAmount.value,
|
||||
subtotal_amount_scale: source.subtotalAmount.scale,
|
||||
|
||||
@ -117,6 +117,7 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
||||
|
||||
const issueReadModelAssembler = buildProformaIssueReadModelAssembler({
|
||||
paymentMethodFinder: catalogs.paymentMethod.finder,
|
||||
taxRegimeFinder: catalogs.taxRegime.finder,
|
||||
});
|
||||
|
||||
const companyReportProfileFinder = new CompanyReportProfileFinder({
|
||||
@ -188,6 +189,7 @@ export function buildProformasDependencies(params: ModuleParams): ProformasInter
|
||||
finder,
|
||||
issuer,
|
||||
issueReadModelAssembler,
|
||||
issueResponseSnapshotBuilder: snapshotBuilders.issue,
|
||||
repository,
|
||||
transactionManager,
|
||||
}),
|
||||
|
||||
@ -5,11 +5,16 @@ import {
|
||||
requireCompanyContextGuard,
|
||||
} from "@erp/core/api";
|
||||
|
||||
import { IssueProformaByIdResponseSchema } from "../../../../../common";
|
||||
import type { IssueProformaUseCase } from "../../../../application/proformas";
|
||||
import type { IIssueProformaByIdRequestMapper } from "../mappers";
|
||||
import { proformasApiErrorMapper } from "../proformas-api-error-mapper.ts";
|
||||
|
||||
export class IssueProformaController extends ExpressController {
|
||||
public constructor(private readonly useCase: IssueProformaUseCase) {
|
||||
public constructor(
|
||||
private readonly useCase: IssueProformaUseCase,
|
||||
private readonly requestMapper: IIssueProformaByIdRequestMapper
|
||||
) {
|
||||
super();
|
||||
this.errorMapper = proformasApiErrorMapper;
|
||||
|
||||
@ -27,15 +32,26 @@ export class IssueProformaController extends ExpressController {
|
||||
return this.forbiddenError("Tenant ID not found");
|
||||
}
|
||||
|
||||
const { proforma_id } = this.req.params;
|
||||
if (!proforma_id) {
|
||||
const params = this.req.params;
|
||||
const proformaId = params.id ?? params.proforma_id;
|
||||
|
||||
if (!proformaId) {
|
||||
return this.invalidInputError("Proforma ID missing");
|
||||
}
|
||||
|
||||
const result = await this.useCase.execute({ proforma_id, companyId });
|
||||
const command = this.requestMapper.map({
|
||||
companyId: companyId.toString(),
|
||||
params: params as
|
||||
| { id: string }
|
||||
| {
|
||||
proforma_id: string;
|
||||
},
|
||||
});
|
||||
|
||||
const result = await this.useCase.execute(command);
|
||||
|
||||
return result.match(
|
||||
(data) => this.ok(data),
|
||||
(data) => this.ok(IssueProformaByIdResponseSchema.parse(data)),
|
||||
(err) => this.handleError(err)
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export * from "./controllers";
|
||||
export * from "./mappers";
|
||||
export * from "./proformas.routes";
|
||||
|
||||
@ -0,0 +1 @@
|
||||
export * from "./issue-proforma-by-id-request.mapper";
|
||||
@ -0,0 +1,27 @@
|
||||
import type {
|
||||
IssueProformaByIdParamsRequestDTO,
|
||||
LegacyIssueProformaByIdParamsRequestDTO,
|
||||
} from "../../../../../common";
|
||||
import type { IssueProformaCommand } from "../../../../application/proformas";
|
||||
|
||||
type IssueProformaRequestMapperInput = {
|
||||
companyId: string;
|
||||
params: IssueProformaByIdParamsRequestDTO | LegacyIssueProformaByIdParamsRequestDTO;
|
||||
actorUserId?: string;
|
||||
};
|
||||
|
||||
export interface IIssueProformaByIdRequestMapper {
|
||||
map(params: IssueProformaRequestMapperInput): IssueProformaCommand;
|
||||
}
|
||||
|
||||
export class IssueProformaByIdRequestMapper implements IIssueProformaByIdRequestMapper {
|
||||
map(params: IssueProformaRequestMapperInput): IssueProformaCommand {
|
||||
const proformaId = "id" in params.params ? params.params.id : params.params.proforma_id;
|
||||
|
||||
return {
|
||||
companyId: params.companyId,
|
||||
proformaId,
|
||||
actorUserId: params.actorUserId,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,7 @@ import {
|
||||
ChangeStatusProformaByIdParamsRequestSchema,
|
||||
ChangeStatusProformaByIdRequestSchema,
|
||||
CreateProformaRequestSchema,
|
||||
LegacyIssueProformaByIdParamsRequestSchema,
|
||||
GetProformaByIdRequestSchema,
|
||||
IssueProformaByIdParamsRequestSchema,
|
||||
ListProformasRequestSchema,
|
||||
@ -27,6 +28,7 @@ import type { IIssuedInvoicePublicServices } from "../../../application";
|
||||
|
||||
import { ChangeStatusProformaController } from "./controllers/change-status-proforma.controller";
|
||||
import { CreateProformaController } from "./controllers/create-proforma.controller";
|
||||
import { IssueProformaByIdRequestMapper } from "./mappers";
|
||||
import { UpdateProformaController } from "./controllers/update-proforma.controller";
|
||||
|
||||
export const proformasRouter = (params: StartParams) => {
|
||||
@ -141,17 +143,29 @@ export const proformasRouter = (params: StartParams) => {
|
||||
}
|
||||
);
|
||||
|
||||
const issueProformaHandler = (req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.useCases.issueProforma(publicServices);
|
||||
const requestMapper = new IssueProformaByIdRequestMapper();
|
||||
const controller = new IssueProformaController(useCase, requestMapper);
|
||||
return controller.execute(req, res, next);
|
||||
};
|
||||
|
||||
router.post(
|
||||
"/:id/issue",
|
||||
validateRequest(IssueProformaByIdParamsRequestSchema, "params"),
|
||||
issueProformaHandler
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/:proforma_id/issue",
|
||||
validateRequest(LegacyIssueProformaByIdParamsRequestSchema, "params"),
|
||||
issueProformaHandler
|
||||
);
|
||||
|
||||
router.put(
|
||||
"/:proforma_id/issue",
|
||||
//checkTabContext,
|
||||
|
||||
validateRequest(IssueProformaByIdParamsRequestSchema, "params"),
|
||||
|
||||
(req: Request, res: Response, next: NextFunction) => {
|
||||
const useCase = deps.useCases.issueProforma(publicServices);
|
||||
const controller = new IssueProformaController(useCase);
|
||||
return controller.execute(req, res, next);
|
||||
}
|
||||
validateRequest(LegacyIssueProformaByIdParamsRequestSchema, "params"),
|
||||
issueProformaHandler
|
||||
);
|
||||
|
||||
app.use(`${config.server.apiBasePath}/proformas`, router);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { UniqueID } from "@repo/rdx-ddd";
|
||||
import { type Maybe, Result } from "@repo/rdx-utils";
|
||||
import { type Transaction, type WhereOptions, literal } from "sequelize";
|
||||
import { type Transaction, type WhereOptions } from "sequelize";
|
||||
|
||||
import type { IProformaNumberGenerator } from "../../../../../application/proformas";
|
||||
import { InvoiceNumber, type InvoiceSerie } from "../../../../../domain";
|
||||
@ -11,6 +11,9 @@ import { CustomerInvoiceModel } from "../../../../common/persistence";
|
||||
* La serie elegida en la proforma solo referencia la futura InvoiceSeries.
|
||||
*/
|
||||
export class SequelizeProformaNumberGenerator implements IProformaNumberGenerator {
|
||||
private static readonly PREFIX = "PF-";
|
||||
private static readonly DIGITS = 4;
|
||||
|
||||
public async getNextForCompany(
|
||||
companyId: UniqueID,
|
||||
_series: Maybe<InvoiceSerie>,
|
||||
@ -22,24 +25,20 @@ export class SequelizeProformaNumberGenerator implements IProformaNumberGenerato
|
||||
};
|
||||
|
||||
try {
|
||||
const lastInvoice = await CustomerInvoiceModel.findOne({
|
||||
const existingInvoices = await CustomerInvoiceModel.findAll({
|
||||
attributes: ["invoice_number"],
|
||||
where,
|
||||
// Orden numérico real: CAST(... AS UNSIGNED)
|
||||
order: [literal("CAST(invoice_number AS UNSIGNED) DESC")],
|
||||
transaction,
|
||||
raw: true,
|
||||
// Bloqueo opcional para evitar carreras si estás dentro de una TX
|
||||
lock: transaction.LOCK.UPDATE, // requiere InnoDB y TX abierta
|
||||
lock: transaction.LOCK.UPDATE,
|
||||
});
|
||||
|
||||
let nextValue = "0001"; // valor inicial por defecto
|
||||
const maxSequence = existingInvoices.reduce((max, currentInvoice) => {
|
||||
const currentSequence = this.extractSequence(currentInvoice.invoice_number);
|
||||
return Math.max(max, currentSequence);
|
||||
}, 0);
|
||||
|
||||
if (lastInvoice) {
|
||||
const current = Number(lastInvoice.invoice_number);
|
||||
const next = Number.isFinite(current) && current > 0 ? current + 1 : 1;
|
||||
nextValue = String(next).padStart(4, "0");
|
||||
}
|
||||
const nextValue = this.formatSequence(maxSequence + 1);
|
||||
|
||||
const numberResult = InvoiceNumber.create(nextValue);
|
||||
if (numberResult.isFailure) {
|
||||
@ -50,9 +49,27 @@ export class SequelizeProformaNumberGenerator implements IProformaNumberGenerato
|
||||
} catch (error) {
|
||||
return Result.fail(
|
||||
new Error(
|
||||
`Error generating invoice number for company ${companyId}: ${(error as Error).message}`
|
||||
`Error generating proforma number for company ${companyId}: ${(error as Error).message}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private extractSequence(invoiceNumber: string): number {
|
||||
const normalized = invoiceNumber.trim().toUpperCase();
|
||||
|
||||
if (normalized.startsWith(SequelizeProformaNumberGenerator.PREFIX)) {
|
||||
const suffix = normalized.slice(SequelizeProformaNumberGenerator.PREFIX.length);
|
||||
const parsed = Number(suffix);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
||||
}
|
||||
|
||||
const parsed = Number(normalized);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
||||
}
|
||||
|
||||
private formatSequence(sequence: number): string {
|
||||
const padded = String(sequence).padStart(SequelizeProformaNumberGenerator.DIGITS, "0");
|
||||
return `${SequelizeProformaNumberGenerator.PREFIX}${padded}`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,7 +28,7 @@ export type CreateProformaItemRequestDTO = z.infer<typeof CreateProformaItemRequ
|
||||
export const CreateProformaRequestSchema = z.object({
|
||||
id: z.uuid(),
|
||||
|
||||
invoice_number: z.string(),
|
||||
invoice_number: z.string().nullable().optional(),
|
||||
series: z.string().nullable(),
|
||||
|
||||
invoice_date: IsoDateSchema,
|
||||
|
||||
@ -1,9 +1,17 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const IssueProformaByIdParamsRequestSchema = z.object({
|
||||
proforma_id: z.string(),
|
||||
id: z.string(),
|
||||
});
|
||||
|
||||
export type IssueProformaByIdParamsRequestDTO = z.infer<
|
||||
typeof IssueProformaByIdParamsRequestSchema
|
||||
>;
|
||||
|
||||
export const LegacyIssueProformaByIdParamsRequestSchema = z.object({
|
||||
proforma_id: z.string(),
|
||||
});
|
||||
|
||||
export type LegacyIssueProformaByIdParamsRequestDTO = z.infer<
|
||||
typeof LegacyIssueProformaByIdParamsRequestSchema
|
||||
>;
|
||||
|
||||
@ -46,7 +46,7 @@ export const GetIssuedInvoiceByIdResponseSchema = z.object({
|
||||
|
||||
payment_method: PaymentMethodRefSchema.nullable(),
|
||||
|
||||
tax_regime: TaxRegimeRefSchema.nullable(),
|
||||
tax_regime: TaxRegimeRefSchema,
|
||||
|
||||
subtotal_amount: MoneySchema,
|
||||
|
||||
|
||||
@ -1,9 +1,35 @@
|
||||
import { CurrencyCodeSchema, IsoDateSchema, MoneySchema } from "@erp/core";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { IssuedInvoiceStatusSchema, TaxRegimeRefSchema } from "../../shared";
|
||||
|
||||
export const IssueProformaIssuedInvoiceRecipientSchema = z.object({
|
||||
name: z.string(),
|
||||
tin: z.string().nullable(),
|
||||
});
|
||||
|
||||
export const IssueProformaIssuedInvoiceTotalsSchema = z.object({
|
||||
taxable_base: MoneySchema,
|
||||
tax_total: MoneySchema,
|
||||
retention_total: MoneySchema,
|
||||
grand_total: MoneySchema,
|
||||
currency_code: CurrencyCodeSchema,
|
||||
});
|
||||
|
||||
export const IssueProformaIssuedInvoiceSchema = z.object({
|
||||
id: z.uuid(),
|
||||
proforma_id: z.uuid().nullable(),
|
||||
series: z.string(),
|
||||
invoice_number: z.string(),
|
||||
invoice_date: IsoDateSchema,
|
||||
status: IssuedInvoiceStatusSchema,
|
||||
recipient: IssueProformaIssuedInvoiceRecipientSchema,
|
||||
tax_regime: TaxRegimeRefSchema,
|
||||
totals: IssueProformaIssuedInvoiceTotalsSchema,
|
||||
});
|
||||
|
||||
export const IssueProformaByIdResponseSchema = z.object({
|
||||
issuedinvoice_id: z.string(),
|
||||
proforma_id: z.string(),
|
||||
customer_id: z.string(),
|
||||
issued_invoice: IssueProformaIssuedInvoiceSchema,
|
||||
});
|
||||
|
||||
export type IssueProformaByIdResponseDTO = z.infer<typeof IssueProformaByIdResponseSchema>;
|
||||
|
||||
@ -2,7 +2,7 @@ import { z } from "zod/v4";
|
||||
|
||||
export const TaxRegimeRefSchema = z.object({
|
||||
code: z.string(),
|
||||
description: z.string(),
|
||||
description: z.string().nullable(),
|
||||
});
|
||||
|
||||
export type TaxRegimeRefDTO = z.infer<typeof TaxRegimeRefSchema>;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user