UNPKG

@invoicing-sdk/application

Version:

Application layer for the invoicing system including use cases, ports, and application services

450 lines (443 loc) 14.3 kB
// src/use-cases/RequestInvoice.ts import { Invoice, InvoiceAlreadyExistsError, OrderNotFoundError } from "@invoicing-sdk/domain"; // src/use-cases/mappers/InvoiceDraftMapper.ts var InvoiceDraftMapper = class { /** * Converts InvoiceDraft domain entity to InvoiceDraftDTO * @param draft - The domain InvoiceDraft entity * @returns InvoiceDraftDTO suitable for presentation layer */ static toDTO(draft) { return { invoiceId: draft.invoiceId, invoiceNumber: draft.invoiceNumber, orderId: draft.orderId, customerSnapshot: { firstName: draft.customerSnapshot.firstName, lastName: draft.customerSnapshot.lastName, street: draft.customerSnapshot.street, zip: draft.customerSnapshot.zip, city: draft.customerSnapshot.city, country: draft.customerSnapshot.country, email: draft.customerSnapshot.email, phone: draft.customerSnapshot.phone }, itemsSnapshot: draft.itemsSnapshot.map((item) => ({ name: item.name, variant: item.variant, productNumber: item.productNumber, unitPrice: item.unitPrice.toPlainObject(), quantity: item.quantity, linePrice: item.linePrice.toPlainObject() })), financials: { subtotal: draft.financials.subtotal.toPlainObject(), taxes: draft.financials.taxes.toPlainObject(), shippingCost: draft.financials.shippingCost.toPlainObject(), discount: draft.financials.discount.toPlainObject(), grandTotal: draft.financials.grandTotal.toPlainObject() }, status: "DRAFT", createdOn: draft.createdOn.toISOString(), createdBy: draft.createdBy }; } }; // src/use-cases/RequestInvoice.ts function makeRequestInvoice(deps) { return async (command) => { const { orderRepository, invoiceRepository, invoiceNumberGenerator } = deps; const { orderId } = command; const order = await orderRepository.findById(orderId); if (!order) { throw new OrderNotFoundError(orderId); } const existingInvoice = await invoiceRepository.findByOrderId(orderId); if (existingInvoice) { throw new InvoiceAlreadyExistsError(orderId); } const financials = { subtotal: order.subtotal, taxes: order.taxes, shippingCost: order.shippingCost, discount: order.discount, grandTotal: order.subtotal.add(order.taxes).add(order.shippingCost).subtract(order.discount) }; const financialsPO = { subtotal: financials.subtotal.toPlainObject(), taxes: financials.taxes.toPlainObject(), shippingCost: financials.shippingCost.toPlainObject(), discount: financials.discount.toPlainObject(), grandTotal: financials.grandTotal.toPlainObject() }; const draft = await Invoice.createDraft( { orderId: order.orderId, customerSnapshot: order.customer, itemsSnapshot: order.items, financials, createdBy: command.requestedBy }, { invoiceNumberGenerator } ); await invoiceRepository.save(draft); const draftDTO = InvoiceDraftMapper.toDTO(draft); const result = { orderId: order.orderId, suggestedInvoiceNumber: draftDTO.invoiceNumber, customerInfo: draftDTO.customerSnapshot, items: draftDTO.itemsSnapshot, financials: draftDTO.financials, draft: draftDTO }; if (deps.eventPublisher) { draft.publishAllUncommitted(deps.eventPublisher); } return result; }; } function createRequestInvoiceUseCase(deps) { return makeRequestInvoice(deps); } // src/use-cases/IssueInvoice.ts import { Invoice as Invoice2, InvoiceDraftNotFoundError, InvoiceIntegrityCheckError, InvoiceStateViolationError, InvoiceStatus as InvoiceStatus2, OrderNotFoundError as OrderNotFoundError2, ShippingCost } from "@invoicing-sdk/domain"; function makeIssueInvoice(deps) { return async (command) => { const { orderRepository, invoiceRepository, invoiceNumberValidator, shippingRateProvider, shippingCostPolicy, pdfRenderer, legalInfo } = deps; const { invoiceNumber, orderId, issuedBy } = command; const order = await orderRepository.findById(orderId); if (!order) { throw new OrderNotFoundError2(orderId); } const existingInvoice = await invoiceRepository.findDraft({ by: "orderId", orderId }); if (!existingInvoice) { throw new InvoiceDraftNotFoundError(orderId); } if (!existingInvoice.requireStatus(InvoiceStatus2.DRAFT)) { throw new InvoiceStateViolationError( invoiceNumber, InvoiceStatus2.DRAFT, existingInvoice.getStatus() ); } if (existingInvoice.invoiceNumber !== invoiceNumber) { throw new InvoiceIntegrityCheckError( invoiceNumber, "INVOICE_NUMBER_MISMATCH", `Invoice draft has ${existingInvoice.invoiceNumber} but received ${invoiceNumber}` ); } const baseTotal = order.subtotal.add(order.taxes).subtract(order.discount); const shippingCost = ShippingCost.determine( baseTotal, shippingRateProvider, shippingCostPolicy ); const grandTotal = baseTotal.add(shippingCost); const financials = { subtotal: order.subtotal, taxes: order.taxes, shippingCost, discount: order.discount, grandTotal }; const invoiceDataForPdf = { invoiceId: "", // Will be set after invoice creation invoiceNumber, orderId, customer: order.customer, items: order.items, financials, createdOn: /* @__PURE__ */ new Date(), createdBy: issuedBy, legalInfo }; const pdfBytes = await pdfRenderer.render(invoiceDataForPdf); const invoice = await Invoice2.issue( { orderId, invoiceNumber, customerSnapshot: existingInvoice.customerSnapshot, itemsSnapshot: existingInvoice.itemsSnapshot, financials, createdBy: issuedBy, // explicit issuer attribution status: InvoiceStatus2.DRAFT }, pdfBytes, { invoiceNumberValidator } ); await invoiceRepository.save(invoice); if (deps.eventPublisher) { invoice.publishAllUncommitted(deps.eventPublisher); } const result = { invoiceId: invoice.invoiceId, invoiceNumber: invoice.invoiceNumber, orderId: invoice.orderId, grandTotal: invoice.financials.grandTotal.toPlainObject(), issuedOn: invoice.createdOn, issuedBy: invoice.createdBy }; return result; }; } function createIssueInvoiceUseCase(deps) { return makeIssueInvoice(deps); } // src/use-cases/CreateInvoice.ts import { Invoice as Invoice3, InvoiceDraftNotFoundError as InvoiceDraftNotFoundError2, InvoiceStateViolationError as InvoiceStateViolationError2, InvoiceStatus as InvoiceStatus3, OrderNotFoundError as OrderNotFoundError3, ShippingCost as ShippingCost2 } from "@invoicing-sdk/domain"; function makeCreateInvoice(deps) { return async (command) => { const { orderRepository, invoiceRepository, invoiceNumberValidator, shippingRateProvider, shippingCostPolicy, pdfRenderer, legalInfo } = deps; const { orderId, invoiceNumber, createdBy } = command; const order = await orderRepository.findById(orderId); if (!order) { throw new OrderNotFoundError3(orderId); } const existingInvoice = await invoiceRepository.findDraft({ by: "invoiceNumber", invoiceNumber }); if (!existingInvoice) { throw new InvoiceDraftNotFoundError2(orderId); } if (!existingInvoice.requireStatus(InvoiceStatus3.DRAFT)) { throw new InvoiceStateViolationError2( orderId, InvoiceStatus3.DRAFT, existingInvoice.getStatus() ); } const grandTotal = order.subtotal.add(order.taxes).add(order.shippingCost).subtract(order.discount); const shippingCost = ShippingCost2.determine( grandTotal, shippingRateProvider, shippingCostPolicy ); const financials = { subtotal: order.subtotal, taxes: order.taxes, shippingCost, discount: order.discount, grandTotal }; const invoiceDataForPdf = { invoiceId: "", // Will be set after invoice creation invoiceNumber, orderId, customer: order.customer, items: order.items, financials, createdOn: /* @__PURE__ */ new Date(), createdBy, legalInfo }; const pdfBytes = await pdfRenderer.render(invoiceDataForPdf); const invoice = await Invoice3.issue( { orderId, invoiceNumber, customerSnapshot: existingInvoice.customerSnapshot, itemsSnapshot: existingInvoice.itemsSnapshot, financials, createdBy, status: InvoiceStatus3.DRAFT // This is always the case after the check above }, pdfBytes, { invoiceNumberValidator } ); await invoiceRepository.save(invoice); if (deps.eventPublisher) { invoice.publishAllUncommitted(deps.eventPublisher); } const result = { invoiceId: invoice.invoiceId, invoiceNumber: invoice.invoiceNumber, orderId: invoice.orderId, grandTotal: invoice.financials.grandTotal.toPlainObject(), createdOn: invoice.createdOn, createdBy: invoice.createdBy }; return result; }; } function createCreateInvoiceUseCase(deps) { return makeCreateInvoice(deps); } // src/use-cases/ListInvoices.ts function makeListInvoices(deps) { return async (query) => { const { invoiceRepository } = deps; if (query.page < 1) { throw new Error("Page number must be greater than 0"); } if (query.limit < 1 || query.limit > 100) { throw new Error("Limit must be between 1 and 100"); } if (query.dateFrom && query.dateTo && query.dateFrom > query.dateTo) { throw new Error("dateFrom must be before or equal to dateTo"); } if (query.minTotal !== void 0 && query.maxTotal !== void 0 && query.minTotal > query.maxTotal) { throw new Error("minTotal must be less than or equal to maxTotal"); } const paginatedResult = await invoiceRepository.list(query); const result = { items: paginatedResult.items.map((invoice) => ({ invoiceId: invoice.invoiceId, invoiceNumber: invoice.invoiceNumber, orderId: invoice.orderId, customerName: `${invoice.customerSnapshot.firstName} ${invoice.customerSnapshot.lastName}`, grandTotal: invoice.financials.grandTotal.toPlainObject(), createdOn: invoice.createdOn, createdBy: invoice.createdBy, itemCount: invoice.getTotalItemCount() })), totalCount: paginatedResult.totalCount, page: paginatedResult.page, limit: paginatedResult.limit, hasNextPage: paginatedResult.hasNextPage, hasPreviousPage: paginatedResult.hasPreviousPage }; return result; }; } function createListInvoicesUseCase(deps) { return makeListInvoices(deps); } // src/use-cases/GetInvoice.ts function makeGetInvoice(deps) { return async (query) => { const { invoiceRepository } = deps; const { invoiceId, invoiceNumber, orderId, includePdf = false } = query; if (!invoiceId && !invoiceNumber && !orderId) { throw new Error("At least one of invoiceId, invoiceNumber, or orderId must be provided"); } let invoice = null; if (invoiceId) { invoice = await invoiceRepository.findById(invoiceId); } else if (invoiceNumber) { invoice = await invoiceRepository.findByInvoiceNumber(invoiceNumber); } else if (orderId) { invoice = await invoiceRepository.findByOrderId(orderId); } if (!invoice) { return null; } const result = { invoiceId: invoice.invoiceId, invoiceNumber: invoice.invoiceNumber, orderId: invoice.orderId, customerSnapshot: { firstName: invoice.customerSnapshot.firstName, lastName: invoice.customerSnapshot.lastName, street: invoice.customerSnapshot.street, zip: invoice.customerSnapshot.zip, city: invoice.customerSnapshot.city, country: invoice.customerSnapshot.country, email: invoice.customerSnapshot.email, phone: invoice.customerSnapshot.phone }, itemsSnapshot: invoice.itemsSnapshot.map((item) => ({ name: item.name, variant: item.variant, productNumber: item.productNumber, unitPrice: item.unitPrice.toPlainObject(), quantity: item.quantity, linePrice: item.linePrice.toPlainObject() })), financials: { subtotal: invoice.financials.subtotal.toPlainObject(), taxes: invoice.financials.taxes.toPlainObject(), shippingCost: invoice.financials.shippingCost.toPlainObject(), discount: invoice.financials.discount.toPlainObject(), grandTotal: invoice.financials.grandTotal.toPlainObject() }, createdOn: invoice.createdOn, createdBy: invoice.createdBy, // Include PDF bytes only if requested pdfBytes: includePdf ? invoice.pdfBytes : void 0 }; return result; }; } function createGetInvoiceUseCase(deps) { return makeGetInvoice(deps); } // src/events/event-bus.ts var EventBusError = class extends Error { constructor(message, cause) { super(message); this.cause = cause; this.name = "EventBusError"; } }; var EventPublishError = class extends EventBusError { constructor(eventType, cause) { super(`Failed to publish event: ${eventType}`, cause); this.name = "EventPublishError"; } }; var EventSubscriptionError = class extends EventBusError { constructor(eventType, cause) { super(`Failed to subscribe to event: ${eventType}`, cause); this.name = "EventSubscriptionError"; } }; export { EventBusError, EventPublishError, EventSubscriptionError, createCreateInvoiceUseCase, createGetInvoiceUseCase, createIssueInvoiceUseCase, createListInvoicesUseCase, createRequestInvoiceUseCase, makeCreateInvoice, makeGetInvoice, makeIssueInvoice, makeListInvoices, makeRequestInvoice }; //# sourceMappingURL=index.mjs.map