UNPKG

@invoiceddd/application

Version:

Application layer for the InvoiceDDD system - use cases and application services

890 lines (887 loc) 34.5 kB
import { Context, Data, Layer, Effect, Queue, Option, Fiber } from 'effect'; import { InvoiceNumberFormatValidator, PDFRenderer, FinancialCalculationService, ShippingPolicy, InvoiceAggregate, InvoiceNumberConflictError as InvoiceNumberConflictError$1 } from '@invoiceddd/domain'; var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var InvoiceRepository = class extends Context.Tag("@/applayer/ports/InvoiceRepository")() { static { __name(this, "InvoiceRepository"); } }; var OrderRepository = class extends Context.Tag("@/applayer/ports/OrderRepository")() { static { __name(this, "OrderRepository"); } }; var InvoiceNumberRepository = class extends Context.Tag("@/applayer/ports/InvoiceNumberRepository")() { static { __name(this, "InvoiceNumberRepository"); } }; var EventBusError = class extends Data.TaggedError("@/EventBus/EventBusError") { static { __name(this, "EventBusError"); } }; var EventBus = Context.GenericTag("@/EventBus"); var EventBusLive = Layer.effect(EventBus, Effect.gen(function* () { const eventQueue = yield* Queue.unbounded(); const subscriptions = /* @__PURE__ */ new Map(); const subscriptionsByType = /* @__PURE__ */ new Map(); let isRunning = false; let processingFiber = Option.none(); const implementation = { publish: /* @__PURE__ */ __name((event) => Effect.gen(function* () { if (!isRunning) { return yield* Effect.fail(new EventBusError({ message: "EventBus is not running. Call start() first.", eventType: event.type })); } yield* Queue.offer(eventQueue, event).pipe(Effect.mapError((cause) => new EventBusError({ message: `Failed to publish event: ${event.type}`, eventType: event.type, cause }))); }), "publish"), subscribe: /* @__PURE__ */ __name((eventType, handler) => Effect.gen(function* () { const subscriptionId = crypto.randomUUID(); const subscription = { eventType, handler, subscriptionId }; subscriptions.set(subscriptionId, subscription); if (!subscriptionsByType.has(eventType)) { subscriptionsByType.set(eventType, /* @__PURE__ */ new Set()); } subscriptionsByType.get(eventType)?.add(subscription); yield* Effect.log(`[EventBus] Registered subscriber for event: ${eventType}`); return subscriptionId; }).pipe(Effect.mapError((cause) => new EventBusError({ message: `Failed to subscribe to event: ${eventType}`, eventType, cause }))), "subscribe"), unsubscribe: /* @__PURE__ */ __name((subscriptionId) => Effect.gen(function* () { const subscription = subscriptions.get(subscriptionId); if (!subscription) { return yield* Effect.fail(new EventBusError({ message: `Subscription not found: ${subscriptionId}` })); } subscriptions.delete(subscriptionId); const typeSubscriptions = subscriptionsByType.get(subscription.eventType); if (typeSubscriptions) { typeSubscriptions.delete(subscription); if (typeSubscriptions.size === 0) { subscriptionsByType.delete(subscription.eventType); } } yield* Effect.log(`[EventBus] Unsubscribed from event: ${subscription.eventType}`); }).pipe(Effect.mapError((cause) => new EventBusError({ message: `Failed to unsubscribe: ${subscriptionId}`, cause }))), "unsubscribe"), start: Effect.gen(function* () { if (isRunning) { yield* Effect.log("[EventBus] Already running"); return; } isRunning = true; const processEvents = Effect.gen(function* () { while (isRunning) { const event = yield* Queue.take(eventQueue); const eventSubscriptions = subscriptionsByType.get(event.type) || /* @__PURE__ */ new Set(); if (eventSubscriptions.size === 0) { yield* Effect.log(`[EventBus] No subscribers for event type: ${event.type}`); continue; } const handlers = Array.from(eventSubscriptions).map((subscription) => Effect.sync(() => subscription.handler(event)).pipe(Effect.flatMap((effect) => effect), Effect.tapError((error) => { return Effect.logError(`[EventBus] Handler error for event ${event.type}:`, error); }), Effect.catchAll(() => { return Effect.void; }))); yield* Effect.all(handlers, { concurrency: "unbounded" }); yield* Effect.log(`[EventBus] Successfully processed event: ${event.type} with ${handlers.length} handlers`); } }).pipe(Effect.forever, Effect.forkDaemon); const fiber = yield* processEvents; processingFiber = Option.some(fiber); yield* Effect.log("[EventBus] Started event processing"); }).pipe(Effect.mapError((cause) => new EventBusError({ message: "Failed to start EventBus", cause }))), stop: Effect.succeed(void 0).pipe(Effect.flatMap(() => Effect.gen(function* () { if (!isRunning) { yield* Effect.log("[EventBus] Already stopped"); return; } isRunning = false; if (Option.isSome(processingFiber)) { yield* Fiber.interrupt(processingFiber.value); } subscriptions.clear(); subscriptionsByType.clear(); processingFiber = Option.none(); yield* Effect.log("[EventBus] Stopped event processing"); })), Effect.mapError((cause) => new EventBusError({ message: "Failed to stop EventBus", cause }))) }; return EventBus.of(implementation); })); var EventPublishError = class extends Data.TaggedError("@/EventPublisher/EventPublishError") { static { __name(this, "EventPublishError"); } }; var EventPublisher = Context.GenericTag("@/EventPublisherTag"); var EventPublisherLive = Layer.effect(EventPublisher, Effect.gen(function* () { const eventBus = yield* EventBus; const implementation = { publish: /* @__PURE__ */ __name((event) => { return eventBus.publish(event).pipe(Effect.tap(() => Effect.log(`[EventPublisher] Published event: ${event.type}`)), Effect.tapError((error) => { return Effect.logError(`[EventPublisher] Failed to publish event ${event.type}:`, error); }), Effect.catchAll(() => { return Effect.void; })); }, "publish") }; return EventPublisher.of(implementation); })); var InvoiceNumberGenerationError = class extends Data.TaggedError("@/InvoiceNumberGenerationError") { static { __name(this, "InvoiceNumberGenerationError"); } }; var InvoiceNumberGenerator = Context.GenericTag("@/InvoiceNumberGenerator"); var InvoiceConfigTag = Context.GenericTag("@/InvoiceConfig"); var InvoiceNumberGeneratorLive = Layer.effect(InvoiceNumberGenerator, Effect.gen(function* () { const repository = yield* InvoiceNumberRepository; const invoiceConfig = yield* InvoiceConfigTag; const implementation = { generate: /* @__PURE__ */ __name(() => { return Effect.gen(function* () { const nextNumber = yield* findNextIncrementalNumber(repository, invoiceConfig); const invoiceNumber = formatInvoiceNumber(nextNumber, invoiceConfig); const exists = yield* repository.existsByInvoiceNumber(invoiceNumber); if (exists) { return yield* Effect.fail(new InvoiceNumberGenerationError({ message: `Generated invoice number '${invoiceNumber}' already exists. This should not happen.` })); } yield* Effect.annotateCurrentSpan({ generatedInvoiceNumber: invoiceNumber, nextNumber, prefix: invoiceConfig.prefix }); return invoiceNumber; }).pipe(Effect.withSpan("InvoiceNumberGenerator.generate")); }, "generate") }; return InvoiceNumberGenerator.of(implementation); })); var formatInvoiceNumber = /* @__PURE__ */ __name((number, config) => { const paddedNumber = number.toString().padStart(4, "0"); if (config.prefix) { return `${config.prefix}-${paddedNumber}`; } return paddedNumber; }, "formatInvoiceNumber"); var findNextIncrementalNumber = /* @__PURE__ */ __name((repository, config) => { return Effect.gen(function* () { let candidate = 1001; const maxAttempts = 1e3; let attempts = 0; while (attempts < maxAttempts) { const candidateNumber = formatInvoiceNumber(candidate, config); const exists = yield* repository.existsByInvoiceNumber(candidateNumber); if (!exists) { yield* Effect.annotateCurrentSpan({ foundAvailableNumber: candidate, attempts: attempts + 1 }); return candidate; } candidate++; attempts++; } return yield* Effect.fail(new InvoiceNumberGenerationError({ message: `Could not find available invoice number after ${maxAttempts} attempts. Starting from ${candidate - maxAttempts}.` })); }).pipe(Effect.withSpan("InvoiceNumberGenerator.findNextIncrementalNumber")); }, "findNextIncrementalNumber"); var InvoiceNumberConflictError = class extends Data.TaggedError("@/InvoiceNumberConflictError") { static { __name(this, "InvoiceNumberConflictError"); } }; var InvoiceNumberUniqueChecker = Context.GenericTag("@/InvoiceNumberUniqueChecker"); var InvoiceNumberUniqueCheckerLive = Layer.effect(InvoiceNumberUniqueChecker, Effect.gen(function* () { const repository = yield* InvoiceNumberRepository; const implementation = { validate: /* @__PURE__ */ __name((invoiceNumber) => { return Effect.gen(function* () { const exists = yield* repository.existsByInvoiceNumber(invoiceNumber); if (exists) { yield* Effect.annotateCurrentSpan({ isUnique: false }); return yield* Effect.fail(new InvoiceNumberConflictError({ message: `Invoice number '${invoiceNumber}' already exists`, affectedInvoiceNumber: invoiceNumber })); } yield* Effect.annotateCurrentSpan({ isUnique: true }); return true; }).pipe(Effect.withSpan("InvoiceNumberUniqueChecker.validate", { attributes: { invoiceNumber } })); }, "validate") }; return InvoiceNumberUniqueChecker.of(implementation); })); // src/use-cases/CreateInvoiceUseCase.ts var InvoiceCreationError = class extends Data.TaggedError("@/CreateInvoiceUseCase/InvoiceCreationError") { static { __name(this, "InvoiceCreationError"); } }; var PDFGenerationFailedError = class extends Data.TaggedError("@/CreateInvoiceUseCase/PDFGenerationFailedError") { static { __name(this, "PDFGenerationFailedError"); } }; var CreateInvoiceUseCase = class extends Context.Tag("@/UseCases/CreateInvoiceUseCase")() { static { __name(this, "CreateInvoiceUseCase"); } }; var CreateInvoiceUseCaseLive = Layer.effect(CreateInvoiceUseCase, Effect.gen(function* () { const invoiceRepository = yield* InvoiceRepository; const invoiceNumberValidator = yield* InvoiceNumberFormatValidator; const invoiceNumberChecker = yield* InvoiceNumberUniqueChecker; const invoiceNumberGenerator = yield* InvoiceNumberGenerator; const pdfRenderer = yield* PDFRenderer; const eventPublisher = yield* EventPublisher; const financialCalculationService = yield* FinancialCalculationService; const shippingPolicy = yield* ShippingPolicy; const implementation = { execute: /* @__PURE__ */ __name((input) => { return Effect.gen(function* () { const invoiceNumber = yield* validateOrGenerateInvoiceNumber(input.invoiceNumber, invoiceNumberValidator, invoiceNumberChecker, invoiceNumberGenerator); const invoiceId = crypto.randomUUID(); const createdOn = /* @__PURE__ */ new Date(); const invoiceData = { invoiceId, orderId: input.orderId, invoiceNumber, customerSnapshot: input.customerSnapshot, itemsSnapshot: [ ...input.itemsSnapshot ], financials: input.financials, createdOn, createdBy: input.createdBy }; const invoice = yield* InvoiceAggregate.create(invoiceData).pipe(Effect.provide(Layer.mergeAll(Layer.succeed(FinancialCalculationService, financialCalculationService), Layer.succeed(ShippingPolicy, shippingPolicy), Layer.succeed(InvoiceNumberFormatValidator, invoiceNumberValidator))), Effect.mapError((error) => new InvoiceCreationError({ message: `Invoice validation failed: ${error.message}`, orderId: input.orderId, invoiceNumber, cause: error }))); const pdfBytes = yield* generateInvoicePDF(invoice, pdfRenderer, input.pdfRenderOptions); const finalInvoiceData = { ...invoiceData, pdfBytes }; const finalInvoice = yield* InvoiceAggregate.create(finalInvoiceData).pipe(Effect.provide(Layer.mergeAll(Layer.succeed(FinancialCalculationService, financialCalculationService), Layer.succeed(ShippingPolicy, shippingPolicy), Layer.succeed(InvoiceNumberFormatValidator, invoiceNumberValidator))), Effect.mapError((error) => new InvoiceCreationError({ message: `Final invoice creation failed: ${error.message.slice(0, error.message.length >= 100 ? 100 : error.message.length)}`, orderId: input.orderId, invoiceNumber, cause: error }))); yield* invoiceRepository.save(finalInvoice.data).pipe(Effect.mapError((error) => new InvoiceCreationError({ message: `Failed to save invoice: ${error.message}`, orderId: input.orderId, invoiceNumber, cause: error }))); const invoiceCreatedEvent = { type: "InvoiceCreated", eventId: crypto.randomUUID(), occurredOn: createdOn, version: 1, aggregateType: "Invoice", aggregateId: invoiceId, correlationId: crypto.randomUUID(), invoiceId, invoiceNumber, orderId: input.orderId, customerId: extractCustomerId(input.customerSnapshot), createdBy: input.createdBy, grandTotal: { amount: input.financials.grandTotal.amount, currency: input.financials.grandTotal.currency.toString() }, pdfBytes, legalInfo: { complianceStandard: "PDF/A-3", germanTaxCompliant: true, zugferdReady: true } }; yield* eventPublisher.publish(invoiceCreatedEvent); return { invoice: finalInvoice, pdfBytes, invoiceCreatedEvent }; }).pipe(Effect.withSpan("UseCases.CreateInvoice.execute", { attributes: { orderId: input.orderId, invoiceNumber: input.invoiceNumber, createdBy: input.createdBy, grandTotal: input.financials.grandTotal.amount } })); }, "execute") }; return CreateInvoiceUseCase.of(implementation); })); var validateOrGenerateInvoiceNumber = /* @__PURE__ */ __name((providedNumber, validator, checker, generator) => { if (providedNumber) { return Effect.gen(function* () { yield* validator.validate(providedNumber); yield* checker.validate(providedNumber); return providedNumber; }).pipe( // Map RepositoryError to InvoiceNumberConflictError for type compatibility Effect.mapError((error) => { if (error._tag === "@/RepositoryError") { return new InvoiceNumberConflictError$1({ message: `Repository error during uniqueness check: ${error.message}`, affectedInvoiceNumber: providedNumber, cause: error }); } return error; }) ); } else { return Effect.gen(function* () { const generatedNumber = yield* generator.generate(); yield* Effect.annotateCurrentSpan({ generatedInvoiceNumber: generatedNumber, wasProvided: false }); return generatedNumber; }).pipe(Effect.catchTags({ "@/RepositoryError": /* @__PURE__ */ __name((error) => new InvoiceNumberGenerationError({ message: `Repository error during invoice number generation: ${error.message}`, cause: error }), "@/RepositoryError") })); } }, "validateOrGenerateInvoiceNumber"); var generateInvoicePDF = /* @__PURE__ */ __name((invoice, pdfRenderer, renderOptions) => { return Effect.gen(function* () { const defaultLegalInfo = pdfRenderer.getDefaultGermanLegalInfo(); const defaultRenderOptions = pdfRenderer.getDefaultRenderOptions(defaultLegalInfo); const finalRenderOptions = renderOptions ? { ...defaultRenderOptions, ...renderOptions.customOptions, legalInfo: renderOptions.legalInfo ? { ...defaultLegalInfo, ...renderOptions.legalInfo } : defaultLegalInfo } : defaultRenderOptions; const pdfData = { invoice: invoice.data, renderOptions: finalRenderOptions }; return yield* pdfRenderer.render(pdfData); }).pipe(Effect.mapError((error) => new PDFGenerationFailedError({ message: `PDF generation failed: ${error.message}`, invoiceId: invoice.invoiceId, invoiceNumber: invoice.invoiceNumber, cause: error }))); }, "generateInvoicePDF"); var extractCustomerId = /* @__PURE__ */ __name((customer) => { return customer.email; }, "extractCustomerId"); var OrderNotFoundError = class extends Data.TaggedError("@/OrderNotFoundError") { static { __name(this, "OrderNotFoundError"); } }; var InvoiceDraftCreationError = class extends Data.TaggedError("@/InvoiceDraftCreationError") { static { __name(this, "InvoiceDraftCreationError"); } }; var RequestInvoiceUseCase = class extends Context.Tag("@/UseCases/RequestInvoiceUseCase")() { static { __name(this, "RequestInvoiceUseCase"); } }; var RequestInvoiceUseCaseLive = Layer.effect(RequestInvoiceUseCase, Effect.gen(function* () { const orderRepository = yield* OrderRepository; const eventPublisher = yield* EventPublisher; const implementation = { execute: /* @__PURE__ */ __name((input) => { return Effect.gen(function* () { const orderOption = yield* orderRepository.findById(input.orderId); if (orderOption._tag === "None") { return yield* Effect.fail(new OrderNotFoundError({ message: `Order with ID ${input.orderId} not found`, orderId: input.orderId })); } const order = orderOption.value; const requestedAt = /* @__PURE__ */ new Date(); const invoiceDraft = yield* createInvoiceDraftFromOrder(order, input.requestedBy, requestedAt); const event = { type: "InvoiceDraftRequested", eventId: crypto.randomUUID(), occurredOn: requestedAt, version: 1, aggregateType: "Invoice", aggregateId: input.orderId, correlationId: crypto.randomUUID(), orderId: input.orderId, requestedBy: input.requestedBy, requestedAt, orderSnapshot: { customerInfo: serializeCustomerInfo(order.customer), items: order.items.map(serializeOrderItem), financials: serializeFinancials(order) } }; yield* eventPublisher.publish(event); return invoiceDraft; }).pipe(Effect.mapError((error) => { if (error._tag === "@/OrderNotFoundError") { return error; } return new InvoiceDraftCreationError({ message: `Failed to create invoice draft: ${String(error)}`, orderId: input.orderId, cause: error }); }), Effect.withSpan("UseCases.RequestInvoice.execute", { attributes: { orderId: input.orderId, requestedBy: input.requestedBy } })); }, "execute") }; return RequestInvoiceUseCase.of(implementation); })); var createInvoiceDraftFromOrder = /* @__PURE__ */ __name((order, requestedBy, requestedAt) => { return Effect.gen(function* () { const customerSnapshot = { firstName: order.customer.firstName, lastName: order.customer.lastName, street: order.customer.street, zip: order.customer.zip, city: order.customer.city, country: order.customer.country, email: order.customer.email, phone: order.customer.phone }; const itemsSnapshot = order.items.map((item) => ({ name: item.name, productNumber: item.productNumber, variant: item.variant, unitPrice: item.unitPrice, quantity: item.quantity, linePrice: item.linePrice })); const financials = { subtotal: order.subtotal, taxes: order.taxes, shippingCost: order.shippingCost, discount: order.discount, grandTotal: order.grandTotal }; yield* Effect.annotateCurrentSpan({ finished: true }); return { orderId: order.orderId, customerSnapshot, itemsSnapshot, financials, requestedBy, requestedAt }; }).pipe(Effect.mapError((error) => { return new InvoiceDraftCreationError({ message: `Failed to transform order to invoice draft: ${error}`, orderId: order.orderId, cause: error }); }), Effect.withSpan("UseCases.RequestInvoice.createInvoiceDraftFromOrder", { attributes: { orderId: order.orderId, requestedBy, requestedAt } })); }, "createInvoiceDraftFromOrder"); var serializeCustomerInfo = /* @__PURE__ */ __name((customer) => ({ firstName: customer.firstName, lastName: customer.lastName, street: customer.street, zip: customer.zip, city: customer.city, country: customer.country, email: customer.email, phone: customer.phone }), "serializeCustomerInfo"); var serializeOrderItem = /* @__PURE__ */ __name((item) => ({ name: item.name, productNumber: item.productNumber, variant: item.variant, unitPrice: { amount: item.unitPrice.amount, currency: item.unitPrice.currency }, quantity: item.quantity, linePrice: { amount: item.linePrice.amount, currency: item.linePrice.currency } }), "serializeOrderItem"); var serializeFinancials = /* @__PURE__ */ __name((order) => ({ subtotal: { amount: order.subtotal.amount, currency: order.subtotal.currency }, taxes: { amount: order.taxes.amount, currency: order.taxes.currency }, shippingCost: { amount: order.shippingCost.amount, currency: order.shippingCost.currency }, discount: { amount: order.discount.amount, currency: order.discount.currency }, grandTotal: { amount: order.grandTotal.amount, currency: order.grandTotal.currency } }), "serializeFinancials"); var GetInvoiceError = class extends Data.TaggedError("@/GetInvoiceUseCase/GetInvoiceError") { static { __name(this, "GetInvoiceError"); } }; var InvoiceNotFoundError = class extends Data.TaggedError("@/GetInvoiceUseCase/InvoiceNotFoundError") { static { __name(this, "InvoiceNotFoundError"); } }; var InvalidSearchCriteriaError = class extends Data.TaggedError("@/GetInvoiceUseCase/InvalidSearchCriteriaError") { static { __name(this, "InvalidSearchCriteriaError"); } }; var GetInvoiceUseCase = class extends Context.Tag("@/UseCases/GetInvoiceUseCase")() { static { __name(this, "GetInvoiceUseCase"); } }; var GetInvoiceUseCaseLive = Layer.effect(GetInvoiceUseCase, Effect.gen(function* () { const invoiceRepository = yield* InvoiceRepository; const invoiceNumberValidator = yield* InvoiceNumberFormatValidator; const implementation = { execute: /* @__PURE__ */ __name((input) => { return Effect.gen(function* () { yield* validateSearchCriteria(input, invoiceNumberValidator); const invoiceOption = yield* searchInvoice(input, invoiceRepository); if (Option.isNone(invoiceOption)) { return yield* Effect.fail(createNotFoundError(input)); } const invoice = invoiceOption.value; const invoiceAggregate = InvoiceAggregate.fromValidatedData(invoice); let output = { invoice: invoiceAggregate }; if (input.returnType === "pdf") { if (!invoice.pdfBytes) { return yield* Effect.fail(new GetInvoiceError({ message: "Wanted to get PDF data for an invoice, but no PDF available. Badummz", cause: invoice })); } output = { ...output, pdfBytes: invoice.pdfBytes }; } return output; }).pipe(Effect.mapError((error) => { if (error._tag === "@/GetInvoiceUseCase/InvoiceNotFoundError" || error._tag === "@/GetInvoiceUseCase/InvalidSearchCriteriaError") { return error; } if (error._tag === "@/RepositoryError") { return error; } return new GetInvoiceError({ message: `Failed to get invoice: ${String(error)}`, cause: error }); }), Effect.withSpan("UseCases.GetInvoice.execute", { attributes: { orderId: input.orderId, invoiceNumber: input.invoiceNumber, returnType: input.returnType ?? "json" } })); }, "execute") }; return GetInvoiceUseCase.of(implementation); })); var validateSearchCriteria = /* @__PURE__ */ __name((input, validator) => { return Effect.gen(function* () { if (!input.orderId && !input.invoiceNumber) { return yield* Effect.fail(new InvalidSearchCriteriaError({ message: "Must provide either orderId or invoiceNumber" })); } if (input.orderId && input.invoiceNumber) { return yield* Effect.fail(new InvalidSearchCriteriaError({ message: "Cannot provide both orderId and invoiceNumber. Choose one search criterion.", providedCriteria: `orderId: ${input.orderId}, invoiceNumber: ${input.invoiceNumber}` })); } if (input.orderId) { if (input.orderId.trim().length === 0) { return yield* Effect.fail(new InvalidSearchCriteriaError({ message: "Order ID cannot be empty", providedCriteria: `orderId: "${input.orderId}"` })); } } if (input.invoiceNumber) { yield* validator.validate(input.invoiceNumber).pipe(Effect.mapError((error) => new InvalidSearchCriteriaError({ message: `Invalid invoice number format: ${error.message}`, providedCriteria: `invoiceNumber: "${input.invoiceNumber}"` }))); } if (input.returnType && ![ "json", "pdf" ].includes(input.returnType)) { return yield* Effect.fail(new InvalidSearchCriteriaError({ message: `Invalid return type. Must be 'json' or 'pdf', but got '${input.returnType}'`, providedCriteria: `returnType: "${input.returnType}"` })); } }); }, "validateSearchCriteria"); var searchInvoice = /* @__PURE__ */ __name((input, repository) => { if (input.orderId) { return repository.findByOrderId(input.orderId); } if (input.invoiceNumber) { return repository.findByInvoiceNumber(input.invoiceNumber); } return Effect.succeed(Option.none()); }, "searchInvoice"); var createNotFoundError = /* @__PURE__ */ __name((input) => { if (input.orderId) { return new InvoiceNotFoundError({ message: `Invoice not found for order ID: ${input.orderId}`, searchValue: input.orderId, searchType: "orderId" }); } if (input.invoiceNumber) { return new InvoiceNotFoundError({ message: `Invoice not found for invoice number: ${input.invoiceNumber}`, searchValue: input.invoiceNumber, searchType: "invoiceNumber" }); } return new InvoiceNotFoundError({ message: "Invoice not found", searchValue: "unknown", searchType: "orderId" }); }, "createNotFoundError"); var ListInvoicesError = class extends Data.TaggedError("@/ListInvoicesUseCase/ListInvoicesError") { static { __name(this, "ListInvoicesError"); } }; var InvalidPaginationError = class extends Data.TaggedError("@/ListInvoicesUseCase/InvalidPaginationError") { static { __name(this, "InvalidPaginationError"); } }; var InvalidFiltersError = class extends Data.TaggedError("@/ListInvoicesUseCase/InvalidFiltersError") { static { __name(this, "InvalidFiltersError"); } }; var ListInvoicesUseCase = class extends Context.Tag("@/UseCases/ListInvoicesUseCase")() { static { __name(this, "ListInvoicesUseCase"); } }; var ListInvoicesUseCaseLive = Layer.effect(ListInvoicesUseCase, Effect.gen(function* () { const invoiceRepository = yield* InvoiceRepository; const invoiceNumberValidator = yield* InvoiceNumberFormatValidator; const implementation = { execute: /* @__PURE__ */ __name((input) => { return Effect.gen(function* () { const pagination = yield* validateAndNormalizePagination(input.pagination); const filters = yield* validateAndNormalizeFilters(input.filters, invoiceNumberValidator); const paginatedResult = yield* invoiceRepository.list(filters, pagination).pipe(Effect.mapError((error) => new ListInvoicesError({ message: `Failed to list invoices: ${error.message}`, cause: error }))); const invoiceAggregates = paginatedResult.invoices.map((invoice) => InvoiceAggregate.fromValidatedData(invoice)); const hasNextPage = pagination.page < paginatedResult.totalPages; const hasPrevPage = pagination.page > 1; yield* Effect.annotateCurrentSpan({ hasNextPage, hasPrevPage, resultCount: invoiceAggregates.length }); return { invoices: invoiceAggregates, totalCount: paginatedResult.totalCount, page: paginatedResult.page, limit: paginatedResult.limit, totalPages: paginatedResult.totalPages, hasNextPage, hasPrevPage }; }).pipe(Effect.withSpan("UseCases.ListInvoices.execute", { attributes: { filters: JSON.stringify(input.filters), pagination: JSON.stringify(input.pagination) } })); }, "execute") }; return ListInvoicesUseCase.of(implementation); })); var validateAndNormalizePagination = /* @__PURE__ */ __name((pagination) => { return Effect.gen(function* () { const page = pagination?.page ?? 1; const limit = pagination?.limit ?? 20; if (page < 1) { return yield* Effect.fail(new InvalidPaginationError({ message: "Page number must be greater than 0", page })); } if (limit < 1) { return yield* Effect.fail(new InvalidPaginationError({ message: "Limit must be greater than 0", limit })); } if (limit > 100) { return yield* Effect.fail(new InvalidPaginationError({ message: "Limit cannot exceed 100 items", limit })); } return { page, limit }; }); }, "validateAndNormalizePagination"); var validateAndNormalizeFilters = /* @__PURE__ */ __name((filters, validator) => { return Effect.gen(function* () { const normalizedFilters = {}; if (filters?.invoiceNumber && validator) { yield* validator.validate(filters.invoiceNumber).pipe(Effect.mapError((error) => new InvalidFiltersError({ message: `Invalid invoice number format: ${error.message}`, invalidField: "invoiceNumber" }))); Object.assign(normalizedFilters, { invoiceNumber: filters.invoiceNumber.trim() }); } if (filters?.dateFrom) { if (filters.dateFrom > /* @__PURE__ */ new Date()) { return yield* Effect.fail(new InvalidFiltersError({ message: "Date from cannot be in the future", invalidField: "dateFrom" })); } Object.assign(normalizedFilters, { dateFrom: filters.dateFrom }); } if (filters?.dateTo) { if (filters.dateTo > /* @__PURE__ */ new Date()) { return yield* Effect.fail(new InvalidFiltersError({ message: "Date to cannot be in the future", invalidField: "dateTo" })); } Object.assign(normalizedFilters, { dateTo: filters.dateTo }); } if (filters?.dateFrom && filters?.dateTo && filters.dateFrom > filters.dateTo) { return yield* Effect.fail(new InvalidFiltersError({ message: "Date from must be before date to", invalidField: "dateRange" })); } if (filters?.minTotal !== void 0) { if (filters.minTotal < 0) { return yield* Effect.fail(new InvalidFiltersError({ message: "Minimum total cannot be negative", invalidField: "minTotal" })); } normalizedFilters.minTotal = filters.minTotal; } if (filters?.maxTotal !== void 0) { if (filters.maxTotal < 0) { return yield* Effect.fail(new InvalidFiltersError({ message: "Maximum total cannot be negative", invalidField: "maxTotal" })); } normalizedFilters.maxTotal = filters.maxTotal; } if (filters?.minTotal !== void 0 && filters?.maxTotal !== void 0 && filters.minTotal > filters.maxTotal) { return yield* Effect.fail(new InvalidFiltersError({ message: "Minimum total must be less than or equal to maximum total", invalidField: "totalRange" })); } if (filters?.createdBy) { normalizedFilters.createdBy = filters.createdBy.trim(); } return normalizedFilters; }); }, "validateAndNormalizeFilters"); export { CreateInvoiceUseCase, CreateInvoiceUseCaseLive, EventBus, EventBusError, EventBusLive, EventPublishError, EventPublisher, EventPublisherLive, GetInvoiceError, GetInvoiceUseCase, GetInvoiceUseCaseLive, InvalidFiltersError, InvalidPaginationError, InvalidSearchCriteriaError, InvoiceConfigTag, InvoiceCreationError, InvoiceDraftCreationError, InvoiceNotFoundError, InvoiceNumberConflictError, InvoiceNumberGenerationError, InvoiceNumberGenerator, InvoiceNumberGeneratorLive, InvoiceNumberRepository, InvoiceNumberUniqueChecker, InvoiceNumberUniqueCheckerLive, InvoiceRepository, ListInvoicesError, ListInvoicesUseCase, ListInvoicesUseCaseLive, OrderNotFoundError, OrderRepository, PDFGenerationFailedError, RequestInvoiceUseCase, RequestInvoiceUseCaseLive }; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map