invoiceddd
Version:
Complete invoice system with domain-driven design - gateway package for easy integration
377 lines (290 loc) ⢠10.8 kB
Markdown
# InvoiceDDD - Complete Invoice System
> A comprehensive, domain-driven TypeScript package for invoice management with PDF generation, storage, and event handling.
[](https://www.npmjs.com/package/invoiceddd)
[](https://www.typescriptlang.org/)
[](https://effect.website/)
## ⨠Features
- **šļø Domain-Driven Design**: Clean architecture with distinct domain, application, and infrastructure layers
- **ā” Effect-TS Powered**: Built on Effect framework for functional programming and composability
- **š PDF Generation**: PDF/A-3 compliant invoices with German legal requirements
- **š¾ Multiple Storage Options**: Support for local, S3, and custom storage backends
- **š Event-Driven**: Domain events for extensibility and integration
- **šÆ Type-Safe**: Full TypeScript support with runtime validation
- **š¦ Tree-Shakable**: Modular design for optimal bundle sizes
- **š Production Ready**: Battle-tested architecture with comprehensive error handling
## š Quick Start
### Installation
```bash
npm install invoiceddd
# or
yarn add invoiceddd
# or
pnpm add invoiceddd
```
### Important notes
This package uses the infrastructure package, which provides implementations for database repositories.
This means, when you use InvoiceDDD in a full-stack environment like Next.js, you can not import this package on the client side.
We have **experimental** support for server-only imports.
On the server, you can use `invoiceddd/server` to use server-only InvoiceDDD features (like migrations, database setup and more). On the client you can use InvoiceDDD as usual, without the server-only parts from the `invoiceddd` import.
**NOTE**: currently, some parts of the infrastructure package are not server-only, but only available in `invoiceddd/server`. We are working on splitting that up more granularly in the future. However, most of them are useless on the client anyway (for example, `StorageService` is not dependant on any server-only part, but is only available in `invoiceddd/server`) - keep an eye on changes. They currently happen very frequently, so it shouldn't take too long.
### Basic Usage
```typescript
import { InvoiceSystem } from 'invoiceddd';
// Create system with default configuration
const system = await InvoiceSystem.create();
// Start HTTP server (optional)
await system.startServer();
// Create an invoice
const result = await system.createInvoice({
orderId: "ORD-2024-001",
customerSnapshot: {
fullName: "John Doe",
email: "john@example.com",
address: {
street: "123 Main St",
city: "Berlin",
postalCode: "10115",
country: "Germany"
}
},
itemsSnapshot: [{
name: "Premium Widget",
unitPrice: 29.99,
quantity: 2,
linePrice: 59.98
}],
financials: {
subtotal: 59.98,
taxes: 11.40,
shippingCost: 5.99,
discount: 0.00,
grandTotal: 77.37,
currency: "EUR"
},
createdBy: "admin@company.com"
});
if (result._tag === "Right") {
console.log(`Invoice created: ${result.right.invoiceNumber}`);
// PDF bytes available at: result.right.pdfBytes
}
```
### Custom Configuration
```typescript
import { InvoiceSystem, ConfigurationBuilder } from 'invoiceddd';
const system = await InvoiceSystem.create({
config: {
database: {
url: 'file:./my-invoices.db'
},
server: {
port: 8080,
host: '0.0.0.0'
},
storage: {
endpoint: 'https://s3.amazonaws.com',
accessKey: process.env.AWS_ACCESS_KEY!,
secretKey: process.env.AWS_SECRET_KEY!,
bucket: 'invoice-pdfs'
},
invoice: {
prefix: 'INVOICE'
}
},
autoStartServer: true
});
```
### Development Setup
```typescript
import { InvoiceSystem, ConfigurationBuilder } from 'invoiceddd';
// Pre-configured development environment
const devSystem = await InvoiceSystem.create({
config: ConfigurationBuilder.development({
database: { url: 'file:./dev-invoices.db' }
})
});
```
## šļø Package Architecture
InvoiceDDD is built as a modular system with clear separation of concerns:
```
invoiceddd/ # Gateway package (this package)
āāā @invoiceddd/domain # Core business logic
āāā @invoiceddd/application # Use cases and ports
āāā @invoiceddd/infrastructure # External integrations
```
### Individual Packages
If you need more granular control, you can install individual packages:
```bash
# Core domain logic only
npm install @invoiceddd/domain
# Application layer with use cases
npm install @invoiceddd/application
# Infrastructure implementations
npm install @invoiceddd/infrastructure
```
## š Documentation
- **[Getting Started Guide](../../docs/getting-started.md)** - Complete setup and first invoice
- **[API Reference](../../docs/api-reference.md)** - Detailed API documentation
- **[Configuration Guide](../../docs/configuration.md)** - All configuration options
- **[Domain Model](../../docs/domain-model.md)** - Business rules and entities
- **[Examples](../../examples/)** - Working code examples
## šÆ Use Cases
### E-commerce Integration
```typescript
// In your order fulfillment process
import { InvoiceSystem } from 'invoiceddd';
const invoiceSystem = await InvoiceSystem.create();
// When order is completed
const invoice = await invoiceSystem.createInvoice({
orderId: order.id,
customerSnapshot: {
fullName: `${order.customer.firstName} ${order.customer.lastName}`,
email: order.customer.email,
address: order.shippingAddress
},
itemsSnapshot: order.items.map(item => ({
name: item.product.name,
unitPrice: item.price,
quantity: item.quantity,
linePrice: item.total
})),
financials: {
subtotal: order.subtotal,
taxes: order.taxes,
shippingCost: order.shipping,
discount: order.discount,
grandTotal: order.total,
currency: order.currency
},
createdBy: order.processedBy
});
```
### Subscription Billing
```typescript
// For recurring invoices
const system = await InvoiceSystem.create({
config: {
invoice: { prefix: 'SUB' } // SUB-1001, SUB-1002, etc.
}
});
// Generate monthly invoice
const invoice = await system.createInvoice({
orderId: `subscription-${customerId}-${month}`,
// ... other details
});
```
### Multi-tenant Applications
```typescript
// Separate database per tenant
const createTenantSystem = (tenantId: string) =>
InvoiceSystem.create({
config: {
database: { url: `file:./invoices-${tenantId}.db` },
storage: { bucket: `invoices-${tenantId}` }
}
});
const tenantA = await createTenantSystem('tenant-a');
const tenantB = await createTenantSystem('tenant-b');
```
## š§ Advanced Configuration
### Custom Storage Provider
```typescript
import { InvoiceSystem, StorageService } from 'invoiceddd';
import { Effect, Layer } from 'effect';
// Implement custom storage
const CustomStorageLive = Layer.succeed(StorageService, {
uploadFile: (key: string, content: Uint8Array) =>
Effect.sync(() => {
// Your custom storage logic
console.log(`Uploading ${key} to custom storage`);
return { url: `https://mystorage.com/${key}` };
})
});
const system = await InvoiceSystem.create({
customLayers: {
storage: CustomStorageLive
}
});
```
### Event Subscribers
```typescript
import { InvoiceSystem } from 'invoiceddd';
const system = await InvoiceSystem.create({
subscribers: [
// Custom event handler
(event) => {
if (event._tag === 'InvoiceCreated') {
console.log(`New invoice: ${event.invoiceNumber}`);
// Send email, update CRM, etc.
}
}
]
});
```
## Adding custom service implementations
> The docs for the LayerBuilder API are not complete yet. We are working on it.
With the flexible `LayerBuilder API`, you can dynamically construct your own service implementation layers with ease. This allows for selective, modular injection of custom implementations while keeping everything type-safe and robust.
Look at this simple example:
```typescript
import { InvoiceNumberGenerator } from 'invoiceddd';
import { LayerBuilder } from 'invoiceddd/layer-builder';
const layerConfig = LayerBuilder.start()
.withCustomInvoiceNumberGenerator(
InvoiceNumberGenerator.of({
// ...
})
)
.build()
const runtime = createSimpleCustomRuntime({}, layerConfig);
// ^ ManagedRuntime<any, any>
```
Yes, the `ManagedRuntime` is untyped, but there is a fix in progress to automatically flatten and adapt the types from `LayerBuilder`.
For most layer builder parts, you have two options to provide your service implementation:
- **Most flexible**: Put in a custom `Layer`, like `Layer.succeed` or `Layer.effect`. All builder parts are just `Context.Tag`s, so it's highly compatible
- **Less verbose**: Just give it the schema. For example, you can just do `InvoiceNumberGenerator.of({ ... })` - it is a shorter version of the first option.
The option you choose depends on your needs. At the end, it's all Effect - do what you want.
> [!NOTE]
> `LayerBuilder` is also available in the `invoiceddd` import. But we recommend importing from `invoiceddd/layer-builder`, because it doesn't depend on external services that might be incompatible with browser environments. We are [working on it](#important-notes).
## š Production Deployment
### Environment Variables
```bash
# Database
DATABASE_URL=postgresql://user:pass@localhost:5432/invoices
# Server
PORT=3000
HOST=0.0.0.0
# Storage (S3)
STORAGE_ENDPOINT=https://s3.amazonaws.com
STORAGE_ACCESS_KEY=your-access-key
STORAGE_SECRET_KEY=your-secret-key
STORAGE_BUCKET=production-invoices
# Invoice settings
INVOICE_PREFIX=INV
```
### Docker Deployment
```dockerfile
FROM oven/bun:1-alpine
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build
EXPOSE 3000
CMD ["bun", "start"]
```
### Health Checks
```typescript
const system = await InvoiceSystem.create();
// Built-in health check endpoint
// GET /health returns system status
await system.startServer();
```
## š¤ Contributing
We welcome contributions! Please see our [Contributing Guide](../../CONTRIBUTING.md) for details.
## š License
MIT License - see [LICENSE](../../LICENSE) file for details.
## š Support
- **Issues**: [GitHub Issues](https://github.com/benjamin-kraatz/invoiceddd/issues)
- **Discussions**: [GitHub Discussions](https://github.com/benjamin-kraatz/invoiceddd/discussions)
- **Documentation**: [Full Documentation](../../docs/)
---
**Made with ā¤ļø, Effect and TypeScript**