UNPKG

invoiceddd

Version:

Complete invoice system with domain-driven design - gateway package for easy integration

669 lines (663 loc) 25.8 kB
import { Config, Layer, ManagedRuntime, Effect, ConfigProvider, Context, Console } from 'effect'; import { OrderRepository, InvoiceNumberRepository, InvoiceNumberGenerator, EventPublisherLive, EventBusLive, InvoiceNumberGeneratorLive, InvoiceNumberUniqueCheckerLive, RequestInvoiceUseCaseLive, CreateInvoiceUseCaseLive, ListInvoicesUseCaseLive, GetInvoiceUseCaseLive } from '@invoiceddd/application'; export { CreateInvoiceUseCase, CreateInvoiceUseCaseLive, EventBus, EventPublisher, EventPublisherLive, GetInvoiceUseCase, GetInvoiceUseCaseLive, InvoiceNumberConflictError, InvoiceNumberGenerationError, InvoiceNumberGenerator, InvoiceNumberRepository, InvoiceNumberUniqueChecker, InvoiceRepository, ListInvoicesUseCase, ListInvoicesUseCaseLive, OrderRepository, RequestInvoiceUseCase, RequestInvoiceUseCaseLive } from '@invoiceddd/application'; import { sanitizeInvoicePrefix, FinancialCalculationServiceLive, ShippingPolicyFreeOver50, InvoiceNumberFormatValidatorLive, InvoiceConfigTag } from '@invoiceddd/domain'; export { BusinessRuleViolationError, CurrencySchema, CustomerInfoSchema, EventCategories, EventTypes, FinancialCalculationService, InvoiceAggregate, InvoiceNumberFormatValidator, InvoiceValidationError, MoneySchema, OrderValidationError, PDFRenderer, ResourceError, ShippingPolicy, ShippingPolicyFreeOver50, StorageError, ValidationError, calculateGrandTotal, calculateLinePrice, calculateSubtotal, createCustomerInfo, createInvoiceFinancials, createOrder, createOrderItem, createOrderWithCalculations, sanitizeInvoicePrefix, validateCustomerInfo, validateInvoice, validateInvoiceFinancials, validateOrder, validateOrderItem } from '@invoiceddd/domain'; import { DatabaseLive, DrizzleOrderRepositoryLive, DrizzleInvoiceRepositoryLive, DrizzleInvoiceNumberRepositoryLive, ConcretePDFRenderer, StorageServiceMock, StorageSubscriberLive } from '@invoiceddd/infrastructure'; import * as Layer2 from 'effect/Layer'; var __defProp = Object.defineProperty; var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); var defaultDBUrl = ":memory:"; var DEFAULT_CONFIG = { database: { url: defaultDBUrl, authToken: "" }, server: { port: 3e3, host: "localhost" }, storage: { endpoint: "", accessKey: "", secretKey: "", bucket: "" }, invoice: {} }; var environmentConfig = Config.all({ database: Config.all({ url: Config.redacted("DATABASE_URL").pipe(Config.withDefault(defaultDBUrl)), authToken: Config.string("DATABASE_AUTH_TOKEN").pipe(Config.withDefault("")) }), server: Config.all({ port: Config.integer("PORT").pipe(Config.withDefault(3e3)), host: Config.string("HOST").pipe(Config.withDefault("localhost")) }), storage: Config.all({ endpoint: Config.string("STORAGE_ENDPOINT").pipe(Config.withDefault("")), accessKey: Config.string("STORAGE_ACCESS_KEY").pipe(Config.withDefault("")), secretKey: Config.string("STORAGE_SECRET_KEY").pipe(Config.withDefault("")), bucket: Config.string("STORAGE_BUCKET").pipe(Config.withDefault("")) }), invoice: Config.all({ prefix: Config.string("INVOICE_PREFIX").pipe(Config.option, Config.map((prefixOption) => prefixOption._tag === "Some" ? sanitizeInvoicePrefix(prefixOption.value) : void 0)) }) }); function loadConfigWithDefaults(userConfig = {}) { const envConfigEffect = Effect.gen(function* () { const config = yield* environmentConfig; return { database: { url: typeof config.database.url === "string" ? config.database.url : config.database.url.toString(), authToken: config.database.authToken }, server: config.server, storage: config.storage, invoice: config.invoice }; }); const envConfig = Effect.runSync(envConfigEffect); const mergedConfig = { database: { ...DEFAULT_CONFIG.database, ...envConfig.database, ...userConfig.database }, server: { ...DEFAULT_CONFIG.server, ...envConfig.server, ...userConfig.server }, storage: { ...DEFAULT_CONFIG.storage, ...envConfig.storage, ...userConfig.storage }, invoice: { ...DEFAULT_CONFIG.invoice, ...envConfig.invoice, ...userConfig.invoice } }; return mergedConfig; } __name(loadConfigWithDefaults, "loadConfigWithDefaults"); var ConfigurationBuilder = { /** * Development configuration with in-memory database */ development: /* @__PURE__ */ __name((overrides = {}) => ({ database: { url: defaultDBUrl, authToken: "" }, server: { port: 3001, host: "localhost" }, ...overrides }), "development"), /** * Production configuration with required environment variables */ production: /* @__PURE__ */ __name((overrides = {}) => ({ server: { port: 8080, host: "0.0.0.0" }, ...overrides }), "production"), /** * Testing configuration with isolated database */ testing: /* @__PURE__ */ __name((overrides = {}) => ({ database: { url: defaultDBUrl, authToken: "" }, server: { port: 0, host: "localhost" }, ...overrides }), "testing") }; var validateConfig = /* @__PURE__ */ __name((config) => { const errors = []; if (!config.database.url) { errors.push("Database URL is required"); } if (config.server.port <= 0 || config.server.port > 65535) { errors.push("Server port must be between 1 and 65535"); } if (!config.server.host) { errors.push("Server host is required"); } const storageFields = [ config.storage.endpoint, config.storage.accessKey, config.storage.secretKey, config.storage.bucket ]; const hasAnyStorageConfig = storageFields.some((field) => field !== ""); const hasAllStorageConfig = storageFields.every((field) => field !== ""); if (hasAnyStorageConfig && !hasAllStorageConfig) { errors.push("If storage is configured, all storage fields (endpoint, accessKey, secretKey, bucket) must be provided"); } return { isValid: errors.length === 0, errors }; }, "validateConfig"); // src/factory/application-runtime.ts var createConfigurationLayer = /* @__PURE__ */ __name((config) => { return Layer.succeed(InvoiceConfigTag, { prefix: config.invoice.prefix ?? void 0 }); }, "createConfigurationLayer"); var createDatabaseConfigLayer = /* @__PURE__ */ __name((config) => { return Layer.setConfigProvider(ConfigProvider.fromMap(/* @__PURE__ */ new Map([ [ "DATABASE_URL", config.database.url ], [ "DATABASE_AUTH_TOKEN", config.database.authToken || "" ] ]))); }, "createDatabaseConfigLayer"); var StorageService = StorageServiceMock; function createInfrastructureLayer(databaseConfigLayer) { const databaseLayer = DatabaseLive.pipe(Layer.provide(databaseConfigLayer)); return Layer.mergeAll(databaseLayer, DrizzleOrderRepositoryLive.pipe(Layer.provide(databaseLayer)), DrizzleInvoiceRepositoryLive.pipe(Layer.provide(databaseLayer)), DrizzleInvoiceNumberRepositoryLive.pipe(Layer.provide(databaseLayer)), ConcretePDFRenderer, StorageService).pipe(Layer.mapError(() => new Error("Infrastructure layer error"))); } __name(createInfrastructureLayer, "createInfrastructureLayer"); function createEventInfrastructureLayer() { return Layer.mergeAll(EventBusLive, StorageSubscriberLive.pipe(Layer.provide(EventBusLive), Layer.provide(StorageService))); } __name(createEventInfrastructureLayer, "createEventInfrastructureLayer"); var createDomainServiceLayer = /* @__PURE__ */ __name((configLayer) => Layer.mergeAll(FinancialCalculationServiceLive, InvoiceNumberFormatValidatorLive.pipe(Layer.provide(configLayer)), ShippingPolicyFreeOver50), "createDomainServiceLayer"); var createDomainRepositoryDependentLayer = /* @__PURE__ */ __name((infrastructureLayer, configLayer) => Layer.mergeAll(InvoiceNumberUniqueCheckerLive.pipe(Layer.provide(infrastructureLayer)), InvoiceNumberGeneratorLive.pipe(Layer.provide(infrastructureLayer), Layer.provide(configLayer))), "createDomainRepositoryDependentLayer"); var createApplicationServiceLayer = /* @__PURE__ */ __name((eventLayer) => EventPublisherLive.pipe(Layer.provide(eventLayer)), "createApplicationServiceLayer"); var createApplicationUseCaseLayer = /* @__PURE__ */ __name((allDependencies) => Layer.mergeAll(RequestInvoiceUseCaseLive, CreateInvoiceUseCaseLive, ListInvoicesUseCaseLive, GetInvoiceUseCaseLive).pipe(Layer.provide(allDependencies)), "createApplicationUseCaseLayer"); var createEventStartupLayer = /* @__PURE__ */ __name(() => Layer.effect(Context.GenericTag("@/EventInfrastructureStartup"), Effect.succeed({})), "createEventStartupLayer"); var createApplicationRuntime = /* @__PURE__ */ __name((userConfig = {}, _enableDevTools = false) => { const config = loadConfigWithDefaults(userConfig); const configLayer = createConfigurationLayer(config); const databaseConfigLayer = createDatabaseConfigLayer(config); const infrastructureLayer = createInfrastructureLayer(databaseConfigLayer); const eventInfrastructureLayer = createEventInfrastructureLayer(); const domainServiceLayer = createDomainServiceLayer(configLayer); const domainRepositoryDependentLayer = createDomainRepositoryDependentLayer(infrastructureLayer, configLayer); const applicationServiceLayer = createApplicationServiceLayer(eventInfrastructureLayer); const eventStartupLayer = createEventStartupLayer(); const allDependencies = Layer.mergeAll(infrastructureLayer, eventInfrastructureLayer, domainServiceLayer, domainRepositoryDependentLayer, applicationServiceLayer); const applicationUseCaseLayer = createApplicationUseCaseLayer(allDependencies); const completeLayer = Layer.mergeAll(allDependencies, applicationUseCaseLayer, eventStartupLayer.pipe(Layer.provide(allDependencies))); const runtime = ManagedRuntime.make(completeLayer.pipe(Layer.provide(eventInfrastructureLayer))); return { runtime, config }; }, "createApplicationRuntime"); var createSimpleCustomRuntime = /* @__PURE__ */ __name((userConfig = {}, customLayers = [], _enableDevTools = false) => { const config = loadConfigWithDefaults(userConfig); const configLayer = createConfigurationLayer(config); const databaseConfigLayer = createDatabaseConfigLayer(config); const isLayerBuilderConfig = !Array.isArray(customLayers); const layerConfig = isLayerBuilderConfig ? customLayers : {}; const createCustomInfrastructureLayer = /* @__PURE__ */ __name(() => { const databaseLayer = DatabaseLive.pipe(Layer.provide(databaseConfigLayer)); const orderRepo = layerConfig.orderRepository ?? DrizzleOrderRepositoryLive.pipe(Layer.provide(databaseLayer)); const invoiceRepo = layerConfig.invoiceRepository ?? DrizzleInvoiceRepositoryLive.pipe(Layer.provide(databaseLayer)); const invoiceNumberRepo = layerConfig.invoiceNumberRepository ?? DrizzleInvoiceNumberRepositoryLive.pipe(Layer.provide(databaseLayer)); const pdfRenderer = layerConfig.pdfRenderer ?? ConcretePDFRenderer; const storageService = layerConfig.storageService ?? StorageService; return Layer.mergeAll(databaseLayer, orderRepo, invoiceRepo, invoiceNumberRepo, pdfRenderer, storageService).pipe(Layer.mapError(() => new Error("Infrastructure layer error"))); }, "createCustomInfrastructureLayer"); const createCustomEventInfrastructureLayer = /* @__PURE__ */ __name(() => { const eventBus = layerConfig.eventBus ?? EventBusLive; const storageService = layerConfig.storageService ?? StorageService; return Layer.mergeAll(eventBus, StorageSubscriberLive.pipe(Layer.provide(eventBus), Layer.provide(storageService))); }, "createCustomEventInfrastructureLayer"); const createCustomDomainServiceLayer = /* @__PURE__ */ __name(() => { const financialCalcService = layerConfig.financialCalculationService ?? FinancialCalculationServiceLive; const shippingPolicy = layerConfig.shippingPolicy ?? ShippingPolicyFreeOver50; return Layer.mergeAll(financialCalcService, InvoiceNumberFormatValidatorLive.pipe(Layer.provide(configLayer)), shippingPolicy); }, "createCustomDomainServiceLayer"); const infrastructureLayer = createCustomInfrastructureLayer(); const eventInfrastructureLayer = createCustomEventInfrastructureLayer(); const domainServiceLayer = createCustomDomainServiceLayer(); const createCustomDomainRepositoryDependentLayer = /* @__PURE__ */ __name(() => { const invoiceNumberGenerator = layerConfig.invoiceNumberGenerator ?? InvoiceNumberGeneratorLive.pipe(Layer.provide(infrastructureLayer), Layer.provide(configLayer)); return Layer.mergeAll(InvoiceNumberUniqueCheckerLive.pipe(Layer.provide(infrastructureLayer)), invoiceNumberGenerator); }, "createCustomDomainRepositoryDependentLayer"); const domainRepositoryDependentLayer = createCustomDomainRepositoryDependentLayer(); const applicationServiceLayer = EventPublisherLive.pipe(Layer.provide(eventInfrastructureLayer)); const eventStartupLayer = createEventStartupLayer(); const baseDependencies = Layer.mergeAll(infrastructureLayer, eventInfrastructureLayer, domainServiceLayer, domainRepositoryDependentLayer, applicationServiceLayer); const allDependencies = !isLayerBuilderConfig && customLayers.length > 0 ? Layer.mergeAll(baseDependencies, ...customLayers) : baseDependencies; const applicationUseCaseLayer = createApplicationUseCaseLayer(allDependencies); const completeLayer = Layer.mergeAll(allDependencies, applicationUseCaseLayer, eventStartupLayer.pipe(Layer.provide(allDependencies))); const runtime = ManagedRuntime.make(completeLayer.pipe(Layer.provide(eventInfrastructureLayer))); return { runtime, config }; }, "createSimpleCustomRuntime"); // src/factory/http-server.ts var createHttpServer = /* @__PURE__ */ __name(async (port, _runtime) => { console.log(`\u{1F310} Mock HTTP server would start on port ${port}`); return { async stop() { console.log("\u{1F6D1} Mock HTTP server stopped"); } }; }, "createHttpServer"); var LayerBuilder = class _LayerBuilder { static { __name(this, "LayerBuilder"); } customLayers = {}; constructor() { } /** * Start a new builder chain. * @returns {LayerBuilder} A new LayerBuilder instance. */ static start() { return new _LayerBuilder(); } /** * Replaces the default shipping policy with a custom implementation. * @param {Layer.Layer<R, E, never>} layer - The custom shipping policy layer. * @returns {LayerBuilder} The builder instance (for chaining). * @template R, E * @example * builder.withCustomShippingPolicy(VIPShippingPolicy) */ withCustomShippingPolicy(layer) { this.customLayers.shippingPolicy = layer; return this; } /** * Replaces the default invoice repository with a custom implementation. * @param {Layer.Layer<R, E, never>} layer - The custom invoice repository layer. * @returns {LayerBuilder} The builder instance (for chaining). * @template R, E * @example * builder.withCustomInvoiceRepository(InMemoryInvoiceRepository) */ withCustomInvoiceRepository(layer) { this.customLayers.invoiceRepository = layer; return this; } withCustomOrderRepository(arg) { if (isLayer(arg)) { this.customLayers.orderRepository = arg; } else { this.customLayers.orderRepository = Layer2.succeed(OrderRepository, arg); } return this; } withCustomInvoiceNumberRepository(arg) { if (isLayer(arg)) { this.customLayers.invoiceNumberRepository = arg; } else { this.customLayers.invoiceNumberRepository = Layer2.succeed(InvoiceNumberRepository, arg); } return this; } withCustomInvoiceNumberGenerator(arg) { if (isLayer(arg)) { this.customLayers.invoiceNumberGenerator = arg; } else { this.customLayers.invoiceNumberGenerator = Layer2.succeed(InvoiceNumberGenerator, arg); } return this; } /** * Replaces the default PDF renderer with a custom implementation. * @param {Layer.Layer<R, E, never>} layer - The custom PDF renderer layer. * @returns {LayerBuilder} The builder instance (for chaining). * @template R, E * @example * builder.withCustomPdfRenderer(MyCustomPdfRenderer) */ withCustomPdfRenderer(layer) { this.customLayers.pdfRenderer = layer; return this; } /** * Replaces the default storage service with a custom implementation. * @param {Layer.Layer<R, E, never>} layer - The custom storage service layer. * @returns {LayerBuilder} The builder instance (for chaining). * @template R, E * @example * builder.withCustomStorageService(MyCustomStorageService) */ withCustomStorageService(layer) { this.customLayers.storageService = layer; return this; } /** * Replaces the default event bus with a custom implementation. * @param {Layer.Layer<R, E, never>} layer - The custom event bus layer. * @returns {LayerBuilder} The builder instance (for chaining). * @template R, E * @example * builder.withCustomEventBus(MyCustomEventBus) */ withCustomEventBus(layer) { this.customLayers.eventBus = layer; return this; } /** * Replaces the default financial calculation service with a custom implementation. * @param {Layer.Layer<R, E, never>} layer - The custom financial calculation service layer. * @returns {LayerBuilder} The builder instance (for chaining). * @template R, E * @example * builder.withCustomFinancialCalculationService(MyCustomFinancialService) */ withCustomFinancialCalculationService(layer) { this.customLayers.financialCalculationService = layer; return this; } /** * Finalizes the builder and returns the custom layer configuration. * @returns {LayerBuilderConfig} The configuration object for custom layers. * @example * const config = builder.build(); */ build() { return { ...this.customLayers }; } }; function isLayer(value) { return typeof value === "object" && value !== null && Layer2.LayerTypeId in value; } __name(isLayer, "isLayer"); // src/factory/layer-configuration.ts var LayerConfigurationManager = class { static { __name(this, "LayerConfigurationManager"); } state = null; enableDevTools; constructor(enableDevTools = false) { this.enableDevTools = enableDevTools; } /** * Initialize the configuration manager with a default profile */ async initialize(defaultProfile, baseAppConfig = {}) { const mergedAppConfig = { ...baseAppConfig, ...defaultProfile.appConfig }; const { runtime, config } = await createSimpleCustomRuntime(mergedAppConfig, defaultProfile.config, this.enableDevTools); this.state = { currentProfile: defaultProfile.name, runtime, config, profiles: /* @__PURE__ */ new Map([ [ defaultProfile.name, defaultProfile ] ]) }; } /** * Register a new configuration profile */ registerProfile(profile) { if (!this.state) { throw new Error("LayerConfigurationManager not initialized"); } this.state.profiles.set(profile.name, profile); } /** * Switch to a different configuration profile */ async switchToProfile(profileName, baseAppConfig = {}) { if (!this.state) { throw new Error("LayerConfigurationManager not initialized"); } const profile = this.state.profiles.get(profileName); if (!profile) { throw new Error(`Profile '${profileName}' not found`); } await this.state.runtime.dispose(); const mergedAppConfig = { ...baseAppConfig, ...profile.appConfig }; const { runtime, config } = await createSimpleCustomRuntime(mergedAppConfig, profile.config, this.enableDevTools); this.state = { ...this.state, currentProfile: profileName, runtime, config }; } /** * Get the current runtime */ getCurrentRuntime() { if (!this.state) { throw new Error("LayerConfigurationManager not initialized"); } return this.state.runtime; } /** * Get the current configuration */ getCurrentConfig() { if (!this.state) { throw new Error("LayerConfigurationManager not initialized"); } return this.state.config; } /** * Get the current profile name */ getCurrentProfileName() { if (!this.state) { throw new Error("LayerConfigurationManager not initialized"); } return this.state.currentProfile; } /** * List all registered profiles */ listProfiles() { if (!this.state) { throw new Error("LayerConfigurationManager not initialized"); } return Array.from(this.state.profiles.values()); } /** * Dispose the current runtime and clean up resources */ async dispose() { if (this.state) { await this.state.runtime.dispose(); this.state = null; } } }; var createConfigurableRuntime = /* @__PURE__ */ __name(async (initialProfile, baseAppConfig = {}, enableDevTools = false) => { const manager = new LayerConfigurationManager(enableDevTools); await manager.initialize(initialProfile, baseAppConfig); return { manager, runtime: manager.getCurrentRuntime(), config: manager.getCurrentConfig() }; }, "createConfigurableRuntime"); var LayerCompositionHelpers = { /** * Create a development profile with in-memory storage and mock services */ createDevelopmentProfile(name = "development") { return { name, description: "Development profile with in-memory storage and mock services", config: LayerBuilder.start().build(), appConfig: { database: { url: ":memory:" }, server: { port: 3001, host: "localhost" } } }; }, /** * Create a testing profile optimized for unit tests */ createTestingProfile(name = "testing") { return { name, description: "Testing profile optimized for unit tests", config: LayerBuilder.start().build(), appConfig: { database: { url: ":memory:" }, server: { port: 0, host: "localhost" } } }; }, /** * Create a production profile with real services */ createProductionProfile(name = "production", customConfig = {}) { return { name, description: "Production profile with real services", config: customConfig, appConfig: { server: { port: 8080, host: "0.0.0.0" } } }; }, /** * Merge multiple layer configurations */ mergeConfigurations(...configs) { return configs.reduce((merged, config) => ({ ...merged, ...config }), {}); }, /** * Create a profile that extends another profile with additional layers */ extendProfile(baseProfile, extensions, newName, newDescription) { return { name: newName, description: newDescription || `Extended ${baseProfile.name}`, config: { ...baseProfile.config, ...extensions }, appConfig: baseProfile.appConfig }; } }; var RuntimeLayerSwitcher = class { static { __name(this, "RuntimeLayerSwitcher"); } manager; profiles = /* @__PURE__ */ new Map(); constructor(manager) { this.manager = manager; } /** * Add a profile for runtime switching */ addProfile(profile) { this.profiles.set(profile.name, profile); this.manager.registerProfile(profile); } /** * Switch to a profile by name */ async switchTo(profileName, appConfig) { await this.manager.switchToProfile(profileName, appConfig); } /** * Get current profile information */ getCurrentProfile() { const currentName = this.manager.getCurrentProfileName(); return this.profiles.get(currentName); } /** * List available profiles for switching */ getAvailableProfiles() { return Array.from(this.profiles.keys()); } }; // src/factory/index.ts var InvoiceSystem = { /** * Create a new invoice system with the provided configuration */ create: /* @__PURE__ */ __name(async (options = {}) => { const { config: userConfig = {}, autoStartServer = false, enableDevTools = false } = options; const { runtime, config: finalConfig } = await createApplicationRuntime(userConfig, enableDevTools); let serverInstance = null; const instance = { async startServer() { if (serverInstance) { throw new Error("Server is already running"); } Console.log("\u{1F310} Starting HTTP server...").pipe(runtime.runPromise); Console.log(`\u{1F4DA} API Documentation will be available at: http://${finalConfig.server.host}:${finalConfig.server.port}/docs`).pipe(runtime.runPromise); serverInstance = await createHttpServer(finalConfig.server.port, runtime); }, async stop() { Console.log("\u{1F6D1} Shutting down invoice system...").pipe(runtime.runPromise); if (serverInstance) { await serverInstance.stop(); serverInstance = null; } await runtime.dispose(); Console.log("\u2705 Invoice system stopped successfully").pipe(runtime.runPromise); }, getRuntime() { return runtime; }, getConfig() { return finalConfig; } }; if (autoStartServer) { await instance.startServer(); } return instance; }, "create") }; var VERSION = "0.1.5"; var PACKAGE_NAME = "invoiceddd"; export { ConfigurationBuilder, DEFAULT_CONFIG, InvoiceSystem, LayerBuilder, LayerCompositionHelpers, LayerConfigurationManager, PACKAGE_NAME, RuntimeLayerSwitcher, VERSION, createApplicationRuntime, createApplicationUseCaseLayer, createConfigurableRuntime, createDatabaseConfigLayer, createSimpleCustomRuntime, loadConfigWithDefaults, validateConfig }; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map