UNPKG

@invoiceddd/domain

Version:

Domain layer for the InvoiceDDD system - business logic, entities, and domain services

1,137 lines (1,128 loc) 45.1 kB
import { Context, Schema, Data, Layer, Effect } from 'effect'; var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var ShippingPolicy = class extends Context.Tag("ShippingPolicy")() { static { __name(this, "ShippingPolicy"); } }; var CurrencySchema = class _CurrencySchema extends Schema.TaggedClass("CurrencySchema")("CurrencySchema", { code: Schema.Union(Schema.Literal("EUR")), symbol: Schema.Union(Schema.Literal("\u20AC")) }) { static { __name(this, "CurrencySchema"); } static euro = new _CurrencySchema({ code: "EUR", symbol: "\u20AC" }); static quick(code, symbol) { return new _CurrencySchema({ code, symbol }); } }; // src/value-objects/Money.ts var hardCappedMaxMoneyValue = 1e7; var InvalidMoneyCurrencies = class extends Data.TaggedError("InvalidMoneyCurrencies") { static { __name(this, "InvalidMoneyCurrencies"); } }; var MoneySchema = class _MoneySchema extends Schema.TaggedClass("@@Money")("Money", { currency: CurrencySchema, amount: Schema.Number.pipe(Schema.greaterThanOrEqualTo(0, { message: /* @__PURE__ */ __name(({ actual: amount }) => `Amount must be non-negative, but is: ${amount}`, "message") }), Schema.lessThanOrEqualTo(hardCappedMaxMoneyValue, { message: /* @__PURE__ */ __name(({ actual }) => `You are too rich to be here. Eat the rich. ${JSON.stringify(actual)}`, "message") })) }) { static { __name(this, "MoneySchema"); } /** * Creates a new MoneySchema instance representing an amount in euros. * @param amount The amount in euros. * @returns A new MoneySchema instance. */ static EURO(amount) { return new _MoneySchema({ amount, currency: CurrencySchema.euro }); } static fromNumeric(amount, currency) { return new _MoneySchema({ amount, currency }); } add(other) { this.assertSameCurrency(other); return new _MoneySchema({ ...this, amount: this.amount + other.amount }); } addMultiple(...others) { let finalAmount = this.amount; for (const other of others) { this.assertSameCurrency(other); finalAmount += other.amount; } return new _MoneySchema({ currency: this.currency, amount: finalAmount }); } addNumeric(amount) { return { ...this, amount: this.amount + amount }; } subtract(other) { this.assertSameCurrency(other); return new _MoneySchema({ ...this, amount: this.amount - other.amount }); } subtractNumeric(amount) { return new _MoneySchema({ ...this, amount: this.amount - amount }); } multiply(factor) { return new _MoneySchema({ ...this, amount: this.amount * factor }); } /** * Divides the current amount by the given divisor. * * **NOTE**: because it's money, it ALWAYS rounds to two decimals! * @param divisor The divisor to divide the current amount * @returns */ divide(divisor) { return new _MoneySchema({ ...this, amount: this.roundTwoDecimalsRaw(this.amount / divisor) }); } roundTwoDecimals() { return new _MoneySchema({ ...this, amount: this.roundTwoDecimalsRaw(this.amount) }); } isLess(than, orEqual) { this.assertSameCurrency(than); if (orEqual === "orEqual") { return this.amount <= than.amount; } return this.amount < than.amount; } isLessNumeric(than, orEqual) { if (orEqual === "orEqual") { return this.amount <= than; } return this.amount < than; } isMore(than, orEqual) { this.assertSameCurrency(than); if (orEqual === "orEqual") { return this.amount >= than.amount; } return this.amount > than.amount; } isMoreNumeric(than, orEqual) { if (orEqual === "orEqual") { return this.amount >= than; } return this.amount > than; } assertSameCurrency(other) { if (this.currency.code !== other.currency.code) { throw new InvalidMoneyCurrencies({ given: this.currency, other: other.currency }); } } roundTwoDecimalsRaw(num) { return Math.round(num * 100) / 100; } }; // src/value-objects/InvoiceFinancials.ts function calculateGrandTotal(subtotal, taxes, shippingCost, discount) { const grandTotal = subtotal.addMultiple(taxes, shippingCost).subtract(discount); return grandTotal.roundTwoDecimals(); } __name(calculateGrandTotal, "calculateGrandTotal"); function validateGrandTotalCalculation(financials) { try { const expectedGrandTotal = calculateGrandTotal(financials.subtotal, financials.taxes, financials.shippingCost, financials.discount); const tolerance = 0.01; return Math.abs(financials.grandTotal.subtract(expectedGrandTotal).amount) <= tolerance; } catch (error) { console.error("Could not validate grand total", error instanceof Error ? error.message : String(error)); return false; } } __name(validateGrandTotalCalculation, "validateGrandTotalCalculation"); var InvoiceFinancialsSchema = Schema.Struct({ subtotal: MoneySchema, discount: MoneySchema, taxes: MoneySchema, shippingCost: MoneySchema, grandTotal: MoneySchema }).pipe(Schema.filter((financials) => { return validateGrandTotalCalculation(financials); }, { message: /* @__PURE__ */ __name(({ actual }) => { return `Grand total must equal subtotal + taxes + shipping cost - discount. Got ${JSON.stringify(actual)}`; }, "message") })); var createInvoiceFinancials = /* @__PURE__ */ __name((data) => ( // ): Schema.Schema.Encoded<typeof InvoiceFinancialsSchema> => Schema.decodeUnknownSync(InvoiceFinancialsSchema)(data) ), "createInvoiceFinancials"); var validateInvoiceFinancials = /* @__PURE__ */ __name((data) => Schema.decodeUnknown(InvoiceFinancialsSchema)(data), "validateInvoiceFinancials"); var createInvoiceFinancialsWithCalculation = /* @__PURE__ */ __name((data) => { const grandTotal = calculateGrandTotal(data.subtotal, data.taxes, data.shippingCost, data.discount); return createInvoiceFinancials({ ...data, grandTotal }); }, "createInvoiceFinancialsWithCalculation"); // src/services/FinancialCalculationService.ts var FinancialValidationError = class extends Data.TaggedError("FinancialValidationError") { static { __name(this, "FinancialValidationError"); } }; var FinancialCalculationService = class extends Context.Tag("FinancialCalculationService")() { static { __name(this, "FinancialCalculationService"); } }; var FinancialCalculationServiceLive = Layer.succeed(FinancialCalculationService, { calculateGrandTotal: /* @__PURE__ */ __name((subtotal, taxes, shippingCost, discount) => calculateGrandTotal(subtotal, taxes, shippingCost, discount), "calculateGrandTotal"), validateDiscountLimit: /* @__PURE__ */ __name((financials) => validateDiscountLimit(financials), "validateDiscountLimit"), validateGrandTotalCalculation: /* @__PURE__ */ __name((financials) => validateGrandTotalCalculation(financials), "validateGrandTotalCalculation"), validateInvoiceFinancialConsistency: /* @__PURE__ */ __name((financials, shippingPolicy) => { return Effect.gen(function* () { const isEligible = yield* shippingPolicy.isFreeShippingEligible(financials); if (isEligible && financials.shippingCost.amount !== 0) { return yield* Effect.fail(new FinancialValidationError({ violations: [ "FreeShippingPolicy" ], message: `Free shipping applies, but shipping costs are positive: ${financials.shippingCost.amount}` })); } const result = validateInvoiceFinancialConsistency(financials); return { isValid: result.isValid }; }); }, "validateInvoiceFinancialConsistency") }); var validateDiscountLimit = /* @__PURE__ */ __name((financials) => { const maxDiscount = financials.subtotal.addMultiple(financials.taxes, financials.shippingCost); return financials.discount.isLess(maxDiscount, "orEqual"); }, "validateDiscountLimit"); var validateInvoiceFinancialConsistency = /* @__PURE__ */ __name((financials) => { const violations = []; if (!validateDiscountLimit(financials)) { violations.push("Discount exceeds maximum allowable amount"); } if (!validateGrandTotalCalculation(financials)) { violations.push("Grand total calculation is incorrect"); } return { isValid: violations.length === 0, violations }; }, "validateInvoiceFinancialConsistency"); var InvoiceNumberFormatError = class extends Data.TaggedError("@/InvoiceNumberFormatError") { static { __name(this, "InvoiceNumberFormatError"); } }; var InvoiceNumberFormatValidator = class extends Context.Tag("@/InvoiceNumberFormatValidator")() { static { __name(this, "InvoiceNumberFormatValidator"); } }; var InvoiceConfigTag = Context.GenericTag("@/InvoiceConfig"); var InvoiceNumberFormatValidatorLive = Layer.effect(InvoiceNumberFormatValidator, Effect.gen(function* () { const invoiceConfig = yield* InvoiceConfigTag; const implementation = { validate: /* @__PURE__ */ __name((invoiceNumber) => { return Effect.gen(function* () { if (!invoiceNumber || invoiceNumber.trim().length === 0) { return yield* Effect.fail(new InvoiceNumberFormatError({ message: "Invoice number cannot be empty" })); } const trimmedNumber = invoiceNumber.trim(); const isValid = yield* validateInvoiceNumberFormat(trimmedNumber, invoiceConfig); if (!isValid) { const expectedFormats = getExpectedFormats(invoiceConfig); return yield* Effect.fail(new InvoiceNumberFormatError({ message: `Invalid invoice number format: '${invoiceNumber}'. Expected formats: ${expectedFormats}` })); } const numericPart = extractNumericPart(trimmedNumber); if (numericPart <= 0) { return yield* Effect.fail(new InvoiceNumberFormatError({ message: `Invalid invoice number: '${invoiceNumber}'. Numeric part must be greater than 0` })); } }); }, "validate") }; return InvoiceNumberFormatValidator.of(implementation); })); var validateInvoiceNumberFormat = /* @__PURE__ */ __name((invoiceNumber, config) => { if (/^\d+$/.test(invoiceNumber)) { return Effect.succeed(true); } if (!config.prefix) { return Effect.succeed(/^\d+$/.test(invoiceNumber)); } const prefixPattern = new RegExp(`^${escapeRegex(config.prefix)}-\\d+$`); return Effect.succeed(prefixPattern.test(invoiceNumber) || /^\d+$/.test(invoiceNumber)); }, "validateInvoiceNumberFormat"); var extractNumericPart = /* @__PURE__ */ __name((invoiceNumber) => { if (/^\d+$/.test(invoiceNumber)) { return parseInt(invoiceNumber, 10); } const parts = invoiceNumber.split("-"); if (parts.length > 1) { const numericPart = parts[parts.length - 1]; return parseInt(numericPart, 10); } return 0; }, "extractNumericPart"); var getExpectedFormats = /* @__PURE__ */ __name((config) => { if (!config.prefix) { return "numeric format (e.g., '1001')"; } return `numeric format (e.g., '1001') or prefixed format (e.g., '${config.prefix}-1001')`; }, "getExpectedFormats"); var escapeRegex = /* @__PURE__ */ __name((str) => { return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); }, "escapeRegex"); var CustomerInfoSchema = Schema.Struct({ firstName: Schema.String.pipe(Schema.nonEmptyString()), lastName: Schema.String.pipe(Schema.nonEmptyString()), street: Schema.String.pipe(Schema.nonEmptyString()), zip: Schema.String.pipe(Schema.nonEmptyString()), city: Schema.String.pipe(Schema.nonEmptyString()), country: Schema.String.pipe(Schema.nonEmptyString()), email: Schema.String.pipe(Schema.nonEmptyString(), Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, { message: /* @__PURE__ */ __name(() => "Invalid email format", "message") })), phone: Schema.optional(Schema.String.pipe(Schema.nonEmptyString())) }); var createCustomerInfo = /* @__PURE__ */ __name((data) => Schema.decodeUnknownSync(CustomerInfoSchema)(data), "createCustomerInfo"); var validateCustomerInfo = /* @__PURE__ */ __name((data) => Schema.decodeUnknown(CustomerInfoSchema)(data), "validateCustomerInfo"); var OrderItemSchema = Schema.Struct({ unitPrice: MoneySchema, quantity: Schema.Number.pipe(Schema.int(), Schema.greaterThan(0, { message: /* @__PURE__ */ __name(() => "Quantity must be a positive integer", "message") })), linePrice: MoneySchema, name: Schema.String.pipe(Schema.nonEmptyString()), variant: Schema.optional(Schema.String.pipe(Schema.nonEmptyString())), productNumber: Schema.optional(Schema.String.pipe(Schema.nonEmptyString())) }).pipe(Schema.filter((item) => { const expectedLinePrice = item.unitPrice.multiply(item.quantity); const tolerance = 0.01; return Math.abs(item.linePrice.subtract(expectedLinePrice).amount) <= tolerance; }, { message: /* @__PURE__ */ __name(() => "Line price must equal unit price multiplied by quantity", "message") })); var createOrderItem = /* @__PURE__ */ __name((data) => Schema.decodeUnknownSync(OrderItemSchema)(data), "createOrderItem"); var validateOrderItem = /* @__PURE__ */ __name((data) => Schema.decodeUnknown(OrderItemSchema)(data), "validateOrderItem"); var calculateLinePrice = /* @__PURE__ */ __name((unitPrice, quantity) => { return unitPrice.multiply(quantity).roundTwoDecimals(); }, "calculateLinePrice"); var createOrderItemWithCalculation = /* @__PURE__ */ __name((data) => { const linePrice = calculateLinePrice(data.unitPrice, data.quantity); return createOrderItem({ ...data, linePrice }); }, "createOrderItemWithCalculation"); // src/entities/Invoice.ts var InvoiceValidationError = class extends Data.TaggedError("@/InvoiceValidationError") { static { __name(this, "InvoiceValidationError"); } }; (class extends Data.TaggedError("@/InvoiceNumberFormatError") { static { __name(this, "InvoiceNumberFormatError"); } }); (class extends Data.TaggedError("@/InvoiceImmutabilityError") { static { __name(this, "InvoiceImmutabilityError"); } }); var InvoiceSchema = Schema.Struct({ invoiceId: Schema.String.pipe(Schema.nonEmptyString(), Schema.pattern(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, { message: /* @__PURE__ */ __name(() => "Invoice ID must be a valid UUID", "message") })), orderId: Schema.String.pipe(Schema.nonEmptyString()), invoiceNumber: Schema.String.pipe(Schema.nonEmptyString()), customerSnapshot: CustomerInfoSchema, itemsSnapshot: Schema.Array(OrderItemSchema).pipe(Schema.minItems(1, { message: /* @__PURE__ */ __name(() => "Invoice must contain at least one item", "message") })), financials: InvoiceFinancialsSchema, createdOn: Schema.DateFromSelf, createdBy: Schema.String.pipe(Schema.nonEmptyString()), // Use a direct schema for Uint8Array that accepts actual Uint8Array instances pdfBytes: Schema.optional(Schema.instanceOf(Uint8Array, { message: /* @__PURE__ */ __name(() => "PDF bytes must be a Uint8Array", "message") })) }).pipe(Schema.filter((invoice) => { const calculatedSubtotal = invoice.itemsSnapshot.reduce((sum, item) => sum.add(item.linePrice), new MoneySchema({ currency: CurrencySchema.euro, amount: 0 })); const tolerance = 0.01; return Math.abs(invoice.financials.subtotal.subtract(calculatedSubtotal).amount) <= tolerance; }, { message: /* @__PURE__ */ __name(() => "Invoice financials subtotal must match sum of item line prices", "message") }), Schema.filter((invoice) => validateGrandTotalCalculation(invoice.financials), { message: /* @__PURE__ */ __name(() => "Invoice financials must have correct grand total calculation", "message") })); var InvoiceAggregate = class _InvoiceAggregate { static { __name(this, "InvoiceAggregate"); } _data; // eslint-disable-next-line no-unused-vars constructor(_data) { this._data = _data; } /** * Factory method to create a new Invoice aggregate with full validation */ static create(data) { return Effect.gen(function* () { const validatedInvoice = yield* Schema.decodeUnknown(InvoiceSchema)(data); const invoiceNumberValidator = yield* InvoiceNumberFormatValidator; yield* invoiceNumberValidator.validate(validatedInvoice.invoiceNumber); const shippingPolicy = yield* ShippingPolicy; const financialValidator = yield* FinancialCalculationService; yield* financialValidator.validateInvoiceFinancialConsistency(validatedInvoice.financials, shippingPolicy); return new _InvoiceAggregate(validatedInvoice); }).pipe(Effect.mapError((error) => new InvoiceValidationError({ message: `Invoice validation failed: ${error.message}`, cause: error }))); } /** * Factory method to reconstruct an Invoice aggregate from validated data * (e.g., when loading from database) */ static fromValidatedData(data) { return new _InvoiceAggregate(data); } // Immutable getters for all properties get invoiceId() { return this._data.invoiceId; } get orderId() { return this._data.orderId; } get invoiceNumber() { return this._data.invoiceNumber; } get customerSnapshot() { return this._data.customerSnapshot; } get itemsSnapshot() { return this._data.itemsSnapshot; } get financials() { return this._data.financials; } get createdOn() { return this._data.createdOn; } get createdBy() { return this._data.createdBy; } get pdfBytes() { return this._data.pdfBytes; } /** * Get the complete invoice data (for serialization/persistence) */ get data() { return this._data; } /** * Business method: Check if invoice belongs to a specific order */ belongsToOrder(orderId) { return this._data.orderId === orderId; } /** * Business method: Check if invoice has a specific invoice number */ hasInvoiceNumber(invoiceNumber) { return this._data.invoiceNumber === invoiceNumber; } /** * Business method: Check if invoice was created by a specific user */ wasCreatedBy(userId) { return this._data.createdBy === userId; } /** * Business method: Check if invoice was created within a date range */ wasCreatedBetween(startDate, endDate) { return this._data.createdOn >= startDate && this._data.createdOn <= endDate; } /** * Business method: Check if invoice total is within a range */ hasTotalBetween(minTotal, maxTotal) { return this._data.financials.grandTotal.isMoreNumeric(minTotal) && this._data.financials.grandTotal.isLessNumeric(maxTotal); } /** * Business method: Get invoice age in days */ getAgeInDays() { const now = /* @__PURE__ */ new Date(); const diffTime = Math.abs(now.getTime() - this._data.createdOn.getTime()); return Math.ceil(diffTime / (1e3 * 60 * 60 * 24)); } /** * Business method: Check if invoice contains a specific product */ containsProduct(productNumber) { return this._data.itemsSnapshot.some((item) => item.productNumber === productNumber); } /** * Business method: Get total quantity of all items */ getTotalQuantity() { return this._data.itemsSnapshot.reduce((total, item) => total + item.quantity, 0); } /** * Business method: Get customer email for notifications */ getCustomerEmail() { return this._data.customerSnapshot.email; } /** * Business method: Get formatted invoice display name */ getDisplayName() { return `Invoice ${this._data.invoiceNumber} for Order ${this._data.orderId}`; } /** * Equality comparison based on invoice ID */ equals(other) { return this._data.invoiceId === other._data.invoiceId; } /** * String representation for debugging */ toString() { return `Invoice(id=${this._data.invoiceId}, number=${this._data.invoiceNumber}, orderId=${this._data.orderId})`; } }; var validateInvoice = /* @__PURE__ */ __name((data) => Schema.decodeUnknown(InvoiceSchema)(data), "validateInvoice"); var OrderSchema = Schema.Struct({ orderId: Schema.String.pipe(Schema.nonEmptyString()), createdOn: Schema.DateFromSelf, shippingCost: MoneySchema, items: Schema.Array(OrderItemSchema).pipe(Schema.minItems(1, { message: /* @__PURE__ */ __name(() => "Order must contain at least one item", "message") })), customer: CustomerInfoSchema, subtotal: MoneySchema, discount: MoneySchema, taxes: MoneySchema, grandTotal: MoneySchema }).pipe(Schema.filter((order) => validateGrandTotalCalculation(order), { message: /* @__PURE__ */ __name(() => "Grand total must equal subtotal + taxes + shipping cost - discount", "message") }), Schema.filter((order) => { const calculatedSubtotal = order.items.reduce((sum, item) => item.linePrice.amount + sum, 0); const tolerance = 0.01; return Math.abs(order.subtotal.subtractNumeric(calculatedSubtotal).amount) <= tolerance; }, { message: /* @__PURE__ */ __name(() => "Subtotal must equal the sum of all item line prices", "message") })); var createOrder = /* @__PURE__ */ __name((data) => Schema.decodeUnknownSync(OrderSchema)(data), "createOrder"); var validateOrder = /* @__PURE__ */ __name((data) => Schema.decodeUnknown(OrderSchema)(data), "validateOrder"); var calculateSubtotal = /* @__PURE__ */ __name((items) => { const subtotal = items.reduce((sum, item) => item.linePrice.add(sum), new MoneySchema({ currency: CurrencySchema.euro, amount: 0 })); return subtotal.roundTwoDecimals(); }, "calculateSubtotal"); var createOrderWithCalculations = /* @__PURE__ */ __name((data) => { const subtotal = calculateSubtotal(data.items); const grandTotal = calculateGrandTotal(subtotal, data.taxes, data.shippingCost, data.discount); return createOrder({ ...data, subtotal, grandTotal }); }, "createOrderWithCalculations"); var PDFGenerationError = class extends Data.TaggedError("@/PDFGenerationError") { static { __name(this, "PDFGenerationError"); } }; var PDFComplianceError = class extends Data.TaggedError("@/PDFComplianceError") { static { __name(this, "PDFComplianceError"); } }; var GermanLegalInfoSchema = Schema.Struct({ // Contact information companyName: Schema.String.pipe(Schema.nonEmptyString()), companyAddress: Schema.Struct({ street: Schema.String.pipe(Schema.nonEmptyString()), zip: Schema.String.pipe(Schema.nonEmptyString()), city: Schema.String.pipe(Schema.nonEmptyString()), country: Schema.String.pipe(Schema.nonEmptyString()) }), phone: Schema.String.pipe(Schema.nonEmptyString()), email: Schema.String, website: Schema.optional(Schema.String), // Legal information managingDirector: Schema.String.pipe(Schema.nonEmptyString()), commercialRegister: Schema.String.pipe(Schema.nonEmptyString()), commercialRegisterNumber: Schema.String.pipe(Schema.nonEmptyString()), jurisdiction: Schema.String.pipe(Schema.nonEmptyString()), // Tax information vatId: Schema.String.pipe(Schema.nonEmptyString(), Schema.pattern(/^DE\d{9}$/)), taxNumber: Schema.String.pipe(Schema.nonEmptyString()), // Bank information bankDetails: Schema.Struct({ bankName: Schema.String.pipe(Schema.nonEmptyString()), iban: Schema.String.pipe(Schema.nonEmptyString(), Schema.pattern(/^DE\d{20}$/)), bic: Schema.String.pipe(Schema.nonEmptyString(), Schema.pattern(/^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$/)) }) }); var PDFRenderOptionsSchema = Schema.Struct({ // PDF/A-3 compliance settings pdfACompliance: Schema.Literal("PDF/A-3"), embedFonts: Schema.Boolean, colorProfile: Schema.String, // German legal requirements legalInfo: GermanLegalInfoSchema, // ZUGfERD/Factur-X preparation includeXMLAttachment: Schema.Boolean, xmlStandard: Schema.Union(Schema.Literal("ZUGfERD"), Schema.Literal("Factur-X")), // Layout and formatting language: Schema.Literal("de"), currency: Schema.Literal("EUR"), dateFormat: Schema.Literal("DD.MM.YYYY"), // Optional customization logoBytes: Schema.optional(Schema.Uint8Array), customFooter: Schema.optional(Schema.String) }); var InvoicePDFDataSchema = Schema.Struct({ invoice: InvoiceSchema, renderOptions: PDFRenderOptionsSchema }); var PDFRenderer = class extends Context.Tag("@/domain/services/PDFRenderer")() { static { __name(this, "PDFRenderer"); } }; var DomainEventSchema = Schema.Struct({ type: Schema.String.pipe(Schema.nonEmptyString()), eventId: Schema.String.pipe(Schema.nonEmptyString(), Schema.pattern(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, { message: /* @__PURE__ */ __name(() => "Event ID must be a valid UUID", "message") })), occurredOn: Schema.DateFromSelf, version: Schema.Number.pipe(Schema.int(), Schema.greaterThanOrEqualTo(1, { message: /* @__PURE__ */ __name(() => "Event version must be a positive integer", "message") })), aggregateType: Schema.String.pipe(Schema.nonEmptyString()), aggregateId: Schema.String.pipe(Schema.nonEmptyString()), correlationId: Schema.optional(Schema.String.pipe(Schema.nonEmptyString())), causationId: Schema.optional(Schema.String.pipe(Schema.nonEmptyString())) }); var InvoiceCreatedSchema = Schema.Struct({ // Base domain event fields type: Schema.Literal("InvoiceCreated"), eventId: Schema.String.pipe(Schema.nonEmptyString(), Schema.pattern(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, { message: /* @__PURE__ */ __name(() => "Event ID must be a valid UUID", "message") })), occurredOn: Schema.DateFromSelf, version: Schema.Number.pipe(Schema.int(), Schema.greaterThanOrEqualTo(1, { message: /* @__PURE__ */ __name(() => "Event version must be a positive integer", "message") })), aggregateType: Schema.String.pipe(Schema.nonEmptyString()), aggregateId: Schema.String.pipe(Schema.nonEmptyString()), correlationId: Schema.optional(Schema.String.pipe(Schema.nonEmptyString())), causationId: Schema.optional(Schema.String.pipe(Schema.nonEmptyString())), // InvoiceCreated specific fields invoiceId: Schema.String.pipe(Schema.nonEmptyString(), Schema.pattern(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, { message: /* @__PURE__ */ __name(() => "Invoice ID must be a valid UUID", "message") })), invoiceNumber: Schema.String.pipe(Schema.nonEmptyString(), Schema.pattern(/^(INV-\d+|\d+)$/, { message: /* @__PURE__ */ __name(() => "Invoice number must follow format INV-1001 or 1221", "message") })), orderId: Schema.String.pipe(Schema.nonEmptyString()), customerId: Schema.String.pipe(Schema.nonEmptyString()), createdBy: Schema.String.pipe(Schema.nonEmptyString()), grandTotal: Schema.Struct({ amount: Schema.Number.pipe(Schema.greaterThanOrEqualTo(0, { message: /* @__PURE__ */ __name(() => "Grand total amount must be non-negative", "message") })), currency: Schema.String.pipe(Schema.nonEmptyString(), Schema.pattern(/^[A-Z]{3}$/, { message: /* @__PURE__ */ __name(() => "Currency must be a 3-letter ISO code", "message") })) }), pdfBytes: Schema.Uint8ArrayFromSelf, legalInfo: Schema.Struct({ complianceStandard: Schema.Literal("PDF/A-3"), germanTaxCompliant: Schema.Boolean, zugferdReady: Schema.Boolean }) }); var InvoiceDraftRequestedSchema = Schema.Struct({ // Base domain event fields type: Schema.Literal("InvoiceDraftRequested"), eventId: Schema.String.pipe(Schema.nonEmptyString(), Schema.pattern(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, { message: /* @__PURE__ */ __name(() => "Event ID must be a valid UUID", "message") })), occurredOn: Schema.DateFromSelf, version: Schema.Number.pipe(Schema.int(), Schema.greaterThanOrEqualTo(1, { message: /* @__PURE__ */ __name(() => "Event version must be a positive integer", "message") })), aggregateType: Schema.String.pipe(Schema.nonEmptyString()), aggregateId: Schema.String.pipe(Schema.nonEmptyString()), correlationId: Schema.optional(Schema.String.pipe(Schema.nonEmptyString())), causationId: Schema.optional(Schema.String.pipe(Schema.nonEmptyString())), // InvoiceDraftRequested specific fields orderId: Schema.String.pipe(Schema.nonEmptyString()), requestedBy: Schema.String.pipe(Schema.nonEmptyString()), requestedAt: Schema.DateFromSelf, orderSnapshot: Schema.Struct({ customerInfo: Schema.Record({ key: Schema.String, value: Schema.Unknown }), items: Schema.Array(Schema.Record({ key: Schema.String, value: Schema.Unknown })), financials: Schema.Record({ key: Schema.String, value: Schema.Unknown }) }) }); var InvoiceCreationFailedSchema = Schema.Struct({ // Base domain event fields type: Schema.Literal("InvoiceCreationFailed"), eventId: Schema.String.pipe(Schema.nonEmptyString(), Schema.pattern(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, { message: /* @__PURE__ */ __name(() => "Event ID must be a valid UUID", "message") })), occurredOn: Schema.DateFromSelf, version: Schema.Number.pipe(Schema.int(), Schema.greaterThanOrEqualTo(1, { message: /* @__PURE__ */ __name(() => "Event version must be a positive integer", "message") })), aggregateType: Schema.String.pipe(Schema.nonEmptyString()), aggregateId: Schema.String.pipe(Schema.nonEmptyString()), correlationId: Schema.optional(Schema.String.pipe(Schema.nonEmptyString())), causationId: Schema.optional(Schema.String.pipe(Schema.nonEmptyString())), // InvoiceCreationFailed specific fields orderId: Schema.String.pipe(Schema.nonEmptyString()), attemptedInvoiceNumber: Schema.optional(Schema.String.pipe(Schema.nonEmptyString())), failureReason: Schema.String.pipe(Schema.nonEmptyString()), failureCategory: Schema.Literal("validation", "technical", "business"), retryable: Schema.Boolean, attemptedBy: Schema.String.pipe(Schema.nonEmptyString()), errorDetails: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })) }); var InvoicePDFUploadedSchema = Schema.Struct({ // Base domain event fields type: Schema.Literal("InvoicePDFUploaded"), eventId: Schema.String.pipe(Schema.nonEmptyString(), Schema.pattern(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, { message: /* @__PURE__ */ __name(() => "Event ID must be a valid UUID", "message") })), occurredOn: Schema.DateFromSelf, version: Schema.Number.pipe(Schema.int(), Schema.greaterThanOrEqualTo(1, { message: /* @__PURE__ */ __name(() => "Event version must be a positive integer", "message") })), aggregateType: Schema.String.pipe(Schema.nonEmptyString()), aggregateId: Schema.String.pipe(Schema.nonEmptyString()), correlationId: Schema.optional(Schema.String.pipe(Schema.nonEmptyString())), causationId: Schema.optional(Schema.String.pipe(Schema.nonEmptyString())), // InvoicePDFUploaded specific fields invoiceId: Schema.String.pipe(Schema.nonEmptyString(), Schema.pattern(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, { message: /* @__PURE__ */ __name(() => "Invoice ID must be a valid UUID", "message") })), invoiceNumber: Schema.String.pipe(Schema.nonEmptyString(), Schema.pattern(/^(INV-\d+|\d+)$/, { message: /* @__PURE__ */ __name(() => "Invoice number must follow format INV-1001 or 1221", "message") })), storageLocation: Schema.String.pipe(Schema.nonEmptyString()), storageProvider: Schema.String.pipe(Schema.nonEmptyString()), uploadedAt: Schema.DateFromSelf, fileSizeBytes: Schema.Number.pipe(Schema.int(), Schema.greaterThan(0, { message: /* @__PURE__ */ __name(() => "File size must be positive", "message") })), checksumMD5: Schema.String.pipe(Schema.nonEmptyString(), Schema.pattern(/^[a-f0-9]{32}$/i, { message: /* @__PURE__ */ __name(() => "Checksum must be a valid MD5 hash", "message") })), contentType: Schema.Literal("application/pdf"), uploadMetadata: Schema.Struct({ retryAttempts: Schema.Number.pipe(Schema.int(), Schema.greaterThanOrEqualTo(0, { message: /* @__PURE__ */ __name(() => "Retry attempts must be non-negative", "message") })), uploadDurationMs: Schema.Number.pipe(Schema.int(), Schema.greaterThan(0, { message: /* @__PURE__ */ __name(() => "Upload duration must be positive", "message") })) }) }); var InvoicePDFUploadFailedSchema = Schema.Struct({ // Base domain event fields type: Schema.Literal("InvoicePDFUploadFailed"), eventId: Schema.String.pipe(Schema.nonEmptyString(), Schema.pattern(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, { message: /* @__PURE__ */ __name(() => "Event ID must be a valid UUID", "message") })), occurredOn: Schema.DateFromSelf, version: Schema.Number.pipe(Schema.int(), Schema.greaterThanOrEqualTo(1, { message: /* @__PURE__ */ __name(() => "Event version must be a positive integer", "message") })), aggregateType: Schema.String.pipe(Schema.nonEmptyString()), aggregateId: Schema.String.pipe(Schema.nonEmptyString()), correlationId: Schema.optional(Schema.String.pipe(Schema.nonEmptyString())), causationId: Schema.optional(Schema.String.pipe(Schema.nonEmptyString())), // InvoicePDFUploadFailed specific fields invoiceId: Schema.String.pipe(Schema.nonEmptyString(), Schema.pattern(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, { message: /* @__PURE__ */ __name(() => "Invoice ID must be a valid UUID", "message") })), invoiceNumber: Schema.String.pipe(Schema.nonEmptyString(), Schema.pattern(/^(INV-\d+|\d+)$/, { message: /* @__PURE__ */ __name(() => "Invoice number must follow format INV-1001 or 1221", "message") })), failureReason: Schema.String.pipe(Schema.nonEmptyString()), failureCategory: Schema.Literal("network", "authentication", "storage", "validation"), httpStatusCode: Schema.optional(Schema.Number.pipe(Schema.int(), Schema.between(100, 599, { message: /* @__PURE__ */ __name(() => "HTTP status code must be between 100 and 599", "message") }))), retryable: Schema.Boolean, retryAttempt: Schema.Number.pipe(Schema.int(), Schema.greaterThanOrEqualTo(0, { message: /* @__PURE__ */ __name(() => "Retry attempt must be non-negative", "message") })), maxRetries: Schema.Number.pipe(Schema.int(), Schema.greaterThanOrEqualTo(0, { message: /* @__PURE__ */ __name(() => "Max retries must be non-negative", "message") })), nextRetryAt: Schema.optional(Schema.DateFromSelf), errorDetails: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })) }); // src/events/index.ts var EventTypes = { INVOICE_CREATED: "InvoiceCreated", INVOICE_DRAFT_REQUESTED: "InvoiceDraftRequested", INVOICE_CREATION_FAILED: "InvoiceCreationFailed", INVOICE_PDF_UPLOADED: "InvoicePDFUploaded", INVOICE_PDF_UPLOAD_FAILED: "InvoicePDFUploadFailed" }; var EventCategories = { INVOICE_LIFECYCLE: [ EventTypes.INVOICE_CREATED, EventTypes.INVOICE_DRAFT_REQUESTED, EventTypes.INVOICE_CREATION_FAILED ], STORAGE_OPERATIONS: [ EventTypes.INVOICE_PDF_UPLOADED, EventTypes.INVOICE_PDF_UPLOAD_FAILED ] }; var ShippingPolicyFreeOver50 = Layer.succeed(ShippingPolicy, { isFreeShippingEligible(financials) { const threshold = MoneySchema.fromNumeric(50, CurrencySchema.quick("EUR", "\u20AC")); const total = financials.subtotal.add(financials.taxes).subtract(financials.discount); return Effect.succeed(total.isMore(threshold, "orEqual")); } }); // src/types/index.ts var sanitizeInvoicePrefix = /* @__PURE__ */ __name((prefix) => { const sanitized = prefix.replace(/[^a-zA-Z0-9-]/g, ""); return sanitized.substring(0, 10); }, "sanitizeInvoicePrefix"); var ValidationError = class extends Data.TaggedError("@/ValidationError") { static { __name(this, "ValidationError"); } }; var MissingDataError = class extends Data.TaggedError("@/MissingDataError") { static { __name(this, "MissingDataError"); } }; var InvalidFormatError = class extends Data.TaggedError("@/InvalidFormatError") { static { __name(this, "InvalidFormatError"); } }; var OutOfRangeError = class extends Data.TaggedError("@/OutOfRangeError") { static { __name(this, "OutOfRangeError"); } }; var InvalidLengthError = class extends Data.TaggedError("@/InvalidLengthError") { static { __name(this, "InvalidLengthError"); } }; var InvalidEnumValueError = class extends Data.TaggedError("@/InvalidEnumValueError") { static { __name(this, "InvalidEnumValueError"); } }; var SchemaValidationError = class extends Data.TaggedError("@/SchemaValidationError") { static { __name(this, "SchemaValidationError"); } }; var BusinessRuleViolationError = class extends Data.TaggedError("@/BusinessRuleViolationError") { static { __name(this, "BusinessRuleViolationError"); } }; var ImmutabilityViolationError = class extends Data.TaggedError("@/ImmutabilityViolationError") { static { __name(this, "ImmutabilityViolationError"); } }; var UniquenessViolationError = class extends Data.TaggedError("@/UniquenessViolationError") { static { __name(this, "UniquenessViolationError"); } }; var InvalidStateTransitionError = class extends Data.TaggedError("@/InvalidStateTransitionError") { static { __name(this, "InvalidStateTransitionError"); } }; var OperationNotAllowedError = class extends Data.TaggedError("@/OperationNotAllowedError") { static { __name(this, "OperationNotAllowedError"); } }; var FinancialCalculationError = class extends Data.TaggedError("@/FinancialCalculationError") { static { __name(this, "FinancialCalculationError"); } }; var CurrencyMismatchError = class extends Data.TaggedError("@/CurrencyMismatchError") { static { __name(this, "CurrencyMismatchError"); } }; var ResourceError = class extends Data.TaggedError("@/ResourceError") { static { __name(this, "ResourceError"); } }; var ResourceNotFoundError = class extends Data.TaggedError("@/ResourceNotFoundError") { static { __name(this, "ResourceNotFoundError"); } }; var ResourceAccessDeniedError = class extends Data.TaggedError("@/ResourceAccessDeniedError") { static { __name(this, "ResourceAccessDeniedError"); } }; var ResourceInUseError = class extends Data.TaggedError("@/ResourceInUseError") { static { __name(this, "ResourceInUseError"); } }; var ResourceLockedError = class extends Data.TaggedError("@/ResourceLockedError") { static { __name(this, "ResourceLockedError"); } }; var ResourceStateError = class extends Data.TaggedError("@/ResourceStateError") { static { __name(this, "ResourceStateError"); } }; (class extends Data.TaggedError("@/InvoiceNumberFormatError") { static { __name(this, "InvoiceNumberFormatError"); } }); var InvoiceNumberConflictError = class extends Data.TaggedError("@/InvoiceNumberConflictError") { static { __name(this, "InvoiceNumberConflictError"); } }; var InvoiceValidationError2 = class extends Data.TaggedError("@/InvoiceValidationError") { static { __name(this, "InvoiceValidationError"); } }; var InvoiceImmutabilityError2 = class extends Data.TaggedError("@/InvoiceImmutabilityError") { static { __name(this, "InvoiceImmutabilityError"); } }; var OrderNotFoundError = class extends Data.TaggedError("@/OrderNotFoundError") { static { __name(this, "OrderNotFoundError"); } }; var OrderValidationError = class extends Data.TaggedError("@/OrderValidationError") { static { __name(this, "OrderValidationError"); } }; var OrderStateError = class extends Data.TaggedError("@/OrderStateError") { static { __name(this, "OrderStateError"); } }; (class extends Data.TaggedError("@/PDFGenerationError") { static { __name(this, "PDFGenerationError"); } }); (class extends Data.TaggedError("@/PDFComplianceError") { static { __name(this, "PDFComplianceError"); } }); var StorageError = class extends Data.TaggedError("@/StorageError") { static { __name(this, "StorageError"); } }; var StorageUploadError = class extends Data.TaggedError("@/StorageUploadError") { static { __name(this, "StorageUploadError"); } }; var RepositoryError = class extends Data.TaggedError("@/RepositoryError") { static { __name(this, "RepositoryError"); } }; var DatabaseConstraintError = class extends Data.TaggedError("@/DatabaseConstraintError") { static { __name(this, "DatabaseConstraintError"); } }; // src/shared/errors/index.ts var ErrorCategories = { VALIDATION: [ "@/ValidationError", "@/MissingDataError", "@/InvalidFormatError", "@/OutOfRangeError", "@/InvalidLengthError", "@/InvalidEnumValueError", "@/SchemaValidationError" ], BUSINESS_RULES: [ "@/BusinessRuleViolationError", "@/ImmutabilityViolationError", "@/UniquenessViolationError", "@/InvalidStateTransitionError", "@/OperationNotAllowedError", "@/FinancialCalculationError", "@/CurrencyMismatchError" ], RESOURCES: [ "@/ResourceError", "@/ResourceNotFoundError", "@/ResourceAccessDeniedError", "@/ResourceInUseError", "@/ResourceLockedError", "@/ResourceStateError" ], INVOICE: [ "@/InvoiceNumberFormatError", "@/InvoiceNumberConflictError", "@/InvoiceValidationError", "@/InvoiceImmutabilityError" ], ORDER: [ "@/OrderNotFoundError", "@/OrderValidationError", "@/OrderStateError" ], PDF: [ "@/PDFGenerationError", "@/PDFComplianceError" ], STORAGE: [ "@/StorageError", "@/StorageUploadError" ], REPOSITORY: [ "@/RepositoryError", "@/DatabaseConstraintError" ] }; var RetryStrategies = { // Never retry validation or business rule errors - they're deterministic NON_RETRYABLE: [ ...ErrorCategories.VALIDATION, ...ErrorCategories.BUSINESS_RULES, ...ErrorCategories.INVOICE, ...ErrorCategories.ORDER ], // Retry technical errors with exponential backoff RETRYABLE_WITH_BACKOFF: [ ...ErrorCategories.STORAGE, ...ErrorCategories.REPOSITORY ], // Retry resource errors with linear backoff RETRYABLE_LINEAR: [ ...ErrorCategories.RESOURCES ], // PDF errors may be retryable depending on the specific error CONDITIONAL_RETRY: [ ...ErrorCategories.PDF ] }; var isValidationError = /* @__PURE__ */ __name((errorTag) => ErrorCategories.VALIDATION.includes(errorTag), "isValidationError"); var isBusinessRuleError = /* @__PURE__ */ __name((errorTag) => ErrorCategories.BUSINESS_RULES.includes(errorTag), "isBusinessRuleError"); var isResourceError = /* @__PURE__ */ __name((errorTag) => ErrorCategories.RESOURCES.includes(errorTag), "isResourceError"); var isRetryableError = /* @__PURE__ */ __name((errorTag) => [ ...RetryStrategies.RETRYABLE_WITH_BACKOFF, ...RetryStrategies.RETRYABLE_LINEAR ].includes(errorTag), "isRetryableError"); var isNonRetryableError = /* @__PURE__ */ __name((errorTag) => RetryStrategies.NON_RETRYABLE.includes(errorTag), "isNonRetryableError"); export { BusinessRuleViolationError, CurrencyMismatchError, CurrencySchema, CustomerInfoSchema, DatabaseConstraintError, DomainEventSchema, ErrorCategories, EventCategories, EventTypes, FinancialCalculationError, FinancialCalculationService, FinancialCalculationServiceLive, GermanLegalInfoSchema, ImmutabilityViolationError, InvalidEnumValueError, InvalidFormatError, InvalidLengthError, InvalidMoneyCurrencies, InvalidStateTransitionError, InvoiceAggregate, InvoiceConfigTag, InvoiceCreatedSchema, InvoiceCreationFailedSchema, InvoiceDraftRequestedSchema, InvoiceFinancialsSchema, InvoiceImmutabilityError2 as InvoiceImmutabilityError, InvoiceNumberConflictError, InvoiceNumberFormatError, InvoiceNumberFormatValidator, InvoiceNumberFormatValidatorLive, InvoicePDFDataSchema, InvoicePDFUploadFailedSchema, InvoicePDFUploadedSchema, InvoiceSchema, InvoiceValidationError2 as InvoiceValidationError, MissingDataError, MoneySchema, OperationNotAllowedError, OrderItemSchema, OrderNotFoundError, OrderSchema, OrderStateError, OrderValidationError, OutOfRangeError, PDFComplianceError, PDFGenerationError, PDFRenderOptionsSchema, PDFRenderer, RepositoryError, ResourceAccessDeniedError, ResourceError, ResourceInUseError, ResourceLockedError, ResourceNotFoundError, ResourceStateError, RetryStrategies, SchemaValidationError, ShippingPolicy, ShippingPolicyFreeOver50, StorageError, StorageUploadError, UniquenessViolationError, ValidationError, calculateGrandTotal, calculateLinePrice, calculateSubtotal, createCustomerInfo, createInvoiceFinancials, createInvoiceFinancialsWithCalculation, createOrder, createOrderItem, createOrderItemWithCalculation, createOrderWithCalculations, isBusinessRuleError, isNonRetryableError, isResourceError, isRetryableError, isValidationError, sanitizeInvoicePrefix, validateCustomerInfo, validateGrandTotalCalculation, validateInvoice, validateInvoiceFinancials, validateOrder, validateOrderItem }; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map