Refactorización Issued invoice / Proformas

This commit is contained in:
David Arranz 2026-02-15 23:14:17 +01:00
parent 8d6b53e431
commit 97544b012d
275 changed files with 6988 additions and 1739 deletions

View File

@ -1,4 +1,4 @@
import { Collection, Result } from "@repo/rdx-utils"; import type { Collection, Result } from "@repo/rdx-utils";
/** /**
* Tipo para los parámetros que reciben los métodos de los mappers * Tipo para los parámetros que reciben los métodos de los mappers

View File

@ -3,15 +3,11 @@ import type { IAggregateRootRepository, UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { FindOptions, ModelDefined, Sequelize, Transaction } from "sequelize"; import type { FindOptions, ModelDefined, Sequelize, Transaction } from "sequelize";
import type { IMapperRegistry } from "../mappers";
export abstract class SequelizeRepository<T> implements IAggregateRootRepository<T> { export abstract class SequelizeRepository<T> implements IAggregateRootRepository<T> {
protected readonly _database!: Sequelize; protected readonly database!: Sequelize;
protected readonly _registry!: IMapperRegistry;
constructor(params: { mapperRegistry: IMapperRegistry; database: Sequelize }) { constructor(params: { database: Sequelize }) {
this._registry = params.mapperRegistry; this.database = params.database;
this._database = params.database;
} }
protected convertCriteria(criteria: Criteria): FindOptions { protected convertCriteria(criteria: Criteria): FindOptions {

View File

@ -8,10 +8,10 @@ import { Result } from "@repo/rdx-utils";
import type { CreateCustomerInvoiceRequestDTO } from "../../../common"; import type { CreateCustomerInvoiceRequestDTO } from "../../../common";
import { import {
CustomerInvoiceItem, IssuedInvoiceItem,
CustomerInvoiceItemDescription, type IssuedInvoiceItemProps,
type CustomerInvoiceItemProps,
ItemAmount, ItemAmount,
ItemDescription,
ItemDiscount, ItemDiscount,
ItemQuantity, ItemQuantity,
} from "../../domain"; } from "../../domain";
@ -20,17 +20,15 @@ import { hasNoUndefinedFields } from "./has-no-undefined-fields";
export function mapDTOToCustomerInvoiceItemsProps( export function mapDTOToCustomerInvoiceItemsProps(
dtoItems: Pick<CreateCustomerInvoiceRequestDTO, "items">["items"] dtoItems: Pick<CreateCustomerInvoiceRequestDTO, "items">["items"]
): Result<CustomerInvoiceItem[], ValidationErrorCollection> { ): Result<IssuedInvoiceItem[], ValidationErrorCollection> {
const errors: ValidationErrorDetail[] = []; const errors: ValidationErrorDetail[] = [];
const items: CustomerInvoiceItem[] = []; const items: IssuedInvoiceItem[] = [];
dtoItems.forEach((item, index) => { dtoItems.forEach((item, index) => {
const path = (field: string) => `items[${index}].${field}`; const path = (field: string) => `items[${index}].${field}`;
const description = extractOrPushError( const description = extractOrPushError(
maybeFromNullableVO(item.description, (value) => maybeFromNullableVO(item.description, (value) => ItemDescription.create(value)),
CustomerInvoiceItemDescription.create(value)
),
path("description"), path("description"),
errors errors
); );
@ -54,7 +52,7 @@ export function mapDTOToCustomerInvoiceItemsProps(
); );
if (errors.length === 0) { if (errors.length === 0) {
const itemProps: CustomerInvoiceItemProps = { const itemProps: IssuedInvoiceItemProps = {
description: description, description: description,
quantity: quantity, quantity: quantity,
unitAmount: unitAmount, unitAmount: unitAmount,
@ -66,7 +64,7 @@ export function mapDTOToCustomerInvoiceItemsProps(
if (hasNoUndefinedFields(itemProps)) { if (hasNoUndefinedFields(itemProps)) {
// Validar y crear el item de factura // Validar y crear el item de factura
const itemOrError = CustomerInvoiceItem.create(itemProps); const itemOrError = IssuedInvoiceItem.create(itemProps);
if (itemOrError.isSuccess) { if (itemOrError.isSuccess) {
items.push(itemOrError.data); items.push(itemOrError.data);

View File

@ -10,12 +10,7 @@ import {
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { CreateCustomerInvoiceRequestDTO } from "../../../common"; import type { CreateCustomerInvoiceRequestDTO } from "../../../common";
import { import { type IProformaProps, InvoiceNumber, InvoiceSerie, InvoiceStatus } from "../../domain";
CustomerInvoiceNumber,
type CustomerInvoiceProps,
CustomerInvoiceSerie,
CustomerInvoiceStatus,
} from "../../domain";
import { mapDTOToCustomerInvoiceItemsProps } from "./map-dto-to-customer-invoice-items-props"; import { mapDTOToCustomerInvoiceItemsProps } from "./map-dto-to-customer-invoice-items-props";
@ -35,12 +30,12 @@ export function mapDTOToCustomerInvoiceProps(dto: CreateCustomerInvoiceRequestDT
const invoiceId = extractOrPushError(UniqueID.create(dto.id), "id", errors); const invoiceId = extractOrPushError(UniqueID.create(dto.id), "id", errors);
const invoiceNumber = extractOrPushError( const invoiceNumber = extractOrPushError(
maybeFromNullableVO(dto.invoice_number, (value) => CustomerInvoiceNumber.create(value)), maybeFromNullableVO(dto.invoice_number, (value) => InvoiceNumber.create(value)),
"invoice_number", "invoice_number",
errors errors
); );
const invoiceSeries = extractOrPushError( const invoiceSeries = extractOrPushError(
maybeFromNullableVO(dto.series, (value) => CustomerInvoiceSerie.create(value)), maybeFromNullableVO(dto.series, (value) => InvoiceSerie.create(value)),
"invoice_series", "invoice_series",
errors errors
); );
@ -71,12 +66,12 @@ export function mapDTOToCustomerInvoiceProps(dto: CreateCustomerInvoiceRequestDT
return Result.fail(new ValidationErrorCollection("Invoice dto mapping failed", errors)); return Result.fail(new ValidationErrorCollection("Invoice dto mapping failed", errors));
} }
const invoiceProps: CustomerInvoiceProps = { const invoiceProps: IProformaProps = {
invoiceNumber: invoiceNumber!, invoiceNumber: invoiceNumber!,
series: invoiceSeries!, series: invoiceSeries!,
invoiceDate: invoiceDate!, invoiceDate: invoiceDate!,
operationDate: operationDate!, operationDate: operationDate!,
status: CustomerInvoiceStatus.createDraft(), status: InvoiceStatus.createDraft(),
currencyCode: currencyCode!, currencyCode: currencyCode!,
}; };

View File

@ -1,5 +1,2 @@
//export * from "./services";
//export * from "./snapshot-builders";
export * from "./issued-invoices"; export * from "./issued-invoices";
export * from "./use-cases"; export * from "./proformas";

View File

@ -1,3 +0,0 @@
export * from "./issued-invoice-document.model";
export * from "./report-cache-key";
export * from "./snapshots";

View File

@ -1,33 +0,0 @@
/**
* Documento legal generado para una factura emitida.
*
*/
export interface IIssuedInvoiceDocument {
payload: Buffer;
mimeType: string;
filename: string;
}
export class IssuedInvoiceDocument implements IIssuedInvoiceDocument {
public readonly payload: Buffer;
public readonly mimeType: string;
public readonly filename: string;
constructor(params: {
payload: Buffer;
filename: string;
}) {
if (!params.payload || params.payload.length === 0) {
throw new Error("IssuedInvoiceDocument payload cannot be empty");
}
if (!params.filename.toLowerCase().endsWith(".pdf")) {
throw new Error("IssuedInvoiceDocument filename must end with .pdf");
}
this.payload = params.payload;
this.mimeType = "application/pdf";
this.filename = params.filename;
}
}

View File

@ -1,59 +0,0 @@
import type { UniqueID } from "@repo/rdx-ddd";
/**
* Clave determinista que identifica de forma única
* un documento legal generado.
*
* Encapsula la regla de idempotencia del caso de uso.
*/
export class ReportCacheKey {
private readonly value: string;
private constructor(value: string) {
this.value = value;
}
static forIssuedInvoice(params: {
companyId: UniqueID;
invoiceId: UniqueID;
language: string;
canonicalModelHash: string;
templateChecksum: string;
rendererVersion: string;
signingProviderVersion: string;
}): ReportCacheKey {
const {
companyId,
invoiceId,
language,
canonicalModelHash,
templateChecksum,
rendererVersion,
signingProviderVersion,
} = params;
// Fail-fast: campos obligatorios
for (const [key, value] of Object.entries(params)) {
if (!value || String(value).trim() === "") {
throw new Error(`ReportCacheKey missing field: ${key}`);
}
}
return new ReportCacheKey(
[
"issued-invoice",
companyId,
invoiceId,
language,
canonicalModelHash,
templateChecksum,
rendererVersion,
signingProviderVersion,
].join("__")
);
}
toString(): string {
return this.value;
}
}

View File

@ -1,4 +0,0 @@
export * from "./issued-invoice-full-snapshot";
export * from "./issued-invoice-item-full-snapshot";
export * from "./issued-invoice-recipient-full-snapshot";
export * from "./issued-invoice-verifactu-full-snapshot";

View File

@ -1 +0,0 @@
export * from "./issued-invoice-list-item-snapshot";

View File

@ -1,3 +0,0 @@
export * from "./issued-invoice-report-item-snapshot";
export * from "./issued-invoice-report-snapshot";
export * from "./issued-invoice-report-tax-snapshot";

View File

@ -1,3 +1,3 @@
export * from "./finder.di"; export * from "./issued-invoice-finder.di";
export * from "./snapshot-builders.di"; export * from "./issued-invoice-snapshot-builders.di";
export * from "./use-cases.di"; export * from "./issued-invoice-use-cases.di";

View File

@ -1 +1 @@
export * from "./sign-document-command"; export * from "./issued-invoice-list.dto";

View File

@ -0,0 +1,43 @@
import type { CurrencyCode, LanguageCode, Percentage, UniqueID, UtcDate } from "@repo/rdx-ddd";
import type { Maybe } from "@repo/rdx-utils";
import type {
InvoiceAmount,
InvoiceNumber,
InvoiceRecipient,
InvoiceSerie,
InvoiceStatus,
VerifactuRecord,
} from "../../../domain";
export type IssuedInvoiceListDTO = {
id: UniqueID;
companyId: UniqueID;
isProforma: boolean;
invoiceNumber: InvoiceNumber;
status: InvoiceStatus;
series: Maybe<InvoiceSerie>;
invoiceDate: UtcDate;
operationDate: Maybe<UtcDate>;
reference: Maybe<string>;
description: Maybe<string>;
customerId: UniqueID;
recipient: InvoiceRecipient;
languageCode: LanguageCode;
currencyCode: CurrencyCode;
discountPercentage: Percentage;
subtotalAmount: InvoiceAmount;
discountAmount: InvoiceAmount;
taxableAmount: InvoiceAmount;
taxesAmount: InvoiceAmount;
totalAmount: InvoiceAmount;
verifactu: Maybe<VerifactuRecord>;
};

View File

@ -1,15 +0,0 @@
/**
* DTO de Application para solicitar la firma de un documento.
*
* Se utiliza exclusivamente para intercambiar datos con la capa
* de infraestructura (DocumentSigningService).
*
* No contiene lógica ni validaciones de negocio.
*/
export interface SignDocumentCommand {
/** PDF sin firmar */
readonly file: Buffer;
/** Identificador estable de la empresa */
readonly companyId: string;
}

View File

@ -1,5 +1,8 @@
export * from "./application-models"; export * from "./application-models";
export * from "./di"; export * from "./di";
export * from "./dtos";
export * from "./mappers";
export * from "./repositories";
export * from "./services"; export * from "./services";
export * from "./snapshot-builders"; export * from "./snapshot-builders";
export * from "./use-cases"; export * from "./use-cases";

View File

@ -0,0 +1,2 @@
export * from "./issued-invoice-domain-mapper.interface";
export * from "./issued-invoice-list-mapper.interface";

View File

@ -0,0 +1,9 @@
import type { MapperParamsType } from "@erp/core/api";
import type { Result } from "@repo/rdx-utils";
import type { IssuedInvoice } from "../../../domain";
export interface IIssuedInvoiceDomainMapper {
mapToPersistence(invoice: IssuedInvoice, params?: MapperParamsType): Result<unknown, Error>;
mapToDomain(raw: unknown, params?: MapperParamsType): Result<IssuedInvoice, Error>;
}

View File

@ -0,0 +1,8 @@
import type { MapperParamsType } from "@erp/core/api";
import type { Result } from "@repo/rdx-utils";
import type { IssuedInvoiceListDTO } from "../dtos";
export interface IIssuedInvoiceListMapper {
mapToDTO(raw: unknown, params?: MapperParamsType): Result<IssuedInvoiceListDTO, Error>;
}

View File

@ -0,0 +1 @@
export * from "./issued-invoice-repository.interface";

View File

@ -0,0 +1,22 @@
import type { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
import type { Collection, Result } from "@repo/rdx-utils";
import type { IssuedInvoice } from "../../../domain";
import type { IssuedInvoiceListDTO } from "../dtos";
export interface IIssuedInvoiceRepository {
create(invoice: IssuedInvoice, transaction?: unknown): Promise<Result<void, Error>>;
getByIdInCompany(
companyId: UniqueID,
id: UniqueID,
transaction: unknown
): Promise<Result<IssuedInvoice, Error>>;
findByCriteriaInCompany(
companyId: UniqueID,
criteria: Criteria,
transaction: unknown
): Promise<Result<Collection<IssuedInvoiceListDTO>, Error>>;
}

View File

@ -1,5 +1,5 @@
export * from "./issued-invoice-document-generator.interface"; export * from "./issued-invoice-document-generator.interface";
export * from "./issued-invoice-document-metadata-factory"; export * from "./issued-invoice-document-metadata-factory";
export * from "./issued-invoice-document-properties-factory"; export * from "./issued-invoice-document-properties-factory";
export * from "./issued-invoice-document-renderer.interface";
export * from "./issued-invoice-finder"; export * from "./issued-invoice-finder";
export * from "./proforma-to-issued-invoice-materializer";

View File

@ -1,10 +0,0 @@
import type { Result } from "@repo/rdx-utils";
import type { IIssuedInvoiceDocument, IssuedInvoiceReportSnapshot } from "../application-models";
export interface IIssuedInvoiceDocumentRenderer {
render(input: {
snapshot: IssuedInvoiceReportSnapshot;
documentId: string;
}): Promise<Result<IIssuedInvoiceDocument, Error>>;
}

View File

@ -4,14 +4,14 @@ import type { UniqueID } from "@repo/rdx-ddd";
import type { Collection, Result } from "@repo/rdx-utils"; import type { Collection, Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize"; import type { Transaction } from "sequelize";
import type { CustomerInvoice, ICustomerInvoiceRepository } from "../../../domain"; import type { ICustomerInvoiceRepository, Proforma } from "../../../domain";
export interface IIssuedInvoiceFinder { export interface IIssuedInvoiceFinder {
findIssuedInvoiceById( findIssuedInvoiceById(
companyId: UniqueID, companyId: UniqueID,
invoiceId: UniqueID, invoiceId: UniqueID,
transaction?: Transaction transaction?: Transaction
): Promise<Result<CustomerInvoice, Error>>; ): Promise<Result<Proforma, Error>>;
issuedInvoiceExists( issuedInvoiceExists(
companyId: UniqueID, companyId: UniqueID,
@ -33,18 +33,10 @@ export class IssuedInvoiceFinder implements IIssuedInvoiceFinder {
companyId: UniqueID, companyId: UniqueID,
invoiceId: UniqueID, invoiceId: UniqueID,
transaction?: Transaction transaction?: Transaction
): Promise<Result<CustomerInvoice, Error>> { ): Promise<Result<Proforma, Error>> {
return this.repository.getIssuedInvoiceByIdInCompany(companyId, invoiceId, transaction, {}); return this.repository.getIssuedInvoiceByIdInCompany(companyId, invoiceId, transaction, {});
} }
async findProformaById(
companyId: UniqueID,
proformaId: UniqueID,
transaction?: Transaction
): Promise<Result<CustomerInvoice, Error>> {
return this.repository.getProformaByIdInCompany(companyId, proformaId, transaction, {});
}
async issuedInvoiceExists( async issuedInvoiceExists(
companyId: UniqueID, companyId: UniqueID,
invoiceId: UniqueID, invoiceId: UniqueID,
@ -55,16 +47,6 @@ export class IssuedInvoiceFinder implements IIssuedInvoiceFinder {
}); });
} }
async proformaExists(
companyId: UniqueID,
proformaId: UniqueID,
transaction?: Transaction
): Promise<Result<boolean, Error>> {
return this.repository.existsByIdInCompany(companyId, proformaId, transaction, {
is_proforma: true,
});
}
async findIssuedInvoicesByCriteria( async findIssuedInvoicesByCriteria(
companyId: UniqueID, companyId: UniqueID,
criteria: Criteria, criteria: Criteria,
@ -77,12 +59,4 @@ export class IssuedInvoiceFinder implements IIssuedInvoiceFinder {
{} {}
); );
} }
async findProformasByCriteria(
companyId: UniqueID,
criteria: Criteria,
transaction?: Transaction
): Promise<Result<Collection<CustomerInvoiceListDTO>, Error>> {
return this.repository.findProformasByCriteriaInCompany(companyId, criteria, transaction, {});
}
} }

View File

@ -1,11 +1,7 @@
import type { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import type { Maybe, Result } from "@repo/rdx-utils"; import type { Maybe, Result } from "@repo/rdx-utils";
import type { import type { ICustomerInvoiceNumberGenerator, InvoiceNumber, InvoiceSerie } from "../../../domain";
CustomerInvoiceNumber,
CustomerInvoiceSerie,
ICustomerInvoiceNumberGenerator,
} from "../../../domain";
export interface IIssuedInvoiceNumberService { export interface IIssuedInvoiceNumberService {
/** /**
@ -13,9 +9,9 @@ export interface IIssuedInvoiceNumberService {
*/ */
nextIssuedInvoiceNumber( nextIssuedInvoiceNumber(
companyId: UniqueID, companyId: UniqueID,
series: Maybe<CustomerInvoiceSerie>, series: Maybe<InvoiceSerie>,
transaction: unknown transaction: unknown
): Promise<Result<CustomerInvoiceNumber, Error>>; ): Promise<Result<InvoiceNumber, Error>>;
} }
export class IssuedInvoiceNumberService implements IIssuedInvoiceNumberService { export class IssuedInvoiceNumberService implements IIssuedInvoiceNumberService {
@ -23,9 +19,9 @@ export class IssuedInvoiceNumberService implements IIssuedInvoiceNumberService {
async nextIssuedInvoiceNumber( async nextIssuedInvoiceNumber(
companyId: UniqueID, companyId: UniqueID,
series: Maybe<CustomerInvoiceSerie>, series: Maybe<InvoiceSerie>,
transaction: unknown transaction: unknown
): Promise<Result<CustomerInvoiceNumber, Error>> { ): Promise<Result<InvoiceNumber, Error>> {
return this.numberGenerator.nextForCompany(companyId, series, transaction); return this.numberGenerator.nextForCompany(companyId, series, transaction);
} }
} }

View File

@ -0,0 +1,56 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { Collection, Result } from "@repo/rdx-utils";
import type { IssuedInvoiceProps, Proforma } from "../../../domain";
export interface IProformaToIssuedInvoiceMaterializer {
materialize(proforma: Proforma, issuedInvoiceId: UniqueID): Result<IssuedInvoiceProps, Error>;
}
export class ProformaToIssuedInvoiceMaterializer implements IProformaToIssuedInvoiceMaterializer {
public materialize(
proforma: Proforma,
issuedInvoiceId: UniqueID
): Result<IssuedInvoiceProps, Error> {
const amounts = proforma.calculateAllAmounts();
const taxGroups = proforma.getTaxes();
const issuedItems = proforma.items.map((item) => ({
description: item.description,
quantity: item.quantity,
unitPrice: item.unitAmount,
taxableAmount: item.getTaxableAmount(),
taxesAmount: item.getTaxesAmount(),
totalAmount: item.getTotalAmount(),
}));
const issuedTaxes = taxGroups.map((group) => ({
ivaCode: group.iva.code,
ivaPercentage: group.iva.percentage,
ivaAmount: group.calculateAmounts().ivaAmount,
recCode: group.rec?.code,
recPercentage: group.rec?.percentage,
recAmount: group.calculateAmounts().recAmount,
retentionCode: group.retention?.code,
retentionPercentage: group.retention?.percentage,
retentionAmount: group.calculateAmounts().retentionAmount,
}));
return Result.ok({
companyId: proforma.companyId,
invoiceNumber: proforma.invoiceNumber,
invoiceDate: proforma.invoiceDate,
customerId: proforma.customerId,
languageCode: proforma.languageCode,
currencyCode: proforma.currencyCode,
paymentMethod: proforma.paymentMethod,
discountPercentage: proforma.discountPercentage,
items: new Collection(issuedItems),
taxes: new Collection(issuedTaxes),
subtotalAmount: amounts.subtotalAmount,
taxableAmount: amounts.taxableAmount,
totalAmount: amounts.totalAmount,
});
}
}

View File

@ -1,4 +1,8 @@
export * from "./issued-invoice-full-snapshot.interface";
export * from "./issued-invoice-full-snapshot-builder"; export * from "./issued-invoice-full-snapshot-builder";
export * from "./issued-invoice-item-full-snapshot.interface";
export * from "./issued-invoice-items-full-snapshot-builder"; export * from "./issued-invoice-items-full-snapshot-builder";
export * from "./issued-invoice-recipient-full-snapshot.interfce";
export * from "./issued-invoice-recipient-full-snapshot-builder"; export * from "./issued-invoice-recipient-full-snapshot-builder";
export * from "./issued-invoice-verifactu-full-snapshot.interface";
export * from "./issued-invoice-verifactu-full-snapshot-builder"; export * from "./issued-invoice-verifactu-full-snapshot-builder";

View File

@ -1,15 +1,15 @@
import type { ISnapshotBuilder } from "@erp/core/api"; import type { ISnapshotBuilder } from "@erp/core/api";
import { toEmptyString } from "@repo/rdx-ddd"; import { toEmptyString } from "@repo/rdx-ddd";
import { type CustomerInvoice, InvoiceAmount } from "../../../../domain"; import { InvoiceAmount, type Proforma } from "../../../../domain";
import type { IssuedInvoiceFullSnapshot } from "../../application-models";
import type { IssuedInvoiceFullSnapshot } from "./issued-invoice-full-snapshot.interface";
import type { IIssuedInvoiceItemsFullSnapshotBuilder } from "./issued-invoice-items-full-snapshot-builder"; import type { IIssuedInvoiceItemsFullSnapshotBuilder } from "./issued-invoice-items-full-snapshot-builder";
import type { IIssuedInvoiceRecipientFullSnapshotBuilder } from "./issued-invoice-recipient-full-snapshot-builder"; import type { IIssuedInvoiceRecipientFullSnapshotBuilder } from "./issued-invoice-recipient-full-snapshot-builder";
import type { IIssuedInvoiceVerifactuFullSnapshotBuilder } from "./issued-invoice-verifactu-full-snapshot-builder"; import type { IIssuedInvoiceVerifactuFullSnapshotBuilder } from "./issued-invoice-verifactu-full-snapshot-builder";
export interface IIssuedInvoiceFullSnapshotBuilder export interface IIssuedInvoiceFullSnapshotBuilder
extends ISnapshotBuilder<CustomerInvoice, IssuedInvoiceFullSnapshot> {} extends ISnapshotBuilder<Proforma, IssuedInvoiceFullSnapshot> {}
export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnapshotBuilder { export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnapshotBuilder {
constructor( constructor(
@ -18,7 +18,7 @@ export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnaps
private readonly verifactuBuilder: IIssuedInvoiceVerifactuFullSnapshotBuilder private readonly verifactuBuilder: IIssuedInvoiceVerifactuFullSnapshotBuilder
) {} ) {}
toOutput(invoice: CustomerInvoice): IssuedInvoiceFullSnapshot { toOutput(invoice: Proforma): IssuedInvoiceFullSnapshot {
const items = this.itemsBuilder.toOutput(invoice.items); const items = this.itemsBuilder.toOutput(invoice.items);
const recipient = this.recipientBuilder.toOutput(invoice); const recipient = this.recipientBuilder.toOutput(invoice);
const verifactu = this.verifactuBuilder.toOutput(invoice); const verifactu = this.verifactuBuilder.toOutput(invoice);
@ -129,7 +129,6 @@ export class IssuedInvoiceFullSnapshotBuilder implements IIssuedInvoiceFullSnaps
metadata: { metadata: {
entity: "issued-invoices", entity: "issued-invoices",
link: "",
}, },
}; };
} }

View File

@ -1,6 +1,6 @@
import type { IssuedInvoiceItemFullSnapshot } from "./issued-invoice-item-full-snapshot"; import type { IssuedInvoiceItemFullSnapshot } from "./issued-invoice-item-full-snapshot.interface";
import type { IssuedInvoiceRecipientFullSnapshot } from "./issued-invoice-recipient-full-snapshot"; import type { IssuedInvoiceRecipientFullSnapshot } from "./issued-invoice-recipient-full-snapshot.interfce";
import type { IssuedInvoiceVerifactuFullSnapshot } from "./issued-invoice-verifactu-full-snapshot"; import type { IssuedInvoiceVerifactuFullSnapshot } from "./issued-invoice-verifactu-full-snapshot.interface";
export interface IssuedInvoiceFullSnapshot { export interface IssuedInvoiceFullSnapshot {
id: string; id: string;

View File

@ -1,7 +1,7 @@
import type { ISnapshotBuilder } from "@erp/core/api"; import type { ISnapshotBuilder } from "@erp/core/api";
import { toEmptyString } from "@repo/rdx-ddd"; import { toEmptyString } from "@repo/rdx-ddd";
import type { CustomerInvoiceItem, CustomerInvoiceItems } from "../../../../domain"; import type { CustomerInvoiceItems, IssuedInvoiceItem } from "../../../../domain";
import type { IssuedInvoiceItemFullSnapshot } from "../../application-models"; import type { IssuedInvoiceItemFullSnapshot } from "../../application-models";
export interface IIssuedInvoiceItemsFullSnapshotBuilder export interface IIssuedInvoiceItemsFullSnapshotBuilder
@ -10,7 +10,7 @@ export interface IIssuedInvoiceItemsFullSnapshotBuilder
export class IssuedInvoiceItemsFullSnapshotBuilder export class IssuedInvoiceItemsFullSnapshotBuilder
implements IIssuedInvoiceItemsFullSnapshotBuilder implements IIssuedInvoiceItemsFullSnapshotBuilder
{ {
private mapItem(invoiceItem: CustomerInvoiceItem, index: number): IssuedInvoiceItemFullSnapshot { private mapItem(invoiceItem: IssuedInvoiceItem, index: number): IssuedInvoiceItemFullSnapshot {
const allAmounts = invoiceItem.calculateAllAmounts(); const allAmounts = invoiceItem.calculateAllAmounts();
return { return {

View File

@ -1,16 +1,16 @@
import type { ISnapshotBuilder } from "@erp/core/api"; import type { ISnapshotBuilder } from "@erp/core/api";
import { DomainValidationError, toEmptyString } from "@repo/rdx-ddd"; import { DomainValidationError, toEmptyString } from "@repo/rdx-ddd";
import type { CustomerInvoice, InvoiceRecipient } from "../../../../domain"; import type { InvoiceRecipient, Proforma } from "../../../../domain";
import type { IssuedInvoiceRecipientFullSnapshot } from "../../application-models"; import type { IssuedInvoiceRecipientFullSnapshot } from "../../application-models";
export interface IIssuedInvoiceRecipientFullSnapshotBuilder export interface IIssuedInvoiceRecipientFullSnapshotBuilder
extends ISnapshotBuilder<CustomerInvoice, IssuedInvoiceRecipientFullSnapshot> {} extends ISnapshotBuilder<Proforma, IssuedInvoiceRecipientFullSnapshot> {}
export class IssuedInvoiceRecipientFullSnapshotBuilder export class IssuedInvoiceRecipientFullSnapshotBuilder
implements IIssuedInvoiceRecipientFullSnapshotBuilder implements IIssuedInvoiceRecipientFullSnapshotBuilder
{ {
toOutput(invoice: CustomerInvoice): IssuedInvoiceRecipientFullSnapshot { toOutput(invoice: Proforma): IssuedInvoiceRecipientFullSnapshot {
if (!invoice.recipient) { if (!invoice.recipient) {
throw DomainValidationError.requiredValue("recipient", { throw DomainValidationError.requiredValue("recipient", {
cause: invoice, cause: invoice,

View File

@ -1,16 +1,16 @@
import type { ISnapshotBuilder } from "@erp/core/api"; import type { ISnapshotBuilder } from "@erp/core/api";
import { DomainValidationError } from "@repo/rdx-ddd"; import { DomainValidationError } from "@repo/rdx-ddd";
import type { CustomerInvoice } from "../../../../domain"; import type { Proforma } from "../../../../domain";
import type { IssuedInvoiceVerifactuFullSnapshot } from "../../application-models"; import type { IssuedInvoiceVerifactuFullSnapshot } from "../../application-models";
export interface IIssuedInvoiceVerifactuFullSnapshotBuilder export interface IIssuedInvoiceVerifactuFullSnapshotBuilder
extends ISnapshotBuilder<CustomerInvoice, IssuedInvoiceVerifactuFullSnapshot> {} extends ISnapshotBuilder<Proforma, IssuedInvoiceVerifactuFullSnapshot> {}
export class IssuedInvoiceVerifactuFullSnapshotBuilder export class IssuedInvoiceVerifactuFullSnapshotBuilder
implements IIssuedInvoiceVerifactuFullSnapshotBuilder implements IIssuedInvoiceVerifactuFullSnapshotBuilder
{ {
toOutput(invoice: CustomerInvoice): IssuedInvoiceVerifactuFullSnapshot { toOutput(invoice: Proforma): IssuedInvoiceVerifactuFullSnapshot {
if (!invoice.verifactu) { if (!invoice.verifactu) {
throw DomainValidationError.requiredValue("verifactu", { throw DomainValidationError.requiredValue("verifactu", {
cause: invoice, cause: invoice,

View File

@ -1 +1,2 @@
export * from "./issued-invoice-list-item-snapshot.interface";
export * from "./issued-invoice-list-item-snapshot-builder"; export * from "./issued-invoice-list-item-snapshot-builder";

View File

@ -2,7 +2,7 @@ import type { ISnapshotBuilder } from "@erp/core/api";
import { toEmptyString } from "@repo/rdx-ddd"; import { toEmptyString } from "@repo/rdx-ddd";
import type { CustomerInvoiceListDTO } from "../../../../infrastructure"; import type { CustomerInvoiceListDTO } from "../../../../infrastructure";
import type { IssuedInvoiceListItemSnapshot } from "../../application-models/snapshots/list"; import type { IssuedInvoiceListItemSnapshot } from "../../application-models";
export interface IIssuedInvoiceListItemSnapshotBuilder export interface IIssuedInvoiceListItemSnapshotBuilder
extends ISnapshotBuilder<CustomerInvoiceListDTO, IssuedInvoiceListItemSnapshot> {} extends ISnapshotBuilder<CustomerInvoiceListDTO, IssuedInvoiceListItemSnapshot> {}

View File

@ -1,3 +1,6 @@
export * from "./issued-invoice-items-report-snapshot-builder"; export * from "./issued-invoice-items-report-snapshot-builder";
export * from "./issued-invoice-report-item-snapshot.interface";
export * from "./issued-invoice-report-snapshot.interface";
export * from "./issued-invoice-report-snapshot-builder"; export * from "./issued-invoice-report-snapshot-builder";
export * from "./issued-invoice-report-tax-snapshot.interface";
export * from "./issued-invoice-tax-report-snapshot-builder"; export * from "./issued-invoice-tax-report-snapshot-builder";

View File

@ -1,5 +1,5 @@
import type { IssuedInvoiceReportItemSnapshot } from "./issued-invoice-report-item-snapshot"; import type { IssuedInvoiceReportItemSnapshot } from "./issued-invoice-report-item-snapshot.interface";
import type { IssuedInvoiceReportTaxSnapshot } from "./issued-invoice-report-tax-snapshot"; import type { IssuedInvoiceReportTaxSnapshot } from "./issued-invoice-report-tax-snapshot.interface";
export interface IssuedInvoiceReportSnapshot { export interface IssuedInvoiceReportSnapshot {
id: string; id: string;

View File

@ -1,3 +1,3 @@
export * from "./get-issued-invoice-by-id.use-case"; export * from "./get-issued-invoice-by-id.use-case";
export * from "./list-issued-invoices.use-case"; export * from "./list-issued-invoices.use-case";
export * from "./report-issued-invoices"; export * from "./report-issued-invoice.use-case";

View File

@ -15,7 +15,7 @@ type ListIssuedInvoicesUseCaseInput = {
export class ListIssuedInvoicesUseCase { export class ListIssuedInvoicesUseCase {
constructor( constructor(
private readonly finder: IIssuedInvoiceFinder, private readonly finder: IIssuedInvoiceFinder,
private readonly itemSnapshotBuilder: IIssuedInvoiceListItemSnapshotBuilder, private readonly listItemSnapshotBuilder: IIssuedInvoiceListItemSnapshotBuilder,
private readonly transactionManager: ITransactionManager private readonly transactionManager: ITransactionManager
) {} ) {}
@ -37,9 +37,8 @@ export class ListIssuedInvoicesUseCase {
const invoices = result.data; const invoices = result.data;
const totalInvoices = invoices.total(); const totalInvoices = invoices.total();
const items = invoices.map((item) => this.itemSnapshotBuilder.toOutput(item)); const items = invoices.map((item) => this.listItemSnapshotBuilder.toOutput(item));
// ?????
const snapshot = { const snapshot = {
page: criteria.pageNumber, page: criteria.pageNumber,
per_page: criteria.pageSize, per_page: criteria.pageSize,

View File

@ -2,9 +2,9 @@ import type { ITransactionManager, RendererFormat } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { IIssuedInvoiceFinder, IssuedInvoiceDocumentGeneratorService } from "../../services"; import type { IIssuedInvoiceFinder, IssuedInvoiceDocumentGeneratorService } from "../services";
import type { IIssuedInvoiceFullSnapshotBuilder } from "../../snapshot-builders"; import type { IIssuedInvoiceFullSnapshotBuilder } from "../snapshot-builders";
import type { IIssuedInvoiceReportSnapshotBuilder } from "../../snapshot-builders/report"; import type { IIssuedInvoiceReportSnapshotBuilder } from "../snapshot-builders/report";
type ReportIssuedInvoiceUseCaseInput = { type ReportIssuedInvoiceUseCaseInput = {
companyId: UniqueID; companyId: UniqueID;
@ -23,7 +23,7 @@ export class ReportIssuedInvoiceUseCase {
) {} ) {}
public async execute(params: ReportIssuedInvoiceUseCaseInput) { public async execute(params: ReportIssuedInvoiceUseCaseInput) {
const { invoice_id, companyId, companySlug, format } = params; const { invoice_id, companyId } = params;
const idOrError = UniqueID.create(invoice_id); const idOrError = UniqueID.create(invoice_id);

View File

@ -1,2 +0,0 @@
//export * from "./issued-invoice-document";
export * from "./report-issued-invoice.use-case";

View File

@ -0,0 +1,4 @@
export * from "./proforma-creator.di";
export * from "./proforma-finder.di";
export * from "./proforma-snapshot-builders.di";
export * from "./proforma-use-cases.di";

View File

@ -0,0 +1,14 @@
import type { ICustomerInvoiceRepository } from "../../../domain/repositories";
import { ProformaFactory } from "../factories";
import { type IProformaCreator, type IProformaNumberGenerator, ProformaCreator } from "../services";
export function buildProformaCreator(
numberService: IProformaNumberGenerator,
repository: ICustomerInvoiceRepository
): IProformaCreator {
return new ProformaCreator({
numberService,
factory: new ProformaFactory(),
repository,
});
}

View File

@ -0,0 +1,6 @@
import type { ICustomerInvoiceRepository } from "../../../domain";
import { type IProformaFinder, ProformaFinder } from "../services";
export function buildProformaFinder(repository: ICustomerInvoiceRepository): IProformaFinder {
return new ProformaFinder(repository);
}

View File

@ -0,0 +1,34 @@
// application/issued-invoices/di/snapshot-builders.di.ts
import {
ProformaFullSnapshotBuilder,
ProformaItemReportSnapshotBuilder,
ProformaItemsFullSnapshotBuilder,
ProformaListItemSnapshotBuilder,
ProformaRecipientFullSnapshotBuilder,
ProformaReportSnapshotBuilder,
ProformaTaxReportSnapshotBuilder,
} from "../snapshot-builders";
export function buildProformaSnapshotBuilders() {
const itemsBuilder = new ProformaItemsFullSnapshotBuilder();
const recipientBuilder = new ProformaRecipientFullSnapshotBuilder();
const fullSnapshotBuilder = new ProformaFullSnapshotBuilder(itemsBuilder, recipientBuilder);
const listSnapshotBuilder = new ProformaListItemSnapshotBuilder();
const itemsReportBuilder = new ProformaItemReportSnapshotBuilder();
const taxesReportBuilder = new ProformaTaxReportSnapshotBuilder();
const reportSnapshotBuilder = new ProformaReportSnapshotBuilder(
itemsReportBuilder,
taxesReportBuilder
);
return {
full: fullSnapshotBuilder,
list: listSnapshotBuilder,
report: reportSnapshotBuilder,
};
}

View File

@ -0,0 +1,76 @@
import type { ITransactionManager } from "@erp/core/api";
import type { IProformaFinder, ProformaDocumentGeneratorService } from "../services";
import type {
IProformaListItemSnapshotBuilder,
IProformaReportSnapshotBuilder,
} from "../snapshot-builders";
import type { IProformaFullSnapshotBuilder } from "../snapshot-builders/full";
import { GetProformaByIdUseCase, ListProformasUseCase, ReportProformaUseCase } from "../use-cases";
export function buildGetProformaByIdUseCase(deps: {
finder: IProformaFinder;
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
transactionManager: ITransactionManager;
}) {
return new GetProformaByIdUseCase(deps.finder, deps.fullSnapshotBuilder, deps.transactionManager);
}
export function buildListProformasUseCase(deps: {
finder: IProformaFinder;
itemSnapshotBuilder: IProformaListItemSnapshotBuilder;
transactionManager: ITransactionManager;
}) {
return new ListProformasUseCase(deps.finder, deps.itemSnapshotBuilder, deps.transactionManager);
}
export function buildReportProformaUseCase(deps: {
finder: IProformaFinder;
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
reportSnapshotBuilder: IProformaReportSnapshotBuilder;
documentService: ProformaDocumentGeneratorService;
transactionManager: ITransactionManager;
}) {
return new ReportProformaUseCase(
deps.finder,
deps.fullSnapshotBuilder,
deps.reportSnapshotBuilder,
deps.documentService,
deps.transactionManager
);
}
/*export function buildCreateProformaUseCase(deps: {
creator: IProformaCreator;
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
transactionManager: ITransactionManager;
}) {
return new CreateProformaUseCase({
mapper: new CreateProformaPropsMapper(),
creator: deps.creator,
fullSnapshotBuilder: deps.fullSnapshotBuilder,
transactionManager: deps.transactionManager,
});
}*/
/*export function buildUpdateProformaUseCase(deps: {
finder: IProformaFinder;
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
}) {
return new UpdateProformaUseCase(deps.finder, deps.fullSnapshotBuilder);
}
export function buildDeleteProformaUseCase(deps: { finder: IProformaFinder }) {
return new DeleteProformaUseCase(deps.finder);
}
export function buildIssueProformaUseCase(deps: { finder: IProformaFinder }) {
return new IssueProformaUseCase(deps.finder);
}
export function buildChangeStatusProformaUseCase(deps: {
finder: IProformaFinder;
transactionManager: ITransactionManager;
}) {
return new ChangeStatusProformaUseCase(deps.finder, deps.transactionManager);
}*/

View File

@ -0,0 +1 @@
export * from "./proforma-list.dto";

View File

@ -0,0 +1,40 @@
import type { CurrencyCode, LanguageCode, Percentage, UniqueID, UtcDate } from "@repo/rdx-ddd";
import type { Maybe } from "@repo/rdx-utils";
import type {
InvoiceAmount,
InvoiceNumber,
InvoiceRecipient,
InvoiceSerie,
InvoiceStatus,
} from "../../../domain";
export type ProformaListDTO = {
id: UniqueID;
companyId: UniqueID;
isProforma: boolean;
invoiceNumber: InvoiceNumber;
status: InvoiceStatus;
series: Maybe<InvoiceSerie>;
invoiceDate: UtcDate;
operationDate: Maybe<UtcDate>;
reference: Maybe<string>;
description: Maybe<string>;
customerId: UniqueID;
recipient: InvoiceRecipient;
languageCode: LanguageCode;
currencyCode: CurrencyCode;
discountPercentage: Percentage;
subtotalAmount: InvoiceAmount;
discountAmount: InvoiceAmount;
taxableAmount: InvoiceAmount;
taxesAmount: InvoiceAmount;
totalAmount: InvoiceAmount;
};

View File

@ -0,0 +1,2 @@
export * from "./proforma-factory";
export * from "./proforma-factory.interface";

View File

@ -0,0 +1,17 @@
import type { UniqueID } from "@repo/rdx-ddd";
import type { Result } from "@repo/rdx-utils";
import type { IProformaProps, Proforma } from "../../../domain";
export interface IProformaFactory {
/**
* Crea una proforma válida para una empresa a partir de props ya validadas.
*
* No persiste el agregado.
*/
createProforma(
companyId: UniqueID,
props: Omit<IProformaProps, "companyId">,
proformaId?: UniqueID
): Result<Proforma, Error>;
}

View File

@ -0,0 +1,16 @@
import type { UniqueID } from "@repo/rdx-ddd";
import type { Result } from "@repo/rdx-utils";
import { type IProformaProps, Proforma } from "../../../domain";
import type { IProformaFactory } from "./proforma-factory.interface";
export class ProformaFactory implements IProformaFactory {
createProforma(
companyId: UniqueID,
props: Omit<IProformaProps, "companyId">,
proformaId?: UniqueID
): Result<Proforma, Error> {
return Proforma.create({ ...props, companyId }, proformaId);
}
}

View File

@ -0,0 +1,8 @@
export * from "./application-models";
export * from "./di";
export * from "./dtos";
export * from "./mappers";
export * from "./repositories";
export * from "./services";
export * from "./snapshot-builders";
export * from "./use-cases";

View File

@ -15,24 +15,25 @@ import {
} from "@repo/rdx-ddd"; } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import type { CreateProformaItemRequestDTO, CreateProformaRequestDTO } from "../../../../../common"; import type { CreateProformaItemRequestDTO, CreateProformaRequestDTO } from "../../../../common";
import { import {
CustomerInvoiceItem,
CustomerInvoiceItemDescription,
type CustomerInvoiceItemProps,
CustomerInvoiceItems, CustomerInvoiceItems,
CustomerInvoiceNumber, type IProformaProps,
type CustomerInvoiceProps, InvoiceNumber,
CustomerInvoiceSerie,
CustomerInvoiceStatus,
InvoicePaymentMethod, InvoicePaymentMethod,
type InvoiceRecipient, type InvoiceRecipient,
InvoiceSerie,
InvoiceStatus,
IssuedInvoiceItem,
type IssuedInvoiceItemProps,
ItemAmount, ItemAmount,
ItemDescription,
ItemDiscount, ItemDiscount,
ItemQuantity, ItemQuantity,
} from "../../../../domain"; } from "../../../domain";
/** /**
* CreateProformaPropsMapper
* Convierte el DTO a las props validadas (CustomerProps). * Convierte el DTO a las props validadas (CustomerProps).
* No construye directamente el agregado. * No construye directamente el agregado.
* *
@ -42,7 +43,7 @@ import {
* *
*/ */
export class CreateCustomerInvoicePropsMapper { export class CreateProformaPropsMapper {
private readonly taxCatalog: JsonTaxCatalogProvider; private readonly taxCatalog: JsonTaxCatalogProvider;
private errors: ValidationErrorDetail[] = []; private errors: ValidationErrorDetail[] = [];
private languageCode?: LanguageCode; private languageCode?: LanguageCode;
@ -57,7 +58,7 @@ export class CreateCustomerInvoicePropsMapper {
try { try {
this.errors = []; this.errors = [];
const defaultStatus = CustomerInvoiceStatus.createDraft(); const defaultStatus = InvoiceStatus.createDraft();
const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", this.errors); const proformaId = extractOrPushError(UniqueID.create(dto.id), "id", this.errors);
@ -72,13 +73,13 @@ export class CreateCustomerInvoicePropsMapper {
const recipient = Maybe.none<InvoiceRecipient>(); const recipient = Maybe.none<InvoiceRecipient>();
const proformaNumber = extractOrPushError( const proformaNumber = extractOrPushError(
CustomerInvoiceNumber.create(dto.invoice_number), InvoiceNumber.create(dto.invoice_number),
"invoice_number", "invoice_number",
this.errors this.errors
); );
const series = extractOrPushError( const series = extractOrPushError(
maybeFromNullableVO(dto.series, (value) => CustomerInvoiceSerie.create(value)), maybeFromNullableVO(dto.series, (value) => InvoiceSerie.create(value)),
"series", "series",
this.errors this.errors
); );
@ -150,7 +151,7 @@ export class CreateCustomerInvoicePropsMapper {
); );
} }
const proformaProps: CustomerInvoiceProps = { const proformaProps: IProformaProps = {
companyId, companyId,
isProforma, isProforma,
proformaId: Maybe.none(), proformaId: Maybe.none(),
@ -194,9 +195,7 @@ export class CreateCustomerInvoicePropsMapper {
items.forEach((item, index) => { items.forEach((item, index) => {
const description = extractOrPushError( const description = extractOrPushError(
maybeFromNullableVO(item.description, (value) => maybeFromNullableVO(item.description, (value) => ItemDescription.create(value)),
CustomerInvoiceItemDescription.create(value)
),
"description", "description",
this.errors this.errors
); );
@ -221,7 +220,7 @@ export class CreateCustomerInvoicePropsMapper {
const taxes = this.mapTaxes(item, index); const taxes = this.mapTaxes(item, index);
const itemProps: CustomerInvoiceItemProps = { const itemProps: IssuedInvoiceItemProps = {
currencyCode: this.currencyCode!, currencyCode: this.currencyCode!,
languageCode: this.languageCode!, languageCode: this.languageCode!,
description: description!, description: description!,
@ -231,7 +230,7 @@ export class CreateCustomerInvoicePropsMapper {
taxes: taxes, taxes: taxes,
}; };
const itemResult = CustomerInvoiceItem.create(itemProps); const itemResult = IssuedInvoiceItem.create(itemProps);
if (itemResult.isSuccess) { if (itemResult.isSuccess) {
invoiceItems.add(itemResult.data); invoiceItems.add(itemResult.data);
} else { } else {

View File

@ -0,0 +1,4 @@
export * from "./create-proforma-props.mapper";
export * from "./proforma-domain-mapper.interface";
export * from "./proforma-list-mapper.interface";
export * from "./update-proforma-props.mapper";

View File

@ -0,0 +1,9 @@
import type { MapperParamsType } from "@erp/core/api";
import type { Result } from "@repo/rdx-utils";
import type { Proforma } from "../../../domain";
export interface IProformaDomainMapper {
mapToPersistence(proforma: Proforma, params?: MapperParamsType): Result<unknown, Error>;
mapToDomain(raw: unknown, params?: MapperParamsType): Result<Proforma, Error>;
}

View File

@ -0,0 +1,8 @@
import type { MapperParamsType } from "@erp/core/api";
import type { Result } from "@repo/rdx-utils";
import type { ProformaListDTO } from "../dtos";
export interface IProformaListMapper {
mapToDTO(raw: unknown, params?: MapperParamsType): Result<ProformaListDTO, Error>;
}

View File

@ -16,7 +16,7 @@ import type { UpdateProformaByIdRequestDTO } from "../../../../../common/dto";
import { type CustomerInvoicePatchProps, CustomerInvoiceSerie } from "../../../../domain"; import { type CustomerInvoicePatchProps, CustomerInvoiceSerie } from "../../../../domain";
/** /**
* mapDTOToUpdateCustomerInvoicePatchProps * UpdateProformaPropsMapper
* Convierte el DTO a las props validadas (CustomerInvoiceProps). * Convierte el DTO a las props validadas (CustomerInvoiceProps).
* No construye directamente el agregado. * No construye directamente el agregado.
* Tri-estado: * Tri-estado:
@ -29,7 +29,7 @@ import { type CustomerInvoicePatchProps, CustomerInvoiceSerie } from "../../../.
* *
*/ */
export function mapDTOToUpdateCustomerInvoicePatchProps(dto: UpdateProformaByIdRequestDTO) { export function UpdateProformaPropsMapper(dto: UpdateProformaByIdRequestDTO) {
try { try {
const errors: ValidationErrorDetail[] = []; const errors: ValidationErrorDetail[] = [];
const props: CustomerInvoicePatchProps = {}; const props: CustomerInvoicePatchProps = {};

View File

@ -0,0 +1 @@
export * from "./proforma-repository.interface";

View File

@ -0,0 +1,43 @@
import type { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
import type { Collection, Result } from "@repo/rdx-utils";
import type { InvoiceStatus, Proforma } from "../../../domain";
import type { ProformaListDTO } from "../dtos";
export interface IProformaRepository {
create(proforma: Proforma, transaction?: unknown): Promise<Result<void, Error>>;
update(proforma: Proforma, transaction?: unknown): Promise<Result<void, Error>>;
existsByIdInCompany(
companyId: UniqueID,
id: UniqueID,
tx: unknown
): Promise<Result<boolean, Error>>;
getByIdInCompany(
companyId: UniqueID,
id: UniqueID,
tx: unknown
): Promise<Result<Proforma, Error>>;
findByCriteriaInCompany(
companyId: UniqueID,
criteria: Criteria,
tx: unknown
): Promise<Result<Collection<ProformaListDTO>, Error>>;
deleteByIdInCompany(
companyId: UniqueID,
id: UniqueID,
tx: unknown
): Promise<Result<boolean, Error>>;
updateStatusByIdInCompany(
companyId: UniqueID,
id: UniqueID,
newStatus: InvoiceStatus,
tx: unknown
): Promise<Result<boolean, Error>>;
}

View File

@ -0,0 +1,7 @@
export * from "./proforma-creator";
export * from "./proforma-document-generator.interface";
export * from "./proforma-document-metadata-factory";
export * from "./proforma-document-properties-factory";
export * from "./proforma-finder";
export * from "./proforma-issuer";
export * from "./proforma-number-generator.interface";

View File

@ -0,0 +1,78 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { ICustomerInvoiceRepository } from "../../../domain";
import type { CustomerInvoice, CustomerInvoiceProps } from "../../../domain/aggregates";
import type { IProformaFactory } from "../factories";
import type { IProformaNumberGenerator } from "./proforma-number-generator.interface";
export interface IProformaCreator {
create(
companyId: UniqueID,
id: UniqueID,
props: CustomerInvoiceProps,
transaction: Transaction
): Promise<Result<CustomerInvoice, Error>>;
}
type ProformaCreatorDeps = {
numberService: IProformaNumberGenerator;
factory: IProformaFactory;
repository: ICustomerInvoiceRepository;
};
export class ProformaCreator implements IProformaCreator {
private readonly numberService: IProformaNumberGenerator;
private readonly factory: IProformaFactory;
private readonly repository: ICustomerInvoiceRepository;
constructor(deps: ProformaCreatorDeps) {
this.numberService = deps.numberService;
this.factory = deps.factory;
this.repository = deps.repository;
}
async create(
companyId: UniqueID,
id: UniqueID,
props: CustomerInvoiceProps,
transaction: Transaction
): Promise<Result<CustomerInvoice, Error>> {
// 1. Obtener siguiente número
const { series } = props;
const numberResult = await this.numberService.getNextForCompany(companyId, series, transaction);
if (numberResult.isFailure) {
return Result.fail(numberResult.error);
}
const invoiceNumber = numberResult.data;
// 2. Crear agregado
const buildResult = this.factory.createProforma(
companyId,
{
...props,
invoiceNumber,
},
id
);
if (buildResult.isFailure) {
return Result.fail(buildResult.error);
}
const proforma = buildResult.data;
// 3. Persistir
const saveResult = await this.repository.create(proforma, transaction);
if (saveResult.isFailure) {
return Result.fail(saveResult.error);
}
return Result.ok(proforma);
}
}

View File

@ -0,0 +1,6 @@
import type { DocumentGenerationService } from "@erp/core/api";
import type { ProformaReportSnapshot } from "../application-models";
export interface ProformaDocumentGeneratorService
extends DocumentGenerationService<ProformaReportSnapshot> {}

View File

@ -0,0 +1,47 @@
import type { IDocumentMetadata, IDocumentMetadataFactory } from "@erp/core/api";
import type { ProformaReportSnapshot } from "../application-models";
/**
* Construye los metadatos del documento PDF de una factura emitida.
*
* - Application-level
* - Determinista
* - Sin IO
*/
export class ProformaDocumentMetadataFactory
implements IDocumentMetadataFactory<ProformaReportSnapshot>
{
build(snapshot: ProformaReportSnapshot): IDocumentMetadata {
if (!snapshot.id) {
throw new Error("ProformaReportSnapshot.id is required");
}
if (!snapshot.company_id) {
throw new Error("ProformaReportSnapshot.companyId is required");
}
return {
documentType: "proforma",
documentId: snapshot.id,
companyId: snapshot.company_id,
companySlug: snapshot.company_slug,
format: "PDF",
languageCode: snapshot.language_code ?? "es",
filename: this.buildFilename(snapshot),
storageKey: this.buildCacheKey(snapshot),
};
}
private buildFilename(snapshot: ProformaReportSnapshot): string {
// Ejemplo: factura-F2024-000123-FULANITO.pdf
return `factura-${snapshot.series}${snapshot.invoice_number}-${snapshot.recipient.name}.pdf`;
}
private buildCacheKey(snapshot: ProformaReportSnapshot): string {
// Versionado explícito para invalidaciones futuras
return ["proforma", snapshot.company_id, snapshot.series, snapshot.invoice_number, "v1"].join(
":"
);
}
}

View File

@ -0,0 +1,23 @@
import type { IDocumentProperties, IDocumentPropertiesFactory } from "@erp/core/api";
import type { ProformaReportSnapshot } from "../application-models";
/**
* Construye los metadatos del documento PDF de una factura emitida.
*
* - Application-level
* - Determinista
* - Sin IO
*/
export class ProformaDocumentPropertiesFactory
implements IDocumentPropertiesFactory<ProformaReportSnapshot>
{
build(snapshot: ProformaReportSnapshot): IDocumentProperties {
return {
title: snapshot.reference,
subject: "proforma",
author: snapshot.company_slug,
creator: "FactuGES ERP",
};
}
}

View File

@ -0,0 +1,57 @@
import type { CustomerInvoiceListDTO } from "@erp/customer-invoices/api/infrastructure";
import type { Criteria } from "@repo/rdx-criteria/server";
import type { UniqueID } from "@repo/rdx-ddd";
import type { Collection, Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { ICustomerInvoiceRepository, Proforma } from "../../../domain";
export interface IProformaFinder {
findProformaById(
companyId: UniqueID,
invoiceId: UniqueID,
transaction?: Transaction
): Promise<Result<Proforma, Error>>;
proformaExists(
companyId: UniqueID,
invoiceId: UniqueID,
transaction?: Transaction
): Promise<Result<boolean, Error>>;
findProformasByCriteria(
companyId: UniqueID,
criteria: Criteria,
transaction?: Transaction
): Promise<Result<Collection<CustomerInvoiceListDTO>, Error>>;
}
export class ProformaFinder implements IProformaFinder {
constructor(private readonly repository: ICustomerInvoiceRepository) {}
async findProformaById(
companyId: UniqueID,
proformaId: UniqueID,
transaction?: Transaction
): Promise<Result<Proforma, Error>> {
return this.repository.getProformaByIdInCompany(companyId, proformaId, transaction, {});
}
async proformaExists(
companyId: UniqueID,
proformaId: UniqueID,
transaction?: Transaction
): Promise<Result<boolean, Error>> {
return this.repository.existsByIdInCompany(companyId, proformaId, transaction, {
is_proforma: true,
});
}
async findProformasByCriteria(
companyId: UniqueID,
criteria: Criteria,
transaction?: Transaction
): Promise<Result<Collection<CustomerInvoiceListDTO>, Error>> {
return this.repository.findProformasByCriteriaInCompany(companyId, criteria, transaction, {});
}
}

View File

@ -0,0 +1,44 @@
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { Transaction } from "sequelize";
import type { IProformaToIssuedInvoiceMaterializer } from "../../issued-invoices";
export class ProformaIssuer implements IProformaIssuer {
private readonly proformaRepository: IProformaRepository;
private readonly issuedInvoiceFactory: IIssuedInvoiceFactory;
private readonly issuedInvoiceRepository: IIssuedInvoiceRepository;
private readonly materializer: IProformaToIssuedInvoiceMaterializer;
constructor(deps: ProformaIssuerDeps) {
this.proformaRepository = deps.proformaRepository;
this.issuedInvoiceFactory = deps.issuedInvoiceFactory;
this.issuedInvoiceRepository = deps.issuedInvoiceRepository;
this.materializer = deps.materializer;
}
public async issue(
proforma: Proforma,
issuedInvoiceId: UniqueID,
transaction: Transaction
): Promise<Result<Proforma, Error>> {
const issueResult = proforma.issue();
if (issueResult.isFailure) return Result.fail(issueResult.error);
const propsResult = this.materializer.materialize(proforma, issuedInvoiceId);
if (propsResult.isFailure) return Result.fail(propsResult.error);
const invoiceResult = this.issuedInvoiceFactory.create(propsResult.data, issuedInvoiceId);
if (invoiceResult.isFailure) {
return Result.fail(invoiceResult.error);
}
await this.issuedInvoiceRepository.save(proforma.companyId, invoiceResult.data, transaction);
await this.proformaRepository.save(proforma.companyId, proforma, transaction);
return Result.ok(proforma);
}
}

View File

@ -1,12 +1,11 @@
import type { InvoiceNumber, InvoiceSerie } from "@erp/customer-invoices/api/domain";
import type { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import type { Maybe, Result } from "@repo/rdx-utils"; import type { Maybe, Result } from "@repo/rdx-utils";
import type { CustomerInvoiceNumber, CustomerInvoiceSerie } from "../value-objects";
/** /**
* Servicio de dominio que define cómo se genera el siguiente número de factura. * Servicio de dominio que define cómo se genera el siguiente número de factura.
*/ */
export interface ICustomerInvoiceNumberGenerator { export interface IProformaNumberGenerator {
/** /**
* Devuelve el siguiente número de factura disponible para una empresa dentro de una "serie" de factura. * Devuelve el siguiente número de factura disponible para una empresa dentro de una "serie" de factura.
* *
@ -14,9 +13,9 @@ export interface ICustomerInvoiceNumberGenerator {
* @param serie - Serie por la que buscar la última factura * @param serie - Serie por la que buscar la última factura
* @param transaction - Transacción activa * @param transaction - Transacción activa
*/ */
nextForCompany( getNextForCompany(
companyId: UniqueID, companyId: UniqueID,
series: Maybe<CustomerInvoiceSerie>, series: Maybe<InvoiceSerie>,
transaction: any transaction: any
): Promise<Result<CustomerInvoiceNumber, Error>>; ): Promise<Result<InvoiceNumber, Error>>;
} }

View File

@ -4,18 +4,17 @@ import type { Transaction } from "sequelize";
import { import {
CustomerInvoiceIsProformaSpecification, CustomerInvoiceIsProformaSpecification,
type CustomerInvoiceStatus, type InvoiceStatus,
ProformaCannotBeDeletedError, ProformaCannotBeDeletedError,
StatusInvoiceIsDraftSpecification, StatusInvoiceIsDraftSpecification,
} from "../../domain"; } from "../../../domain";
import type { import type {
CustomerInvoice, CustomerInvoice,
CustomerInvoicePatchProps, CustomerInvoicePatchProps,
CustomerInvoiceProps, CustomerInvoiceProps,
} from "../../domain/aggregates"; } from "../../../domain/aggregates";
import type { ICustomerInvoiceRepository } from "../../domain/repositories"; import type { ICustomerInvoiceRepository } from "../../../domain/repositories";
import type { IProformaFactory } from "../../services/proforma-factory";
import type { IProformaFactory } from "./proforma-factory";
export type IIssuedInvoiceWriteService = {}; export type IIssuedInvoiceWriteService = {};
@ -122,7 +121,7 @@ export class IssuedInvoiceWriteService implements IIssuedInvoiceWriteService {
async updateProformaStatus( async updateProformaStatus(
companyId: UniqueID, companyId: UniqueID,
proformaId: UniqueID, proformaId: UniqueID,
newStatus: CustomerInvoiceStatus, newStatus: InvoiceStatus,
transaction?: Transaction transaction?: Transaction
): Promise<Result<boolean, Error>> { ): Promise<Result<boolean, Error>> {
return this.repository.updateProformaStatusByIdInCompany( return this.repository.updateProformaStatusByIdInCompany(

View File

@ -0,0 +1,6 @@
export * from "./proforma-full-snapshot.interface";
export * from "./proforma-full-snapshot-builder";
export * from "./proforma-item-full-snapshot.interface";
export * from "./proforma-items-full-snapshot-builder";
export * from "./proforma-recipient-full-snapshot.interface";
export * from "./proforma-recipient-full-snapshot-builder";

View File

@ -0,0 +1,130 @@
import type { ISnapshotBuilder } from "@erp/core/api";
import { toEmptyString } from "@repo/rdx-ddd";
import { InvoiceAmount, type Proforma } from "../../../../domain";
import type { IProformaFullSnapshot } from "./proforma-full-snapshot.interface";
import type { IProformaItemsFullSnapshotBuilder } from "./proforma-items-full-snapshot-builder";
import type { IProformaRecipientFullSnapshotBuilder } from "./proforma-recipient-full-snapshot-builder";
export interface IProformaFullSnapshotBuilder
extends ISnapshotBuilder<Proforma, IProformaFullSnapshot> {}
export class ProformaFullSnapshotBuilder implements IProformaFullSnapshotBuilder {
constructor(
private readonly itemsBuilder: IProformaItemsFullSnapshotBuilder,
private readonly recipientBuilder: IProformaRecipientFullSnapshotBuilder
) {}
toOutput(invoice: Proforma): IProformaFullSnapshot {
const items = this.itemsBuilder.toOutput(invoice.items);
const recipient = this.recipientBuilder.toOutput(invoice);
const allAmounts = invoice.calculateAllAmounts();
const payment = invoice.paymentMethod.match(
(payment) => {
const { id, payment_description } = payment.toObjectString();
return {
payment_id: id,
payment_description,
};
},
() => undefined
);
let totalIvaAmount = InvoiceAmount.zero(invoice.currencyCode.code);
let totalRecAmount = InvoiceAmount.zero(invoice.currencyCode.code);
let totalRetentionAmount = InvoiceAmount.zero(invoice.currencyCode.code);
const invoiceTaxes = invoice.getTaxes().map((taxGroup) => {
const { ivaAmount, recAmount, retentionAmount, totalAmount } = taxGroup.calculateAmounts();
totalIvaAmount = totalIvaAmount.add(ivaAmount);
totalRecAmount = totalRecAmount.add(recAmount);
totalRetentionAmount = totalRetentionAmount.add(retentionAmount);
return {
taxable_amount: taxGroup.taxableAmount.toObjectString(),
iva_code: taxGroup.iva.code,
iva_percentage: taxGroup.iva.percentage.toObjectString(),
iva_amount: ivaAmount.toObjectString(),
rec_code: taxGroup.rec.match(
(rec) => rec.code,
() => ""
),
rec_percentage: taxGroup.rec.match(
(rec) => rec.percentage.toObjectString(),
() => ({ value: "", scale: "" })
),
rec_amount: recAmount.toObjectString(),
retention_code: taxGroup.retention.match(
(retention) => retention.code,
() => ""
),
retention_percentage: taxGroup.retention.match(
(retention) => retention.percentage.toObjectString(),
() => ({ value: "", scale: "" })
),
retention_amount: retentionAmount.toObjectString(),
taxes_amount: totalAmount.toObjectString(),
};
});
return {
id: invoice.id.toString(),
company_id: invoice.companyId.toString(),
is_proforma: invoice.isProforma ? "true" : "false",
invoice_number: invoice.invoiceNumber.toString(),
status: invoice.status.toPrimitive(),
series: toEmptyString(invoice.series, (value) => value.toString()),
invoice_date: invoice.invoiceDate.toDateString(),
operation_date: toEmptyString(invoice.operationDate, (value) => value.toDateString()),
reference: toEmptyString(invoice.reference, (value) => value.toString()),
description: toEmptyString(invoice.description, (value) => value.toString()),
notes: toEmptyString(invoice.notes, (value) => value.toString()),
language_code: invoice.languageCode.toString(),
currency_code: invoice.currencyCode.toString(),
customer_id: invoice.customerId.toString(),
recipient,
payment_method: payment,
subtotal_amount: allAmounts.subtotalAmount.toObjectString(),
items_discount_amount: allAmounts.itemDiscountAmount.toObjectString(),
discount_percentage: invoice.discountPercentage.toObjectString(),
discount_amount: allAmounts.globalDiscountAmount.toObjectString(),
taxable_amount: allAmounts.taxableAmount.toObjectString(),
iva_amount: totalIvaAmount.toObjectString(),
rec_amount: totalRecAmount.toObjectString(),
retention_amount: totalRetentionAmount.toObjectString(),
taxes_amount: allAmounts.taxesAmount.toObjectString(),
total_amount: allAmounts.totalAmount.toObjectString(),
taxes: invoiceTaxes,
items,
metadata: {
entity: "proformas",
},
};
}
}

View File

@ -0,0 +1,67 @@
import type { IProformaItemFullSnapshot } from "./proforma-item-full-snapshot.interface";
import type { IProformaRecipientFullSnapshot } from "./proforma-recipient-full-snapshot.interface";
export interface IProformaFullSnapshot {
id: string;
company_id: string;
is_proforma: "true" | "false";
invoice_number: string;
status: string;
series: string;
invoice_date: string;
operation_date: string;
reference: string;
description: string;
notes: string;
language_code: string;
currency_code: string;
customer_id: string;
recipient: IProformaRecipientFullSnapshot;
payment_method?: {
payment_id: string;
payment_description: string;
};
subtotal_amount: { value: string; scale: string; currency_code: string };
items_discount_amount: { value: string; scale: string; currency_code: string };
discount_percentage: { value: string; scale: string };
discount_amount: { value: string; scale: string; currency_code: string };
taxable_amount: { value: string; scale: string; currency_code: string };
iva_amount: { value: string; scale: string; currency_code: string };
rec_amount: { value: string; scale: string; currency_code: string };
retention_amount: { value: string; scale: string; currency_code: string };
taxes_amount: { value: string; scale: string; currency_code: string };
total_amount: { value: string; scale: string; currency_code: string };
taxes: Array<{
taxable_amount: { value: string; scale: string; currency_code: string };
iva_code: string;
iva_percentage: { value: string; scale: string };
iva_amount: { value: string; scale: string; currency_code: string };
rec_code: string;
rec_percentage: { value: string; scale: string };
rec_amount: { value: string; scale: string; currency_code: string };
retention_code: string;
retention_percentage: { value: string; scale: string };
retention_amount: { value: string; scale: string; currency_code: string };
taxes_amount: { value: string; scale: string; currency_code: string };
}>;
items: IProformaItemFullSnapshot[];
metadata?: Record<string, string>;
}

View File

@ -0,0 +1,34 @@
export interface IProformaItemFullSnapshot {
id: string;
is_valued: string;
position: string;
description: string;
quantity: { value: string; scale: string };
unit_amount: { value: string; scale: string; currency_code: string };
subtotal_amount: { value: string; scale: string; currency_code: string };
discount_percentage: { value: string; scale: string };
discount_amount: { value: string; scale: string; currency_code: string };
global_discount_percentage: { value: string; scale: string };
global_discount_amount: { value: string; scale: string; currency_code: string };
taxable_amount: { value: string; scale: string; currency_code: string };
iva_code: string;
iva_percentage: { value: string; scale: string };
iva_amount: { value: string; scale: string; currency_code: string };
rec_code: string;
rec_percentage: { value: string; scale: string };
rec_amount: { value: string; scale: string; currency_code: string };
retention_code: string;
retention_percentage: { value: string; scale: string };
retention_amount: { value: string; scale: string; currency_code: string };
taxes_amount: { value: string; scale: string; currency_code: string };
total_amount: { value: string; scale: string; currency_code: string };
}

View File

@ -0,0 +1,94 @@
import type { ISnapshotBuilder } from "@erp/core/api";
import { toEmptyString } from "@repo/rdx-ddd";
import type { CustomerInvoiceItems, IssuedInvoiceItem } from "../../../../domain";
import type { IProformaItemFullSnapshot } from "./proforma-item-full-snapshot.interface";
export interface IProformaItemsFullSnapshotBuilder
extends ISnapshotBuilder<CustomerInvoiceItems, IProformaItemFullSnapshot[]> {}
export class ProformaItemsFullSnapshotBuilder implements IProformaItemsFullSnapshotBuilder {
private mapItem(invoiceItem: IssuedInvoiceItem, index: number): IProformaItemFullSnapshot {
const allAmounts = invoiceItem.calculateAllAmounts();
return {
id: invoiceItem.id.toPrimitive(),
is_valued: String(invoiceItem.isValued),
position: String(index),
description: toEmptyString(invoiceItem.description, (value) => value.toPrimitive()),
quantity: invoiceItem.quantity.match(
(quantity) => quantity.toObjectString(),
() => ({ value: "", scale: "" })
),
unit_amount: invoiceItem.unitAmount.match(
(unitAmount) => unitAmount.toObjectString(),
() => ({ value: "", scale: "", currency_code: "" })
),
subtotal_amount: allAmounts.subtotalAmount.toObjectString(),
discount_percentage: invoiceItem.itemDiscountPercentage.match(
(discountPercentage) => discountPercentage.toObjectString(),
() => ({ value: "", scale: "" })
),
discount_amount: allAmounts.itemDiscountAmount.toObjectString(),
global_discount_percentage: invoiceItem.globalDiscountPercentage.match(
(discountPercentage) => discountPercentage.toObjectString(),
() => ({ value: "", scale: "" })
),
global_discount_amount: allAmounts.globalDiscountAmount.toObjectString(),
taxable_amount: allAmounts.taxableAmount.toObjectString(),
iva_code: invoiceItem.taxes.iva.match(
(iva) => iva.code,
() => ""
),
iva_percentage: invoiceItem.taxes.iva.match(
(iva) => iva.percentage.toObjectString(),
() => ({ value: "", scale: "" })
),
iva_amount: allAmounts.ivaAmount.toObjectString(),
rec_code: invoiceItem.taxes.rec.match(
(rec) => rec.code,
() => ""
),
rec_percentage: invoiceItem.taxes.rec.match(
(rec) => rec.percentage.toObjectString(),
() => ({ value: "", scale: "" })
),
rec_amount: allAmounts.recAmount.toObjectString(),
retention_code: invoiceItem.taxes.retention.match(
(retention) => retention.code,
() => ""
),
retention_percentage: invoiceItem.taxes.retention.match(
(retention) => retention.percentage.toObjectString(),
() => ({ value: "", scale: "" })
),
retention_amount: allAmounts.retentionAmount.toObjectString(),
taxes_amount: allAmounts.taxesAmount.toObjectString(),
total_amount: allAmounts.totalAmount.toObjectString(),
};
}
toOutput(invoiceItems: CustomerInvoiceItems): IProformaItemFullSnapshot[] {
return invoiceItems.map((item, index) => this.mapItem(item, index));
}
}

View File

@ -0,0 +1,43 @@
import type { ISnapshotBuilder } from "@erp/core/api";
import { DomainValidationError, toEmptyString } from "@repo/rdx-ddd";
import type { InvoiceRecipient, Proforma } from "../../../../domain";
import type { ProformaRecipientFullSnapshot } from "../../application-models";
export interface IProformaRecipientFullSnapshotBuilder
extends ISnapshotBuilder<Proforma, ProformaRecipientFullSnapshot> {}
export class ProformaRecipientFullSnapshotBuilder implements IProformaRecipientFullSnapshotBuilder {
toOutput(invoice: Proforma): ProformaRecipientFullSnapshot {
if (!invoice.recipient) {
throw DomainValidationError.requiredValue("recipient", {
cause: invoice,
});
}
return invoice.recipient.match(
(recipient: InvoiceRecipient) => ({
id: invoice.customerId.toString(),
name: recipient.name.toString(),
tin: recipient.tin.toString(),
street: toEmptyString(recipient.street, (v) => v.toString()),
street2: toEmptyString(recipient.street2, (v) => v.toString()),
city: toEmptyString(recipient.city, (v) => v.toString()),
province: toEmptyString(recipient.province, (v) => v.toString()),
postal_code: toEmptyString(recipient.postalCode, (v) => v.toString()),
country: toEmptyString(recipient.country, (v) => v.toString()),
}),
() => ({
id: "",
name: "",
tin: "",
street: "",
street2: "",
city: "",
province: "",
postal_code: "",
country: "",
})
);
}
}

View File

@ -0,0 +1,11 @@
export interface IProformaRecipientFullSnapshot {
id: string;
name: string;
tin: string;
street: string;
street2: string;
city: string;
province: string;
postal_code: string;
country: string;
}

View File

@ -0,0 +1,2 @@
export * from "./proforma-list-item-snapshot.interface";
export * from "./proforma-list-item-snapshot-builder";

View File

@ -0,0 +1,46 @@
import type { ISnapshotBuilder } from "@erp/core/api";
import { toEmptyString } from "@repo/rdx-ddd";
import type { CustomerInvoiceListDTO } from "../../../../infrastructure";
import type { ProformaListItemSnapshot } from "../../application-models";
export interface IProformaListItemSnapshotBuilder
extends ISnapshotBuilder<CustomerInvoiceListDTO, ProformaListItemSnapshot> {}
export class ProformaListItemSnapshotBuilder implements IProformaListItemSnapshotBuilder {
toOutput(proforma: CustomerInvoiceListDTO): ProformaListItemSnapshot {
const recipient = proforma.recipient.toObjectString();
return {
id: proforma.id.toString(),
company_id: proforma.companyId.toString(),
is_proforma: proforma.isProforma,
customer_id: proforma.customerId.toString(),
invoice_number: proforma.invoiceNumber.toString(),
status: proforma.status.toPrimitive(),
series: toEmptyString(proforma.series, (value) => value.toString()),
invoice_date: proforma.invoiceDate.toDateString(),
operation_date: toEmptyString(proforma.operationDate, (value) => value.toDateString()),
reference: toEmptyString(proforma.reference, (value) => value.toString()),
description: toEmptyString(proforma.description, (value) => value.toString()),
recipient,
language_code: proforma.languageCode.code,
currency_code: proforma.currencyCode.code,
subtotal_amount: proforma.subtotalAmount.toObjectString(),
discount_percentage: proforma.discountPercentage.toObjectString(),
discount_amount: proforma.discountAmount.toObjectString(),
taxable_amount: proforma.taxableAmount.toObjectString(),
taxes_amount: proforma.taxesAmount.toObjectString(),
total_amount: proforma.totalAmount.toObjectString(),
metadata: {
entity: "proforma",
},
};
}
}

View File

@ -0,0 +1,40 @@
export interface ProformaListItemSnapshot {
id: string;
company_id: string;
is_proforma: boolean;
customer_id: string;
invoice_number: string;
status: string;
series: string;
invoice_date: string;
operation_date: string;
language_code: string;
currency_code: string;
reference: string;
description: string;
recipient: {
tin: string;
name: string;
street: string;
street2: string;
city: string;
postal_code: string;
province: string;
country: string;
};
subtotal_amount: { value: string; scale: string; currency_code: string };
discount_percentage: { value: string; scale: string };
discount_amount: { value: string; scale: string; currency_code: string };
taxable_amount: { value: string; scale: string; currency_code: string };
taxes_amount: { value: string; scale: string; currency_code: string };
total_amount: { value: string; scale: string; currency_code: string };
metadata?: Record<string, string>;
}

View File

@ -0,0 +1,6 @@
export * from "./proforma-items-report-snapshot-builder";
export * from "./proforma-report-item-snapshot.interface";
export * from "./proforma-report-snapshot.interface";
export * from "./proforma-report-snapshot-builder";
export * from "./proforma-report-tax-snapshot.interface";
export * from "./proforma-tax-report-snapshot-builder";

View File

@ -0,0 +1,35 @@
import { MoneyDTOHelper, PercentageDTOHelper, QuantityDTOHelper } from "@erp/core";
import type { ISnapshotBuilder, ISnapshotBuilderParams } from "@erp/core/api";
import type { ProformaFullSnapshot, ProformaReportItemSnapshot } from "../../application-models";
export interface IProformaItemReportSnapshotBuilder
extends ISnapshotBuilder<ProformaFullSnapshot["items"], ProformaReportItemSnapshot[]> {}
export class ProformaItemReportSnapshotBuilder implements IProformaItemReportSnapshotBuilder {
toOutput(
items: ProformaFullSnapshot["items"],
params?: ISnapshotBuilderParams
): ProformaReportItemSnapshot[] {
const locale = params?.locale as string;
const moneyOptions = {
hideZeros: true,
minimumFractionDigits: 2,
};
return items.map((item) => ({
description: item.description,
quantity: QuantityDTOHelper.format(item.quantity, locale, { minimumFractionDigits: 0 }),
unit_amount: MoneyDTOHelper.format(item.unit_amount, locale, moneyOptions),
subtotal_amount: MoneyDTOHelper.format(item.subtotal_amount, locale, moneyOptions),
discount_percentage: PercentageDTOHelper.format(item.discount_percentage, locale, {
minimumFractionDigits: 0,
}),
discount_amount: MoneyDTOHelper.format(item.discount_amount, locale, moneyOptions),
taxable_amount: MoneyDTOHelper.format(item.taxable_amount, locale, moneyOptions),
taxes_amount: MoneyDTOHelper.format(item.taxes_amount, locale, moneyOptions),
total_amount: MoneyDTOHelper.format(item.total_amount, locale, moneyOptions),
}));
}
}

View File

@ -0,0 +1,11 @@
export interface ProformaReportItemSnapshot {
description: string;
quantity: string;
unit_amount: string;
subtotal_amount: string;
discount_percentage: string;
discount_amount: string;
taxable_amount: string;
taxes_amount: string;
total_amount: string;
}

View File

@ -0,0 +1,92 @@
import { DateHelper, MoneyDTOHelper, PercentageDTOHelper } from "@erp/core";
import type { ISnapshotBuilder, ISnapshotBuilderParams } from "@erp/core/api";
import type {
ProformaFullSnapshot,
ProformaReportItemSnapshot,
ProformaReportSnapshot,
ProformaReportTaxSnapshot,
} from "../../application-models";
export interface IProformaReportSnapshotBuilder
extends ISnapshotBuilder<ProformaFullSnapshot, ProformaReportSnapshot> {}
export class ProformaReportSnapshotBuilder implements IProformaReportSnapshotBuilder {
constructor(
private readonly itemsBuilder: ISnapshotBuilder<
ProformaFullSnapshot["items"],
ProformaReportItemSnapshot[]
>,
private readonly taxesBuilder: ISnapshotBuilder<
ProformaFullSnapshot["taxes"],
ProformaReportTaxSnapshot[]
>
) {}
toOutput(
snapshot: ProformaFullSnapshot,
params?: ISnapshotBuilderParams
): ProformaReportSnapshot {
const locale = params?.locale as string;
const moneyOptions = {
hideZeros: true,
minimumFractionDigits: 2,
};
return {
id: snapshot.id,
company_id: snapshot.company_id,
company_slug: "rodax",
invoice_number: snapshot.invoice_number,
series: snapshot.series,
status: snapshot.status,
reference: snapshot.reference,
language_code: snapshot.language_code,
currency_code: snapshot.currency_code,
invoice_date: DateHelper.format(snapshot.invoice_date, locale),
payment_method: snapshot.payment_method?.payment_description ?? "",
notes: snapshot.notes,
recipient: {
name: snapshot.recipient.name,
tin: snapshot.recipient.tin,
format_address: this.formatAddress(snapshot.recipient),
},
items: this.itemsBuilder.toOutput(snapshot.items, { locale }),
taxes: this.taxesBuilder.toOutput(snapshot.taxes, { locale }),
subtotal_amount: MoneyDTOHelper.format(snapshot.subtotal_amount, locale, moneyOptions),
discount_percentage: PercentageDTOHelper.format(snapshot.discount_percentage, locale, {
hideZeros: true,
}),
discount_amount: MoneyDTOHelper.format(snapshot.discount_amount, locale, moneyOptions),
taxable_amount: MoneyDTOHelper.format(snapshot.taxable_amount, locale, moneyOptions),
taxes_amount: MoneyDTOHelper.format(snapshot.taxes_amount, locale, moneyOptions),
total_amount: MoneyDTOHelper.format(snapshot.total_amount, locale, moneyOptions),
};
}
private formatAddress(recipient: ProformaFullSnapshot["recipient"]): string {
const lines: string[] = [];
if (recipient.street) lines.push(recipient.street);
if (recipient.street2) lines.push(recipient.street2);
const cityLine = [recipient.postal_code, recipient.city].filter(Boolean).join(" ");
if (cityLine) lines.push(cityLine);
if (recipient.province && recipient.province !== recipient.city) {
lines.push(recipient.province);
}
if (recipient.country && recipient.country !== "es") {
lines.push(recipient.country);
}
return lines.join("\n");
}
}

View File

@ -0,0 +1,35 @@
import type { ProformaReportItemSnapshot } from "./proforma-report-item-snapshot.interface";
import type { ProformaReportTaxSnapshot } from "./proforma-report-tax-snapshot.interface";
export interface ProformaReportSnapshot {
id: string;
company_id: string;
company_slug: string;
invoice_number: string;
series: string;
status: string;
reference: string;
language_code: string;
currency_code: string;
invoice_date: string;
payment_method: string;
notes: string;
recipient: {
name: string;
tin: string;
format_address: string;
};
items: ProformaReportItemSnapshot[];
taxes: ProformaReportTaxSnapshot[];
subtotal_amount: string;
discount_percentage: string;
discount_amount: string;
taxable_amount: string;
taxes_amount: string;
total_amount: string;
}

View File

@ -0,0 +1,17 @@
export interface ProformaReportTaxSnapshot {
taxable_amount: string;
iva_code: string;
iva_percentage: string;
iva_amount: string;
rec_code: string;
rec_percentage: string;
rec_amount: string;
retention_code: string;
retention_percentage: string;
retention_amount: string;
taxes_amount: string;
}

View File

@ -0,0 +1,39 @@
import { MoneyDTOHelper, PercentageDTOHelper } from "@erp/core";
import type { ISnapshotBuilder, ISnapshotBuilderParams } from "@erp/core/api";
import type { ProformaFullSnapshot, ProformaReportTaxSnapshot } from "../../application-models";
export interface IProformaTaxReportSnapshotBuilder
extends ISnapshotBuilder<ProformaFullSnapshot["taxes"], ProformaReportTaxSnapshot[]> {}
export class ProformaTaxReportSnapshotBuilder implements IProformaTaxReportSnapshotBuilder {
toOutput(
taxes: ProformaFullSnapshot["taxes"],
params?: ISnapshotBuilderParams
): ProformaReportTaxSnapshot[] {
const locale = params?.locale as string;
const moneyOptions = {
hideZeros: true,
minimumFractionDigits: 2,
};
return taxes.map((tax) => ({
taxable_amount: MoneyDTOHelper.format(tax.taxable_amount, locale, moneyOptions),
iva_code: tax.iva_code,
iva_percentage: PercentageDTOHelper.format(tax.iva_percentage, locale),
iva_amount: MoneyDTOHelper.format(tax.iva_amount, locale, moneyOptions),
rec_code: tax.rec_code,
rec_percentage: PercentageDTOHelper.format(tax.rec_percentage, locale),
rec_amount: MoneyDTOHelper.format(tax.rec_amount, locale, moneyOptions),
retention_code: tax.retention_code,
retention_percentage: PercentageDTOHelper.format(tax.retention_percentage, locale),
retention_amount: MoneyDTOHelper.format(tax.retention_amount, locale, moneyOptions),
taxes_amount: MoneyDTOHelper.format(tax.taxes_amount, locale, moneyOptions),
}));
}
}

View File

@ -0,0 +1,62 @@
import type { ITransactionManager } from "@erp/core/api";
import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { CreateProformaRequestDTO } from "../../../../../common";
import type { CreateProformaPropsMapper } from "../../mappers";
import type { IProformaCreator } from "../../services";
import type { IProformaFullSnapshotBuilder } from "../../snapshot-builders";
type CreateProformaUseCaseInput = {
companyId: UniqueID;
dto: CreateProformaRequestDTO;
};
type CreateProformaUseCaseDeps = {
mapper: CreateProformaPropsMapper;
creator: IProformaCreator;
fullSnapshotBuilder: IProformaFullSnapshotBuilder;
transactionManager: ITransactionManager;
};
export class CreateProformaUseCase {
private readonly mapper: CreateProformaPropsMapper;
private readonly creator: IProformaCreator;
private readonly fullSnapshotBuilder: IProformaFullSnapshotBuilder;
private readonly transactionManager: ITransactionManager;
constructor(deps: CreateProformaUseCaseDeps) {
this.mapper = deps.mapper;
this.creator = deps.creator;
this.fullSnapshotBuilder = deps.fullSnapshotBuilder;
this.transactionManager = deps.transactionManager;
}
public async execute(params: CreateProformaUseCaseInput) {
const { dto, companyId } = params;
// 1) Mapear DTO → props de dominio
const mappedResult = this.mapper.map(dto, companyId);
if (mappedResult.isFailure) {
return Result.fail(mappedResult.error);
}
const { props, id } = mappedResult.data;
return this.transactionManager.complete(async (transaction) => {
try {
const createResult = await this.creator.create(companyId, id, props, transaction);
if (createResult.isFailure) {
return Result.fail(createResult.error);
}
const snapshot = this.fullSnapshotBuilder.toOutput(createResult.data);
return Result.ok(snapshot);
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}

View File

@ -0,0 +1,49 @@
import type { ITransactionManager } from "@erp/core/api";
import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import type { IProformaFinder } from "../services";
import type { IProformaFullSnapshotBuilder } from "../snapshot-builders";
type GetProformaUseCaseInput = {
companyId: UniqueID;
proforma_id: string;
};
export class GetProformaByIdUseCase {
constructor(
private readonly finder: IProformaFinder,
private readonly fullSnapshotBuilder: IProformaFullSnapshotBuilder,
private readonly transactionManager: ITransactionManager
) {}
public execute(params: GetProformaUseCaseInput) {
const { proforma_id, companyId } = params;
const idOrError = UniqueID.create(proforma_id);
if (idOrError.isFailure) {
return Result.fail(idOrError.error);
}
const proformaId = idOrError.data;
return this.transactionManager.complete(async (transaction) => {
try {
const proformaResult = await this.finder.findProformaById(
companyId,
proformaId,
transaction
);
if (proformaResult.isFailure) {
return Result.fail(proformaResult.error);
}
const fullSnapshot = this.fullSnapshotBuilder.toOutput(proformaResult.data);
return Result.ok(fullSnapshot);
} catch (error: unknown) {
return Result.fail(error as Error);
}
});
}
}

Some files were not shown because too many files have changed in this diff Show More