diff --git a/modules/core/src/api/infrastructure/sequelize/sequelize-transaction-manager.ts b/modules/core/src/api/infrastructure/sequelize/sequelize-transaction-manager.ts index d36ccf87..a1c1b291 100644 --- a/modules/core/src/api/infrastructure/sequelize/sequelize-transaction-manager.ts +++ b/modules/core/src/api/infrastructure/sequelize/sequelize-transaction-manager.ts @@ -21,7 +21,7 @@ export class SequelizeTransactionManager extends TransactionManager { } protected async _rollbackTransaction(): Promise { - throw new InfrastructureError("Database not available"); + throw new InfrastructureError("[SequelizeTransactionManager] Database not available"); } constructor(database: Sequelize, options?: { isolationLevel?: string }) { @@ -32,7 +32,9 @@ export class SequelizeTransactionManager extends TransactionManager { get database(): Sequelize { if (!this._database) { - throw new InfrastructureUnavailableError("Database not available"); + throw new InfrastructureUnavailableError( + "[SequelizeTransactionManager] Database not available" + ); } return this._database; } @@ -43,7 +45,9 @@ export class SequelizeTransactionManager extends TransactionManager { */ async complete(work: (transaction: Transaction) => Promise): Promise { if (!this._database) { - throw new InfrastructureUnavailableError("Database not available"); + throw new InfrastructureUnavailableError( + "[SequelizeTransactionManager] Database not available" + ); } // Evita transacciones anidadas según la política del TransactionManager base @@ -52,7 +56,9 @@ export class SequelizeTransactionManager extends TransactionManager { "❌ Cannot start a new transaction inside another. Nested transactions are not allowed.", { label: "SequelizeTransactionManager.complete" } ); - throw new Error("A transaction is already active. Nested transactions are not allowed."); + throw new InfrastructureError( + "[SequelizeTransactionManager] A transaction is already active. Nested transactions are not allowed." + ); } try { diff --git a/modules/customer-invoices/src/api/application/specs/index.ts b/modules/customer-invoices/src/api/application/specs/index.ts index 4510ef9a..e5c2b414 100644 --- a/modules/customer-invoices/src/api/application/specs/index.ts +++ b/modules/customer-invoices/src/api/application/specs/index.ts @@ -1,2 +1 @@ -//export * from "./participantAddressFinder"; -//export * from "./participantFinder"; +export * from "./status-invoice_is_approved.spec"; diff --git a/modules/customer-invoices/src/api/application/specs/status-invoice_is_approved.spec.ts b/modules/customer-invoices/src/api/application/specs/status-invoice_is_approved.spec.ts new file mode 100644 index 00000000..a33a3b7d --- /dev/null +++ b/modules/customer-invoices/src/api/application/specs/status-invoice_is_approved.spec.ts @@ -0,0 +1,8 @@ +import { CompositeSpecification } from "@repo/rdx-ddd"; +import { CustomerInvoice } from "../../domain"; + +export class StatusInvoiceIsApprovedSpecification extends CompositeSpecification { + public async isSatisfiedBy(invoice: CustomerInvoice): Promise { + return invoice.status.isApproved(); + } +} diff --git a/modules/customer-invoices/src/api/application/use-cases/index.ts b/modules/customer-invoices/src/api/application/use-cases/index.ts index b33437d3..51261c63 100644 --- a/modules/customer-invoices/src/api/application/use-cases/index.ts +++ b/modules/customer-invoices/src/api/application/use-cases/index.ts @@ -3,3 +3,4 @@ export * from "./get-customer-invoice.use-case"; export * from "./list-customer-invoices.use-case"; export * from "./report"; //export * from "./update-customer-invoice.use-case"; +export * from "./issue-customer-invoice.use-case"; diff --git a/modules/customer-invoices/src/api/application/use-cases/issue-customer-invoice.use-case.ts b/modules/customer-invoices/src/api/application/use-cases/issue-customer-invoice.use-case.ts new file mode 100644 index 00000000..af7827b2 --- /dev/null +++ b/modules/customer-invoices/src/api/application/use-cases/issue-customer-invoice.use-case.ts @@ -0,0 +1,72 @@ +import { EntityNotFoundError, ITransactionManager } from "@erp/core/api"; +import { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; +import { CustomerInvoiceNumber, CustomerInvoiceService } from "../../domain"; +import { StatusInvoiceIsApprovedSpecification } from "../specs"; + +type IssueCustomerInvoiceUseCaseInput = { + companyId: UniqueID; + invoice_id: string; +}; + +export class IssueCustomerInvoiceUseCase { + constructor( + private readonly service: CustomerInvoiceService, + private readonly transactionManager: ITransactionManager + ) {} + + public execute(params: IssueCustomerInvoiceUseCaseInput) { + const { invoice_id, companyId } = params; + + const idOrError = UniqueID.create(invoice_id); + + if (idOrError.isFailure) { + return Result.fail(idOrError.error); + } + + const invoiceId = idOrError.data; + + return this.transactionManager.complete(async (transaction) => { + try { + const invoiceResult = await this.service.getInvoiceByIdInCompany( + companyId, + invoiceId, + transaction + ); + + if (invoiceResult.isFailure) { + return Result.fail(invoiceResult.error); + } + + const invoiceProforma = invoiceResult.data; + + const isOk = new StatusInvoiceIsApprovedSpecification().isSatisfiedBy(invoiceProforma); + + if (!isOk) { + return Result.fail( + new EntityNotFoundError("Customer invoice", "id", invoiceId.toString()) + ); + } + + // La factura se puede emitir. + // Pedir el número de factura + const newInvoiceNumber = CustomerInvoiceNumber.create("xxx/001").data; + + // Asignamos el número de la factura + + const issuedInvoiceResult = invoiceProforma.issueInvoice(newInvoiceNumber); + if (issuedInvoiceResult.isFailure) { + return Result.fail(new EntityNotFoundError("Customer invoice", "id", error)); + } + + const issuedInvoice = issuedInvoiceResult.data; + + this.service.saveInvoice(issuedInvoice, transaction); + + //return await this.service.IssueInvoiceByIdInCompany(companyId, invoiceId, transaction); + } catch (error: unknown) { + return Result.fail(error as Error); + } + }); + } +} diff --git a/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts b/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts index 64d6d3ed..aa9ed6fb 100644 --- a/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts +++ b/modules/customer-invoices/src/api/domain/aggregates/customer-invoice.ts @@ -38,12 +38,15 @@ export interface CustomerInvoiceProps { languageCode: LanguageCode; currencyCode: CurrencyCode; - taxes: InvoiceTaxes; items: CustomerInvoiceItems; paymentMethod: Maybe; discountPercentage: Percentage; + + verifactu_qr: string; + verifactu_url: string; + verifactu_status: string; } export interface ICustomerInvoice { @@ -56,6 +59,8 @@ export interface ICustomerInvoice { getTaxableAmount(): InvoiceAmount; getTaxesAmount(): InvoiceAmount; getTotalAmount(): InvoiceAmount; + + issueInvoice(newInvoiceNumber: CustomerInvoiceNumber): Result; } export type CustomerInvoicePatchProps = Partial>; @@ -65,7 +70,6 @@ export class CustomerInvoice implements ICustomerInvoice { private _items!: CustomerInvoiceItems; - private _taxes!: InvoiceTaxes; protected constructor(props: CustomerInvoiceProps, id?: UniqueID) { super(props, id); @@ -75,8 +79,6 @@ export class CustomerInvoice languageCode: props.languageCode, currencyCode: props.currencyCode, }); - - this._taxes = props.taxes; } static create(props: CustomerInvoiceProps, id?: UniqueID): Result { @@ -167,7 +169,7 @@ export class CustomerInvoice } public get taxes(): InvoiceTaxes { - return this._taxes; + return this.items.getTaxesAmountByTaxes(); } public get hasRecipient() { @@ -240,4 +242,16 @@ export class CustomerInvoice totalAmount, }; } + + public issueInvoice(newInvoiceNumber: CustomerInvoiceNumber) { + return CustomerInvoice.create( + { + ...this.props, + status: CustomerInvoiceStatus.createEmitted(), + isProforma: false, + invoiceNumber: newInvoiceNumber, + }, + this.id + ); + } } diff --git a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts b/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts index 43f7fe04..62a80778 100644 --- a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts +++ b/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-item.ts @@ -162,6 +162,10 @@ export class CustomerInvoiceItem return this._getTotalAmount(taxableAmount, taxesAmount); } + public getTaxesAmountByTaxes() { + return this.taxes.getTaxesAmountByTaxes(this.getTaxableAmount()); + } + public getAllAmounts() { const subtotalAmount = this.getSubtotalAmount(); const discountAmount = this._getDiscountAmount(subtotalAmount); diff --git a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-items.ts b/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-items.ts index 8710a2b8..e1f1e218 100644 --- a/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-items.ts +++ b/modules/customer-invoices/src/api/domain/entities/customer-invoice-items/customer-invoice-items.ts @@ -1,6 +1,8 @@ +import { Tax } from "@erp/core/api"; import { CurrencyCode, LanguageCode } from "@repo/rdx-ddd"; import { Collection } from "@repo/rdx-utils"; import { ItemAmount } from "../../value-objects"; +import { InvoiceTax, InvoiceTaxes } from "../invoice-taxes"; import { CustomerInvoiceItem } from "./customer-invoice-item"; export interface CustomerInvoiceItemsProps { @@ -70,4 +72,32 @@ export class CustomerInvoiceItems extends Collection { ItemAmount.zero(this._currencyCode.code) ); } + + public getTaxesAmountByTaxes(): InvoiceTaxes { + InvoiceTaxes.create({}); + + const taxesMap = new Map(); + const currencyCode = this._currencyCode.code; + + for (const item of this.getAll()) { + for (const { tax, taxesAmount } of item.getTaxesAmountByTaxes()) { + const current = taxesMap.get(tax) ?? ItemAmount.zero(currencyCode); + taxesMap.set(tax, current.add(taxesAmount)); + } + } + + const items: InvoiceTax[] = []; + for (const [tax, taxesAmount] of taxesMap) { + items.push( + InvoiceTax.create({ + tax, + taxesAmount, + }).data + ); + } + + return InvoiceTaxes.create({ + items: items, + }); + } } diff --git a/modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-tax.ts b/modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-tax.ts index 3b093d69..70bbb2f2 100644 --- a/modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-tax.ts +++ b/modules/customer-invoices/src/api/domain/entities/invoice-taxes/invoice-tax.ts @@ -1,10 +1,12 @@ import { Tax } from "@erp/core/api"; import { DomainEntity, UniqueID } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; +import { ItemAmount } from "../../value-objects"; import { InvoiceAmount } from "../../value-objects/invoice-amount"; export interface InvoiceTaxProps { tax: Tax; + taxesAmount: ItemAmount; } export class InvoiceTax extends DomainEntity { diff --git a/modules/customer-invoices/src/api/domain/entities/item-taxes/item-taxes.ts b/modules/customer-invoices/src/api/domain/entities/item-taxes/item-taxes.ts index 8f9efe53..05dfe704 100644 --- a/modules/customer-invoices/src/api/domain/entities/item-taxes/item-taxes.ts +++ b/modules/customer-invoices/src/api/domain/entities/item-taxes/item-taxes.ts @@ -23,6 +23,21 @@ export class ItemTaxes extends Collection { ); } + public getTaxesAmountByTaxCode(taxCode: string, taxableAmount: ItemAmount): ItemAmount { + const currencyCode = taxableAmount.currencyCode; + + return this.filter((itemTax) => itemTax.tax.code === taxCode).reduce((totalAmount, itemTax) => { + return itemTax.getTaxAmount(taxableAmount).add(totalAmount); + }, ItemAmount.zero(currencyCode)); + } + + public getTaxesAmountByTaxes(taxableAmount: ItemAmount): { + return this.getAll().map((taxItem) => ({ + tax: taxItem.tax, + taxesAmount: this.getTaxesAmountByTaxCode(taxItem.tax.code, taxableAmount), + })); + } + public getCodesToString(): string { return this.getAll() .map((taxItem) => taxItem.tax.code) diff --git a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-status.ts b/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-status.ts index cab590cd..aa1463e7 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-status.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-status.ts @@ -74,6 +74,10 @@ export class CustomerInvoiceStatus extends ValueObject this.ok(data), + (err) => this.handleError(err) + ); + } +} diff --git a/modules/customer-invoices/src/api/infrastructure/express/customer-invoices.routes.ts b/modules/customer-invoices/src/api/infrastructure/express/customer-invoices.routes.ts index 175f1a14..6f5ff708 100644 --- a/modules/customer-invoices/src/api/infrastructure/express/customer-invoices.routes.ts +++ b/modules/customer-invoices/src/api/infrastructure/express/customer-invoices.routes.ts @@ -116,5 +116,17 @@ export const customerInvoicesRouter = (params: ModuleParams) => { } ); +router.put( + "/:invoice_id/issue", + //checkTabContext, + validateRequest(XXX, "params"), + validateRequest(XXX, "body"), + (req: Request, res: Response, next: NextFunction) => { + const useCase = deps.build.issue(); + const controller = new IssueCustomerInvoiceController(useCase); + return controller.execute(req, res, next); + } + + app.use(`${baseRoutePath}/customer-invoices`, router); }; diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice-item.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice-item.mapper.ts index b18737e1..5a1cbaa8 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice-item.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice-item.mapper.ts @@ -5,12 +5,15 @@ import { ValidationErrorDetail, extractOrPushError, maybeFromNullableVO, + toNullable, } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { InferCreationAttributes } from "sequelize"; import { + CustomerInvoice, CustomerInvoiceItem, CustomerInvoiceItemDescription, + CustomerInvoiceItemProps, CustomerInvoiceProps, ItemAmount, ItemDiscount, @@ -42,7 +45,10 @@ export class CustomerInvoiceItemDomainMapper this._taxesMapper = new ItemTaxesDomainMapper(params); } - private mapAttributesToDomain(source: CustomerInvoiceItemModel, params?: MapperParamsType) { + private mapAttributesToDomain( + source: CustomerInvoiceItemModel, + params?: MapperParamsType + ): Partial & { itemId?: UniqueID } { const { errors, index, attributes } = params as { index: number; errors: ValidationErrorDetail[]; @@ -65,7 +71,7 @@ export class CustomerInvoiceItemDomainMapper const quantity = extractOrPushError( maybeFromNullableVO(source.quantity_value, (value) => ItemQuantity.create({ value })), - `items[${index}].discount_percentage`, + `items[${index}].quantity`, errors ); @@ -77,14 +83,23 @@ export class CustomerInvoiceItemDomainMapper errors ); + const discountPercentage = extractOrPushError( + maybeFromNullableVO(source.discount_percentage_value, (value) => + ItemDiscount.create({ value }) + ), + `items[${index}].discount_percentage`, + errors + ); + return { itemId, + languageCode: attributes.languageCode, currencyCode: attributes.currencyCode, description, quantity, - unitAmount, + discountPercentage, }; } @@ -112,16 +127,7 @@ export class CustomerInvoiceItemDomainMapper } } - // 3) Importes - const discountPercentage = extractOrPushError( - maybeFromNullableVO(source.discount_percentage_value, (value) => - ItemDiscount.create({ value }) - ), - `items[${index}].discount_percentage`, - errors - ); - - // 4) Taxes (colección a nivel de item/línea) + // 3) Taxes (colección a nivel de item/línea) const taxesResults = this._taxesMapper.mapToDomainCollection( source.taxes, source.taxes.length, @@ -133,7 +139,7 @@ export class CustomerInvoiceItemDomainMapper if (taxesResults.isFailure) { errors.push({ - path: `taxes[${index}].discount_percentage`, + path: "taxes", message: taxesResults.error.message, }); } @@ -151,7 +157,7 @@ export class CustomerInvoiceItemDomainMapper description: attributes.description!, quantity: attributes.quantity!, unitAmount: attributes.unitAmount!, - discountPercentage: discountPercentage!, + discountPercentage: attributes.discountPercentage!, taxes, }, attributes.itemId @@ -172,63 +178,59 @@ export class CustomerInvoiceItemDomainMapper source: CustomerInvoiceItem, params?: MapperParamsType ): Result, Error> { - throw new Error("not implemented"); - /* - - const { index, sourceParent } = params as { + const { errors, index, parent } = params as { index: number; - sourceParent: CustomerInvoice; + parent: CustomerInvoice; + errors: ValidationErrorDetail[]; }; - - return { + const taxesResults = this._taxesMapper.mapToPersistenceArray(source.taxes, params); + + if (taxesResults.isFailure) { + errors.push({ + path: "taxes", + message: taxesResults.error.message, + }); + } + + const allAmounts = source.getAllAmounts(); + + return Result.ok({ item_id: source.id.toPrimitive(), - invoice_id: sourceParent.id.toPrimitive(), + invoice_id: parent.id.toPrimitive(), position: index, - description: toNullable(source.description, (description) => description.toPrimitive()), + description: toNullable(source.description, (v) => v.toPrimitive()), - quantity_value: toNullable(source.quantity, (value) => value.toPrimitive().value), - quantity_scale: source.quantity.match( - (value) => value.toPrimitive().scale, - () => ItemQuantity.DEFAULT_SCALE - ), + quantity_value: toNullable(source.quantity, (v) => v.toPrimitive().value), + quantity_scale: toNullable(source.quantity, (v) => v.toPrimitive().scale), - unit_amount_value: toNullable(source.unitAmount, (value) => value.toPrimitive().value), - unit_amount_scale: source.unitAmount.match( - (value) => value.toPrimitive().scale, - () => ItemAmount.DEFAULT_SCALE - ), + unit_amount_value: toNullable(source.unitAmount, (v) => v.toPrimitive().value), + unit_amount_scale: toNullable(source.unitAmount, (v) => v.toPrimitive().scale), - subtotal_amount_value: source.subtotalAmount.toPrimitive().value, - subtotal_amount_scale: source.subtotalAmount.toPrimitive().scale, + subtotal_amount_value: allAmounts.subtotalAmount.value, + subtotal_amount_scale: allAmounts.subtotalAmount.scale, discount_percentage_value: toNullable( source.discountPercentage, - (value) => value.toPrimitive().value + (v) => v.toPrimitive().value ), - discount_percentage_scale: source.discountPercentage.match( - (value) => value.toPrimitive().scale, - () => ItemAmount.DEFAULT_SCALE + discount_percentage_scale: toNullable( + source.discountPercentage, + (v) => v.toPrimitive().scale ), - discount_amount_value: toNullable( - source.discountAmount, - (value) => value.toPrimitive().value - ), - discount_amount_scale: source.discountAmount.match( - (value) => value.toPrimitive().scale, - () => ItemDiscount.DEFAULT_SCALE - ), + discount_amount_value: allAmounts.discountAmount.value, + discount_amount_scale: allAmounts.discountAmount.scale, - taxable_amount_value: source.taxableAmount.toPrimitive().value, - taxable_amount_scale: source.taxableAmount.toPrimitive().value, + taxable_amount_value: allAmounts.taxableAmount.value, + taxable_amount_scale: allAmounts.taxableAmount.value, - taxes_amount_value: source.taxesAmount.toPrimitive().value, - taxes_amount_scale: source.taxesAmount.toPrimitive().value, + taxes_amount_value: allAmounts.taxesAmount.value, + taxes_amount_scale: allAmounts.taxesAmount.scale, - total_amount_value: source.totalAmount.toPrimitive().value, - total_amount_scale: source.totalAmount.toPrimitive().scale, - };*/ + total_amount_value: allAmounts.totalAmount.value, + total_amount_scale: allAmounts.totalAmount.scale, + }); } } diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice.mapper.ts index 722685d3..2515700e 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/domain/customer-invoice.mapper.ts @@ -1,4 +1,9 @@ -import { ISequelizeDomainMapper, MapperParamsType, SequelizeDomainMapper } from "@erp/core/api"; +import { + ISequelizeDomainMapper, + InfrastructureError, + MapperParamsType, + SequelizeDomainMapper, +} from "@erp/core/api"; import { CurrencyCode, LanguageCode, @@ -10,6 +15,7 @@ import { ValidationErrorDetail, extractOrPushError, maybeFromNullableVO, + toNullable, } from "@repo/rdx-ddd"; import { Maybe, Result } from "@repo/rdx-utils"; import { @@ -25,7 +31,7 @@ import { InvoiceTaxes } from "../../../domain/entities/invoice-taxes"; import { CustomerInvoiceCreationAttributes, CustomerInvoiceModel } from "../../sequelize"; import { CustomerInvoiceItemDomainMapper as CustomerInvoiceItemFullMapper } from "./customer-invoice-item.mapper"; import { InvoiceRecipientDomainMapper as InvoiceRecipientFullMapper } from "./invoice-recipient.mapper"; -import { TaxesDomainMapper as TaxesFullMapper } from "./taxes.mapper"; +import { TaxesDomainMapper as TaxesFullMapper } from "./invoice-taxes.mapper"; export interface ICustomerInvoiceDomainMapper extends ISequelizeDomainMapper< @@ -324,33 +330,86 @@ export class CustomerInvoiceDomainMapper source: CustomerInvoice, params?: MapperParamsType ): Result { - throw new Error("not implemented"); + const errors: ValidationErrorDetail[] = []; - /*const items = this._itemsMapper.mapCollectionToPersistence(source.items, params); + // 1) Items + const itemsResult = this._itemsMapper.mapToPersistenceArray(source.items, { + errors, + parent: source, + ...params, + }); + if (itemsResult.isFailure) { + errors.push({ + path: "items", + message: itemsResult.error.message, + }); + } - const customer = source.recipient.match( - (recipient) => - ({ - customer_id: recipient.id.toPrimitive(), - customer_tin: recipient.tin.toPrimitive(), - customer_name: recipient.name.toPrimitive(), - customer_street: toNullable(recipient.address.street, (street) => street.toPrimitive()), - customer_street2: toNullable(recipient.address.street2, (street2) => - street2.toPrimitive() - ), - customer_city: toNullable(recipient.address.city, (city) => city.toPrimitive()), - customer_province: toNullable(recipient.address.province, (province) => - province.toPrimitive() - ), - customer_postal_code: toNullable(recipient.address.postalCode, (postalCode) => - postalCode.toPrimitive() - ), - customer_country: toNullable(recipient.address.country, (country) => - country.toPrimitive() - ), - }) as any, - () => ({ - customer_id: source.customerId.toPrimitive(), + // 1) Taxes + const taxesResult = this._taxesMapper.mapToPersistenceArray(source.taxes, { + errors, + parent: source, + ...params, + }); + if (taxesResult.isFailure) { + errors.push({ + path: "taxes", + message: taxesResult.error.message, + }); + } + + // 3) Calcular totales + const allAmounts = source.getAllAmounts(); + + // 4) Construir parte + const invoiceValues: Partial = { + id: source.id.toPrimitive(), + company_id: source.companyId.toPrimitive(), + + is_proforma: source.isProforma, + status: source.status.toPrimitive(), + series: toNullable(source.series, (series) => series.toPrimitive()), + invoice_number: source.invoiceNumber.toPrimitive(), + invoice_date: source.invoiceDate.toPrimitive(), + operation_date: toNullable(source.operationDate, (operationDate) => + operationDate.toPrimitive() + ), + language_code: source.languageCode.toPrimitive(), + currency_code: source.currencyCode.toPrimitive(), + + notes: toNullable(source.notes, (notes) => notes.toPrimitive()), + + subtotal_amount_value: allAmounts.subtotalAmount.value, + subtotal_amount_scale: allAmounts.subtotalAmount.scale, + + discount_percentage_value: source.discountPercentage.toPrimitive().value, + discount_percentage_scale: source.discountPercentage.toPrimitive().scale, + + discount_amount_value: allAmounts.discountAmount.value, + discount_amount_scale: allAmounts.discountAmount.scale, + + taxable_amount_value: allAmounts.taxableAmount.value, + taxable_amount_scale: allAmounts.taxableAmount.scale, + + taxes_amount_value: allAmounts.taxesAmount.value, + taxes_amount_scale: allAmounts.taxesAmount.scale, + + total_amount_value: allAmounts.totalAmount.value, + total_amount_scale: allAmounts.totalAmount.scale, + + customer_id: source.customerId.toPrimitive(), + + payment_method_id: toNullable(source.paymentMethod, (payment) => payment.toObjectString().id), + payment_method_description: toNullable( + source.paymentMethod, + (payment) => payment.toObjectString().payment_description + ), + }; + + // 5) Cliente / Recipient ?? + // Si es proforma no guardamos los campos como históricos (snapshots) + if (source.isProforma) { + Object.assign(invoiceValues, { customer_tin: null, customer_name: null, customer_street: null, @@ -359,48 +418,40 @@ export class CustomerInvoiceDomainMapper customer_province: null, customer_postal_code: null, customer_country: null, - }) - ) as any; + }); + } else { + const recipient = source.recipient.getOrUndefined(); + if (!recipient) { + return Result.fail( + new InfrastructureError( + "[CustomerInvoiceDomainMapper] Issued customer invoice w/o recipient data" + ) + ); + } - return { - id: source.id.toPrimitive(), - company_id: source.companyId.toPrimitive(), + Object.assign(invoiceValues, { + customer_tin: recipient.tin.toPrimitive(), + customer_name: recipient.name.toPrimitive(), + customer_street: toNullable(recipient.street, (v) => v.toPrimitive()), + customer_street2: toNullable(recipient.street2, (v) => v.toPrimitive()), + customer_city: toNullable(recipient.city, (v) => v.toPrimitive()), + customer_province: toNullable(recipient.province, (v) => v.toPrimitive()), + customer_postal_code: toNullable(recipient.postalCode, (v) => v.toPrimitive()), + customer_country: toNullable(recipient.country, (v) => v.toPrimitive()), + } as Partial); + } - status: source.status.toPrimitive(), - series: toNullable(source.series, (series) => series.toPrimitive()), + // 7) Si hubo errores de mapeo, devolvemos colección de validación + if (errors.length > 0) { + return Result.fail( + new ValidationErrorCollection("Customer invoice mapping to persistence failed", errors) + ); + } - invoice_number: source.invoiceNumber.toPrimitive(), - invoice_date: source.invoiceDate.toPrimitive(), - - operation_date: toNullable(source.operationDate, (operationDate) => - operationDate.toPrimitive() - ), - - notes: toNullable(source.notes, (notes) => notes.toPrimitive()), - - language_code: source.languageCode.code, - currency_code: source.currencyCode.code, - - subtotal_amount_value: 0, //subtotal.amount, - subtotal_amount_scale: 2, //subtotal.scale, - - discount_percentage_value: source.discountPercentage.value, - discount_percentage_scale: source.discountPercentage.scale, - - discount_amount_value: source.discountAmount.value, - discount_amount_scale: source.discountAmount.scale, - - taxable_amount_value: source.taxableAmount.value, - taxable_amount_scale: source.taxableAmount.scale, - - taxes_amount_value: source.taxAmount.value, - taxes_amount_scale: source.taxAmount.scale, - - total_amount_value: 0, //total.amount, - total_amount_scale: 2, //total.scale, - - items, - ...customer, - };*/ + return Result.ok({ + ...invoiceValues, + items: itemsResult.data, + taxes: taxesResult.data, + }); } } diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/domain/taxes.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/domain/invoice-taxes.mapper.ts similarity index 69% rename from modules/customer-invoices/src/api/infrastructure/mappers/domain/taxes.mapper.ts rename to modules/customer-invoices/src/api/infrastructure/mappers/domain/invoice-taxes.mapper.ts index 69dd472e..f3b32f42 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/domain/taxes.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/domain/invoice-taxes.mapper.ts @@ -7,7 +7,8 @@ import { } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import { CustomerInvoiceProps } from "../../../domain"; +import { InferCreationAttributes } from "sequelize"; +import { CustomerInvoice, CustomerInvoiceProps } from "../../../domain"; import { InvoiceTax } from "../../../domain/entities/invoice-taxes"; import { CustomerInvoiceTaxCreationAttributes, CustomerInvoiceTaxModel } from "../../sequelize"; @@ -63,10 +64,31 @@ export class TaxesDomainMapper extends SequelizeDomainMapper< return createResult; } - /*public mapToPersistence( + public mapToPersistence( source: InvoiceTax, params?: MapperParamsType - ): CustomerInvoiceTaxCreationAttributes { - throw new Error("not implemented"); - }*/ + ): Result, Error> { + const { errors, parent } = params as { + index: number; + parent: CustomerInvoice; + errors: ValidationErrorDetail[]; + }; + + + const taxableAmount = parent.getTaxableAmount() + const taxesAmount = + + return Result.ok({ + tax_id: source.id.toPrimitive(), + invoice_id: parent.id.toPrimitive(), + + tax_code: source.tax.code, + + taxable_amount_value: taxableAmount.value, + taxable_amount_scale: taxableAmount.scale, + + taxes_amount_value: taxesAmount.value, + taxes_amount_scale: taxesAmount.scale, + }); + } } diff --git a/modules/customer-invoices/src/api/infrastructure/mappers/domain/item-taxes.mapper.ts b/modules/customer-invoices/src/api/infrastructure/mappers/domain/item-taxes.mapper.ts index e68fbc09..872a450f 100644 --- a/modules/customer-invoices/src/api/infrastructure/mappers/domain/item-taxes.mapper.ts +++ b/modules/customer-invoices/src/api/infrastructure/mappers/domain/item-taxes.mapper.ts @@ -1,22 +1,37 @@ import { JsonTaxCatalogProvider } from "@erp/core"; -import { MapperParamsType, SequelizeDomainMapper, Tax } from "@erp/core/api"; +import { + ISequelizeDomainMapper, + MapperParamsType, + SequelizeDomainMapper, + Tax, +} from "@erp/core/api"; +import { CustomerInvoiceItem, ItemTax } from "@erp/customer-invoices/api/domain"; import { ValidationErrorCollection, ValidationErrorDetail, extractOrPushError, } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; -import { ItemTax } from "../../../domain"; import { - CustomerInvoiceItemCreationAttributes, + CustomerInvoiceItemTaxCreationAttributes, CustomerInvoiceItemTaxModel, } from "../../sequelize"; -export class ItemTaxesDomainMapper extends SequelizeDomainMapper< - CustomerInvoiceItemTaxModel, - CustomerInvoiceItemCreationAttributes, - ItemTax -> { +export interface IItemTaxesDomainMapper + extends ISequelizeDomainMapper< + CustomerInvoiceItemTaxModel, + CustomerInvoiceItemTaxCreationAttributes, + ItemTax + > {} + +export class ItemTaxesDomainMapper + extends SequelizeDomainMapper< + CustomerInvoiceItemTaxModel, + CustomerInvoiceItemTaxCreationAttributes, + ItemTax + > + implements IItemTaxesDomainMapper +{ private _taxCatalog!: JsonTaxCatalogProvider; constructor(params: MapperParamsType) { @@ -60,10 +75,29 @@ export class ItemTaxesDomainMapper extends SequelizeDomainMapper< return createResult; } - /*public mapToPersistence( + public mapToPersistence( source: ItemTax, params?: MapperParamsType - ): CustomerInvoiceItemCreationAttributes { - throw new Error("not implemented"); - }*/ + ): Result { + const { errors, parent } = params as { + parent: CustomerInvoiceItem; + errors: ValidationErrorDetail[]; + }; + + const taxableAmount = parent.getTaxableAmount(); + const taxAmount = source.getTaxAmount(taxableAmount); + + return Result.ok({ + tax_id: source.id.toPrimitive(), + item_id: parent.id.toPrimitive(), + + tax_code: source.tax.code, + + taxable_amount_value: taxableAmount.value, + taxable_amount_scale: taxableAmount.scale, + + taxes_amount_value: taxAmount.value, + taxes_amount_scale: taxAmount.scale, + }); + } } diff --git a/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice.model.ts b/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice.model.ts index 32e49bbb..cf3ba4f6 100644 --- a/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice.model.ts +++ b/modules/customer-invoices/src/api/infrastructure/sequelize/models/customer-invoice.model.ts @@ -40,6 +40,7 @@ export class CustomerInvoiceModel extends Model< declare operation_date: string; declare language_code: string; declare currency_code: string; + //declare xxxxxx declare notes: string; diff --git a/modules/doc-numbering/package.json b/modules/doc-numbering/package.json new file mode 100644 index 00000000..2be280e0 --- /dev/null +++ b/modules/doc-numbering/package.json @@ -0,0 +1,23 @@ +{ + "name": "@erp/doc-numbering", + "version": "0.0.1", + "main": "src/index.ts", + "types": "src/index.ts", + "exports": { + "./api": "./src/api/index.ts" + }, + "peerDependencies": { + "sequelize": "^6.37.5", + "express": "^4.18.2", + "zod": "^4.1.11" + }, + "devDependencies": { "@types/express": "^4.17.21" }, + "dependencies": { + "@erp/auth": "workspace:*", + "@erp/core": "workspace:*", + "@repo/rdx-ddd": "workspace:*", + "@repo/rdx-criteria": "workspace:*", + "@repo/rdx-utils": "workspace:*", + "@repo/rdx-logger": "workspace:*" + } +} diff --git a/modules/doc-numbering/src/api/domain/aggregates/doc-number.ts b/modules/doc-numbering/src/api/domain/aggregates/doc-number.ts new file mode 100644 index 00000000..e83dd12b --- /dev/null +++ b/modules/doc-numbering/src/api/domain/aggregates/doc-number.ts @@ -0,0 +1,43 @@ +import { AggregateRoot, UniqueID, UtcDate } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; +import { DocType } from "../value-objects"; + +export interface DocNumberProps { + docType: DocType; // INVOICE, QUOTATION, DELIVERY_NOTE, PAYMENT... + series: string; // opcional: "2025", "Sucursal-01" + currentValue: number; + formatPattern: string; // ej: "{year}/{number:000000}" + lastAssignedAt: UtcDate; +} + +export class DocNumber extends AggregateRoot { + static create(props: DocNumberProps, id?: UniqueID): Result { + const docNumber = new DocNumber(props, id); + + // Reglas de negocio / validaciones + // ... + // ... + + // 🔹 Disparar evento de dominio "CustomerAuthenticatedEvent" + //const { contact } = props; + //user.addDomainEvent(new CustomerAuthenticatedEvent(id, contact.toString())); + + return Result.ok(docNumber); + } + + public get docType(): DocType { + return this.props.docType; + } + + public get series(): string { + return this.props.series; + } + + public get currentValue(): number { + return this.props.currentValue; + } + + public get formatPattern(): string { + return this.props.formatPattern; + } +} diff --git a/modules/doc-numbering/src/api/domain/aggregates/index.ts b/modules/doc-numbering/src/api/domain/aggregates/index.ts new file mode 100644 index 00000000..d32cbec2 --- /dev/null +++ b/modules/doc-numbering/src/api/domain/aggregates/index.ts @@ -0,0 +1 @@ +export * from "./doc-number"; diff --git a/modules/doc-numbering/src/api/domain/entities/index.ts b/modules/doc-numbering/src/api/domain/entities/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/modules/doc-numbering/src/api/domain/index.ts b/modules/doc-numbering/src/api/domain/index.ts new file mode 100644 index 00000000..2c5c423d --- /dev/null +++ b/modules/doc-numbering/src/api/domain/index.ts @@ -0,0 +1,5 @@ +export * from "./aggregates"; +export * from "./entities"; +export * from "./repositories"; +export * from "./services"; +export * from "./value-objects"; diff --git a/modules/doc-numbering/src/api/domain/repositories/doc-number-repository.interface.ts b/modules/doc-numbering/src/api/domain/repositories/doc-number-repository.interface.ts new file mode 100644 index 00000000..8c68841d --- /dev/null +++ b/modules/doc-numbering/src/api/domain/repositories/doc-number-repository.interface.ts @@ -0,0 +1,12 @@ +import { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; +import { DocNumber } from "../aggregates/doc-number"; + +export interface IDocNumberingRepository { + getByReferenceInCompany( + companyId: UniqueID, + reference: string, + transaction?: any + ): Promise>; + save(reference: string, transaction?: any): Promise>; +} diff --git a/modules/doc-numbering/src/api/domain/repositories/index.ts b/modules/doc-numbering/src/api/domain/repositories/index.ts new file mode 100644 index 00000000..b9705864 --- /dev/null +++ b/modules/doc-numbering/src/api/domain/repositories/index.ts @@ -0,0 +1 @@ +export * from "./doc-number-repository.interface"; diff --git a/modules/doc-numbering/src/api/domain/value-objects/doc-type.ts b/modules/doc-numbering/src/api/domain/value-objects/doc-type.ts new file mode 100644 index 00000000..2e17b65b --- /dev/null +++ b/modules/doc-numbering/src/api/domain/value-objects/doc-type.ts @@ -0,0 +1,136 @@ +import { ValueObject, translateZodValidationError } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; +import { z } from "zod/v4"; + +// 🔹 Conjunto canónico de tipos admitidos en todo el ERP +export const DOCUMENT_TYPE_CODES = [ + "INVOICE", + "QUOTATION", + "DELIVERY_NOTE", + "PAYMENT", + "CREDIT_NOTE", + "RECEIPT", + "PURCHASE_ORDER", +] as const; + +export type DocTypeCode = (typeof DOCUMENT_TYPE_CODES)[number]; + +// 🔹 Alias comunes (entrada libre) → código canónico +const ALIASES: Record = { + // facturas + INVOICE: "INVOICE", + FACTURA: "INVOICE", + "FACTURA-RECTIFICATIVA": "CREDIT_NOTE", + FACTURA_RECTIFICATIVA: "CREDIT_NOTE", + "CREDIT-NOTE": "CREDIT_NOTE", + CREDIT_NOTE: "CREDIT_NOTE", + ABONO: "CREDIT_NOTE", + + // presupuestos + QUOTATION: "QUOTATION", + QUOTE: "QUOTATION", + PRESUPUESTO: "QUOTATION", + + // albaranes + "DELIVERY-NOTE": "DELIVERY_NOTE", + DELIVERY_NOTE: "DELIVERY_NOTE", + ALBARAN: "DELIVERY_NOTE", + ALBARÁN: "DELIVERY_NOTE", + + // pagos / cobros + PAYMENT: "PAYMENT", + PAGO: "PAYMENT", + RECEIPT: "RECEIPT", + RECIBO: "RECEIPT", + + // pedidos + "PURCHASE-ORDER": "PURCHASE_ORDER", + PURCHASE_ORDER: "PURCHASE_ORDER", + "ORDEN-DE-COMPRA": "PURCHASE_ORDER", + ORDEN_DE_COMPRA: "PURCHASE_ORDER", +}; + +// 🔹 Normaliza texto a forma comparable: mayúsculas, sin acentos, separadores como "" +function normalizeToken(input: string): string { + return ( + input + .trim() + .toUpperCase() + // eliminar diacríticos (tildes) + .normalize("NFD") + .replace(/\p{Diacritic}/gu, "") + // espacios/guiones → guion bajo + .replace(/[\s-]+/g, "") + ); +} + +interface DocTypeProps { + value: DocTypeCode; +} + +export class DocType extends ValueObject { + // Validación de código canónico con zod + protected static validateCanonical(value: string) { + const schema = z.enum(DOCUMENT_TYPE_CODES); + return schema.safeParse(value); + } + + // Intenta mapear la entrada libre a un código canónico + protected static canonicalize(input: string): Result { + const token = normalizeToken(input); + const fromAlias = ALIASES[token]; + const candidate = (fromAlias ?? token) as string; + + const parsed = DocType.validateCanonical(candidate); + if (!parsed.success) { + return Result.fail(translateZodValidationError("DocType creation failed", parsed.error)); + } + return Result.ok(parsed.data); + } + + /** + * Factoría desde entrada libre (admite alias). + * Devuelve Result con el VO o el error de validación. + */ + static create(input: string) { + const canon = DocType.canonicalize(input); + if (canon.isFailure) { + return Result.fail(canon.error); + } + return Result.ok(new DocType({ value: canon.data })); + } + + /** + * Factoría directa para código canónico ya validado. + * Útil en mappers desde persistencia. + */ + static from(code: DocTypeCode): DocType { + return new DocType({ value: code }); + } + + /** + * Lista de tipos canónicos soportados (para UI/validadores externos). + */ + static list(): readonly DocTypeCode[] { + return DOCUMENT_TYPE_CODES; + } + + /** + * ¿Pertenece a alguno de los tipos indicados? + */ + isOneOf(...types: DocTypeCode[]): boolean { + return types.includes(this.props.value); + } + + getProps(): DocTypeCode { + return this.props.value; + } + + toPrimitive(): DocTypeCode { + return this.getProps(); + } + + toString(): string { + return this.getProps(); + } +} diff --git a/modules/doc-numbering/src/api/domain/value-objects/index.ts b/modules/doc-numbering/src/api/domain/value-objects/index.ts new file mode 100644 index 00000000..b82a7101 --- /dev/null +++ b/modules/doc-numbering/src/api/domain/value-objects/index.ts @@ -0,0 +1 @@ +export * from "./doc-type"; diff --git a/modules/doc-numbering/src/api/index.ts b/modules/doc-numbering/src/api/index.ts new file mode 100644 index 00000000..7ef6d69b --- /dev/null +++ b/modules/doc-numbering/src/api/index.ts @@ -0,0 +1,29 @@ +import { IModuleServer, ModuleParams } from "@erp/core/api"; +import { models } from "./infrastructure"; + +export * from "./infrastructure/sequelize"; + +export const docNumberingAPIModule: IModuleServer = { + name: "customers", + version: "1.0.0", + dependencies: [], + + async init(params: ModuleParams) { + const { logger } = params; + logger.info("🚀 Document Numbering module initialized", { label: this.name }); + }, + async registerDependencies(params) { + const { logger } = params; + logger.info("🚀 Document Numbering module dependencies registered", { + label: this.name, + }); + return { + models, + services: { + /*...*/ + }, + }; + }, +}; + +export default docNumberingAPIModule; diff --git a/modules/doc-numbering/src/api/infrastructure/indes.ts b/modules/doc-numbering/src/api/infrastructure/indes.ts new file mode 100644 index 00000000..e69de29b diff --git a/modules/doc-numbering/src/api/infrastructure/index.ts b/modules/doc-numbering/src/api/infrastructure/index.ts new file mode 100644 index 00000000..2af2dbff --- /dev/null +++ b/modules/doc-numbering/src/api/infrastructure/index.ts @@ -0,0 +1 @@ +export * from ""; diff --git a/modules/doc-numbering/src/api/infrastructure/sequelize/index.ts b/modules/doc-numbering/src/api/infrastructure/sequelize/index.ts new file mode 100644 index 00000000..1b51346d --- /dev/null +++ b/modules/doc-numbering/src/api/infrastructure/sequelize/index.ts @@ -0,0 +1,2 @@ +export * from "./models"; +export * from "./repositories"; diff --git a/modules/doc-numbering/src/api/infrastructure/sequelize/models/doc-number.ts b/modules/doc-numbering/src/api/infrastructure/sequelize/models/doc-number.ts new file mode 100644 index 00000000..a22b9d63 --- /dev/null +++ b/modules/doc-numbering/src/api/infrastructure/sequelize/models/doc-number.ts @@ -0,0 +1,74 @@ +import { DataTypes, InferAttributes, InferCreationAttributes, Model, Sequelize } from "sequelize"; + +export type DocNumberCreationAttributes = InferCreationAttributes & {}; + +export class DocNumberModel extends Model< + InferAttributes, + InferCreationAttributes +> { + declare id: string; // UUID + declare docType: string; // ej. "INVOICE" + declare series: string | null; // ej. "2025", "Sucursal-01" + declare currentValue: number; // último número asignado + declare formatPattern: string; // ej. "{series}/{number:000000}" + + static associate(database: Sequelize) {} + + static hooks(database: Sequelize) {} +} + +export default (database: Sequelize) => { + DocNumberModel.init( + { + id: { + type: DataTypes.UUID, + primaryKey: true, + }, + docType: { + type: DataTypes.STRING(), + allowNull: false, + }, + series: { + type: DataTypes.STRING(), + allowNull: true, + defaultValue: null, + }, + currentValue: { + type: DataTypes.INTEGER.UNSIGNED, + allowNull: false, + defaultValue: 0, + }, + formatPattern: { + type: DataTypes.STRING(), + allowNull: false, + defaultValue: "{series}/{number:000000}", + }, + }, + { + sequelize: database, + tableName: "doc-numbers", + + underscored: true, + paranoid: false, // no softs deletes + timestamps: true, + + createdAt: "created_at", + updatedAt: "updated_at", + deletedAt: "deleted_at", + + indexes: [ + { + unique: true, + fields: ["docType", "series"], + }, + ], + + whereMergeStrategy: "and", // <- cómo tratar el merge de un scope + + defaultScope: {}, + + scopes: {}, + } + ); + return DocNumberModel; +}; diff --git a/modules/doc-numbering/src/api/infrastructure/sequelize/models/index.ts b/modules/doc-numbering/src/api/infrastructure/sequelize/models/index.ts new file mode 100644 index 00000000..d32cbec2 --- /dev/null +++ b/modules/doc-numbering/src/api/infrastructure/sequelize/models/index.ts @@ -0,0 +1 @@ +export * from "./doc-number"; diff --git a/modules/doc-numbering/src/api/infrastructure/sequelize/repositories/doc-number.repository.ts b/modules/doc-numbering/src/api/infrastructure/sequelize/repositories/doc-number.repository.ts new file mode 100644 index 00000000..9e00c244 --- /dev/null +++ b/modules/doc-numbering/src/api/infrastructure/sequelize/repositories/doc-number.repository.ts @@ -0,0 +1,41 @@ +import { EntityNotFoundError, SequelizeRepository, translateSequelizeError } from "@erp/core/api"; + +import { UniqueID } from "@repo/rdx-ddd"; +import { Result } from "@repo/rdx-utils"; +import { Transaction } from "sequelize"; +import { DocNumber, IDocNumberingRepository } from "../../../domain"; +import { DocNumberModel } from "../models"; + +export class DocNumberRepository + extends SequelizeRepository + implements IDocNumberingRepository +{ + async getByReferenceInCompany( + companyId: UniqueID, + reference: string, + transaction?: any + ): Promise> { + try { + const mapper: IDocNumberDomainMapper = this._registry.getDomainMapper({ + resource: "doc-number", + }); + + const row = await DocNumberModel.findOne({ + where: { reference, company_id: companyId.toString() }, + transaction, + }); + + if (!row) { + return Result.fail(new EntityNotFoundError("Customer", "id", id.toString())); + } + + const customer = mapper.mapToDomain(row); + return customer; + } catch (error: any) { + return Result.fail(translateSequelizeError(error)); + } + } + save(reference: string, transaction?: Transaction): Promise> { + throw new Error("Method not implemented."); + } +} diff --git a/modules/doc-numbering/src/api/infrastructure/sequelize/repositories/index.ts b/modules/doc-numbering/src/api/infrastructure/sequelize/repositories/index.ts new file mode 100644 index 00000000..e69de29b diff --git a/modules/doc-numbering/tsconfig.json b/modules/doc-numbering/tsconfig.json new file mode 100644 index 00000000..b4a95fde --- /dev/null +++ b/modules/doc-numbering/tsconfig.json @@ -0,0 +1,33 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@erp/customer-invoices/*": ["./src/*"] + }, + + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf46d81c..509a2335 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -344,8 +344,8 @@ importers: specifier: ^6.37.5 version: 6.37.7(mysql2@3.14.1) zod: - specifier: ^4.1.11 - version: 4.1.11 + specifier: ^3.25.67 + version: 3.25.67 devDependencies: '@biomejs/biome': specifier: 1.9.4 @@ -663,6 +663,40 @@ importers: specifier: ^3.2.4 version: 3.2.4(@types/debug@4.1.12)(@types/node@24.0.3)(jiti@2.4.2)(less@4.3.0)(lightningcss@1.30.1)(sass@1.89.0)(stylus@0.62.0)(terser@5.40.0)(tsx@4.19.4) + modules/document-numbering: + dependencies: + '@erp/auth': + specifier: workspace:* + version: link:../auth + '@erp/core': + specifier: workspace:* + version: link:../core + '@repo/rdx-criteria': + specifier: workspace:* + version: link:../../packages/rdx-criteria + '@repo/rdx-ddd': + specifier: workspace:* + version: link:../../packages/rdx-ddd + '@repo/rdx-logger': + specifier: workspace:* + version: link:../../packages/rdx-logger + '@repo/rdx-utils': + specifier: workspace:* + version: link:../../packages/rdx-utils + express: + specifier: ^4.18.2 + version: 4.21.2 + sequelize: + specifier: ^6.37.5 + version: 6.37.7(mysql2@3.14.1) + zod: + specifier: ^4.1.11 + version: 4.1.11 + devDependencies: + '@types/express': + specifier: ^4.17.21 + version: 4.17.23 + modules/verifactu: dependencies: '@erp/auth':