@invoiceddd/application
Version:
Application layer for the InvoiceDDD system - use cases and application services
617 lines (470 loc) • 15.3 kB
Markdown
# @invoiceddd/application
> Application layer containing use cases and application services for the InvoiceDDD system.
[](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.