@invoiceddd/application
Version:
Application layer for the InvoiceDDD system - use cases and application services
890 lines (887 loc) • 34.5 kB
JavaScript
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