UNPKG

@invoiceddd/application

Version:

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

617 lines (470 loc) 15.3 kB
# @invoiceddd/application > Application layer containing use cases and application services for the InvoiceDDD system. [![NPM Version](https://img.shields.io/npm/v/@invoiceddd/application)](https://www.npmjs.com/package/@invoiceddd/application) ## Overview This package provides the application layer of the DDD invoice system, containing: - **Use Cases**: Business workflows and application-specific logic - **Ports**: Interfaces for infrastructure dependencies - **Application Services**: Event publishing and other application concerns The application layer orchestrates domain logic while remaining independent of infrastructure concerns. ## Installation ```bash npm install @invoiceddd/application @invoiceddd/domain ``` ## Key Features - **Clean Architecture**: Depends only on domain layer, defines interfaces for infrastructure - **Effect-TS Integration**: Built with Effect for composability and error handling - **Type-Safe Use Cases**: Fully typed business workflows with comprehensive error handling - **Event-Driven**: Built-in domain event publishing and subscription - **Testable**: Easy mocking and testing with dependency injection ## Use Cases ### CreateInvoiceUseCase Creates and finalizes invoices with PDF generation. ```typescript import { CreateInvoiceUseCase } from '@invoiceddd/application'; import { Effect } from 'effect'; const createInvoice = yield* CreateInvoiceUseCase; const result = yield* createInvoice.execute({ orderId: 'order-123', invoiceNumber: 'INV-2025-001', // Optional - will be generated customerSnapshot: { firstName: 'John', lastName: 'Doe', email: 'john@example.com', street: '123 Main St', city: 'Berlin', zip: '10115', country: 'Germany' }, itemsSnapshot: [{ name: 'Product A', quantity: 2, unitPrice: { amount: 100, currency: 'EUR' }, linePrice: { amount: 200, currency: 'EUR' } }], financials: { subtotal: { amount: 200, currency: 'EUR' }, taxes: { amount: 38, currency: 'EUR' }, shippingCost: { amount: 0, currency: 'EUR' }, discount: { amount: 0, currency: 'EUR' }, grandTotal: { amount: 238, currency: 'EUR' } }, createdBy: 'user-456' }); if (result._tag === 'Right') { console.log(`Invoice created: ${result.right.invoiceNumber}`); console.log(`PDF size: ${result.right.pdfBytes.length} bytes`); } ``` ### RequestInvoiceUseCase Creates an invoice draft from an existing order. ```typescript import { RequestInvoiceUseCase } from '@invoiceddd/application'; const requestInvoice = yield* RequestInvoiceUseCase; const draft = yield* requestInvoice.execute({ orderId: 'order-123', requestedBy: 'user-456' }); // Returns editable invoice data for user review ``` ### GetInvoiceUseCase Retrieves a single invoice by various criteria. ```typescript import { GetInvoiceUseCase } from '@invoiceddd/application'; const getInvoice = yield* GetInvoiceUseCase; // Get by order ID const result = yield* getInvoice.execute({ orderId: 'order-123', returnType: 'json' // or 'pdf' }); // Get by invoice number const result = yield* getInvoice.execute({ invoiceNumber: 'INV-2025-001', returnType: 'pdf' }); ``` ### ListInvoicesUseCase Queries invoices with filtering and pagination. ```typescript import { ListInvoicesUseCase } from '@invoiceddd/application'; const listInvoices = yield* ListInvoicesUseCase; const result = yield* listInvoices.execute({ filters: { createdBy: 'user-456', dateFrom: new Date('2025-01-01'), dateTo: new Date('2025-12-31'), minTotal: 100 }, pagination: { page: 1, limit: 20 } }); console.log(`Found ${result.totalCount} invoices`); result.invoices.forEach(invoice => { console.log(`${invoice.invoiceNumber}: ${invoice.financials.grandTotal.amount}`); }); ``` ## Ports (Infrastructure Interfaces) The application layer defines interfaces that infrastructure must implement: ### InvoiceRepository ```typescript import { InvoiceRepository } from '@invoiceddd/application'; // Infrastructure layer implements this class DrizzleInvoiceRepository implements InvoiceRepository { save(invoice: Invoice): Effect.Effect<void, RepositoryError> { // Database implementation } findByOrderId(orderId: string): Effect.Effect<Option<Invoice>, RepositoryError> { // Database query implementation } findByInvoiceNumber(invoiceNumber: string): Effect.Effect<Option<Invoice>, RepositoryError> { // Database query implementation } list(filters: InvoiceFilters, pagination: Pagination): Effect.Effect<InvoiceList, RepositoryError> { // Database query with filtering } } ``` ### OrderRepository ```typescript import { OrderRepository } from '@invoiceddd/application'; class DrizzleOrderRepository implements OrderRepository { findById(orderId: string): Effect.Effect<Option<Order>, RepositoryError> { // Order lookup implementation } } ``` ### InvoiceNumberRepository ```typescript import { InvoiceNumberRepository } from '@invoiceddd/application'; class DrizzleInvoiceNumberRepository implements InvoiceNumberRepository { existsByInvoiceNumber(invoiceNumber: string): Effect.Effect<boolean, RepositoryError> { // Uniqueness check implementation } } ``` ## Application Services ### EventPublisher Publishes domain events for integration with other systems. ```typescript import { EventPublisher } from '@invoiceddd/application'; const eventPublisher = yield* EventPublisher; // Automatically called by use cases yield* eventPublisher.publish({ _tag: 'InvoiceCreated', invoiceId: '123', invoiceNumber: 'INV-1001', orderId: 'order-123', createdBy: 'user-456', createdAt: new Date(), // ... other event data }); ``` ## Error Handling All use cases return typed errors for comprehensive error handling: ```typescript import { CreateInvoiceUseCase } from '@invoiceddd/application'; const result = yield* createInvoice.execute(input).pipe( Effect.catchTags({ InvoiceValidationError: (error) => { console.error('Validation failed:', error.details); return Effect.fail('Invalid invoice data'); }, RepositoryError: (error) => { console.error('Database error:', error.message); return Effect.fail('Database operation failed'); }, PDFGenerationFailedError: (error) => { console.error('PDF generation failed:', error.message); return Effect.fail('PDF generation failed'); }, BusinessRuleViolationError: (error) => { console.error('Business rule violation:', error.message); return Effect.fail('Business rule violated'); } }) ); ``` ## Testing Use Effect-TS test utilities with mock implementations: ```typescript import { Effect, Layer, Option } from 'effect'; import { CreateInvoiceUseCase, InvoiceRepository } from '@invoiceddd/application'; // Mock repository implementation const MockInvoiceRepository = Layer.succeed( InvoiceRepository, { save: () => Effect.void, findByOrderId: () => Effect.succeed(Option.none()), findByInvoiceNumber: () => Effect.succeed(Option.none()), list: () => Effect.succeed({ invoices: [], totalCount: 0, page: 1, limit: 20, totalPages: 0 }) } ); // Test setup const TestLayer = Layer.mergeAll( MockInvoiceRepository, // ... other test dependencies ); // Test execution const testProgram = Effect.gen(function* () { const createInvoice = yield* CreateInvoiceUseCase; const result = yield* createInvoice.execute({ orderId: 'test-order', // ... test data }); expect(result._tag).toBe('Right'); }); await Effect.runPromise( testProgram.pipe(Effect.provide(TestLayer)) ); ``` ## Integration Examples ### Express.js Integration ```typescript import express from 'express'; import { CreateInvoiceUseCase } from '@invoiceddd/application'; import { Effect } from 'effect'; const app = express(); app.post('/invoices', async (req, res) => { const program = Effect.gen(function* () { const createInvoice = yield* CreateInvoiceUseCase; return yield* createInvoice.execute(req.body); }); const result = await Effect.runPromise( program.pipe(Effect.provide(AppLayer)) ); if (result._tag === 'Right') { res.json(result.right); } else { res.status(400).json({ error: result.left }); } }); ``` ### Event Integration ```typescript import { EventPublisher } from '@invoiceddd/application'; // Custom event handler const handleInvoiceCreated = (event: InvoiceCreated) => Effect.gen(function* () { // Send email notification yield* sendEmailNotification(event.customerEmail); // Update analytics yield* updateAnalytics(event); // Integration with external systems yield* notifyERPSystem(event); }); // Setup event subscription const eventPublisher = yield* EventPublisher; yield* eventPublisher.subscribe('InvoiceCreated', handleInvoiceCreated); ``` ## Dependencies - `@invoiceddd/domain` - Core domain logic - `effect` - Functional programming framework ## Related Packages - `@invoiceddd/domain` - Core business logic and entities - `@invoiceddd/infrastructure` - Database and external service implementations - `invoiceddd` - Complete system with easy setup ## Documentation For complete documentation, see the [main documentation](../../docs/). ## Use Cases ### CreateInvoiceUseCase Creates and finalizes invoices with PDF generation. ```typescript import { CreateInvoiceUseCase } from '@invoiceddd/application'; const createInvoice = yield* CreateInvoiceUseCase; const result = yield* createInvoice.execute({ orderId: 'order-123', invoiceNumber: 'INV-2025-001', // Optional - will be generated customerSnapshot: { firstName: 'John', lastName: 'Doe', email: 'john@example.com', // ... other customer fields }, itemsSnapshot: [{ name: 'Product A', quantity: 2, unitPrice: { amount: 100, currency: 'EUR' }, // ... other item fields }], financials: { subtotal: { amount: 200, currency: 'EUR' }, taxes: { amount: 38, currency: 'EUR' }, grandTotal: { amount: 238, currency: 'EUR' }, // ... other financial fields }, createdBy: 'user-456' }); ``` ### RequestInvoiceUseCase Creates an invoice draft from an existing order. ```typescript import { RequestInvoiceUseCase } from '@invoiceddd/application'; const requestInvoice = yield* RequestInvoiceUseCase; const draft = yield* requestInvoice.execute({ orderId: 'order-123', requestedBy: 'user-456' }); ``` ### GetInvoiceUseCase Retrieves a single invoice by various criteria. ```typescript import { GetInvoiceUseCase } from '@invoiceddd/application'; const getInvoice = yield* GetInvoiceUseCase; // Get by order ID const result = yield* getInvoice.execute({ orderId: 'order-123', returnType: 'json' // or 'pdf' }); // Get by invoice number const result = yield* getInvoice.execute({ invoiceNumber: 'INV-2025-001', returnType: 'pdf' }); ``` ### ListInvoicesUseCase Queries invoices with filtering and pagination. ```typescript import { ListInvoicesUseCase } from '@invoiceddd/application'; const listInvoices = yield* ListInvoicesUseCase; const result = yield* listInvoices.execute({ filters: { createdBy: 'user-456', dateFrom: new Date('2025-01-01'), dateTo: new Date('2025-12-31'), minTotal: 100 }, pagination: { page: 1, limit: 20 } }); ``` ## Ports (Infrastructure Interfaces) ### InvoiceRepository Interface for invoice persistence operations. ```typescript import { InvoiceRepositoryTag } from '@invoiceddd/application'; // Implement in infrastructure layer class DrizzleInvoiceRepository implements InvoiceRepository { save(invoice: Invoice) { /* ... */ } findByOrderId(orderId: string) { /* ... */ } findByInvoiceNumber(invoiceNumber: string) { /* ... */ } list(filters: InvoiceFilters, pagination: Pagination) { /* ... */ } } // Provide via dependency injection const InvoiceRepositoryLive = Layer.succeed( InvoiceRepositoryTag, new DrizzleInvoiceRepository() ); ``` ### OrderRepository Interface for order retrieval operations. ```typescript import { OrderRepositoryTag } from '@invoiceddd/application'; class DrizzleOrderRepository implements OrderRepository { findById(orderId: string) { /* ... */ } } ``` ### InvoiceNumberRepository Interface for invoice number uniqueness checking. ```typescript import { InvoiceNumberRepositoryTag } from '@invoiceddd/application'; class DrizzleInvoiceNumberRepository implements InvoiceNumberRepository { existsByInvoiceNumber(invoiceNumber: string) { /* ... */ } } ``` ## Application Services ### EventBus In-memory event bus for domain event publishing and subscription. ```typescript import { EventBus, EventBusLive } from '@invoiceddd/application'; // Start event processing const eventBus = yield* EventBus; yield* eventBus.start(); // Subscribe to events const subscriptionId = yield* eventBus.subscribe( 'InvoiceCreated', (event) => Effect.log(`Invoice created: ${event.invoiceNumber}`) ); // Publish events yield* eventBus.publish(domainEvent); // Cleanup yield* eventBus.unsubscribe(subscriptionId); yield* eventBus.stop(); ``` ### EventPublisher Simplified interface for publishing domain events. ```typescript import { EventPublisher, EventPublisherLive } from '@invoiceddd/application'; const eventPublisher = yield* EventPublisher; yield* eventPublisher.publish({ type: 'InvoiceCreated', invoiceId: '123', // ... other event data }); ``` ## Error Handling All use cases return typed errors for proper error handling: ```typescript import { CreateInvoiceUseCase } from '@invoiceddd/application'; const result = yield* createInvoice.execute(input).pipe( Effect.catchTags({ InvoiceValidationError: (error) => Effect.fail(`Validation failed: ${error.message}`), RepositoryError: (error) => Effect.fail(`Database error: ${error.message}`), PDFGenerationFailedError: (error) => Effect.fail(`PDF generation failed: ${error.message}`) }) ); ``` ## Testing Use Effect-TS test utilities with mock implementations: ```typescript import { Effect, Layer } from 'effect'; import { CreateInvoiceUseCase } from '@invoiceddd/application'; // Mock repository const MockInvoiceRepository = Layer.succeed( InvoiceRepositoryTag, { save: () => Effect.void, findByOrderId: () => Effect.succeed(Option.none()), findByInvoiceNumber: () => Effect.succeed(Option.none()), list: () => Effect.succeed({ invoices: [], totalCount: 0, page: 1, limit: 20, totalPages: 0 }) } ); // Test layer const TestLayer = Layer.mergeAll( MockInvoiceRepository, EventBusLive, // ... other test dependencies ); // Run test const testProgram = Effect.gen(function* () { const createInvoice = yield* CreateInvoiceUseCase; // ... test logic }); await Effect.runPromise( testProgram.pipe(Effect.provide(TestLayer)) ); ``` ## Migration Notes Some application code that depends on infrastructure has not yet been migrated to maintain DDD boundaries. See [MIGRATION_NOTE.md](./MIGRATION_NOTE.md) for details on the refactoring approach. ## API Reference For complete API documentation, see the TypeScript definitions and JSDoc comments in the source code. ## License See the main package README for license information.