Migrate InvoiceService to dependency injection pattern

- Add INVOICE_SERVICE token to DI container
- Register InvoiceService in base module with proper lifecycle
- Update market store to use dependency injection instead of singleton
- Remove exported singleton from InvoiceService class
- Add comprehensive migration documentation with examples
- Maintain type safety with proper TypeScript interfaces

This migration eliminates the legacy singleton pattern and improves:
- Testability through service injection
- Modular architecture with clear boundaries
- Single source of truth for service instances
- Consistent dependency injection patterns

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
padreug 2025-09-07 01:10:55 +02:00
parent 6cb10a31db
commit 7a32085ee1
5 changed files with 383 additions and 6 deletions

View file

@ -0,0 +1,369 @@
# Dependency Injection Migration Plan
## Overview
This document outlines the migration approach for converting legacy exported singleton services to the dependency injection pattern, following the successful authentication architecture refactoring.
## Legacy Pattern vs New Pattern
### ❌ Legacy Pattern (To Be Replaced)
```typescript
// Service definition with exported singleton
export class SomeService {
// service implementation
}
export const someService = new SomeService()
// Consumer code
import { someService } from '@/services/someService'
someService.doSomething()
```
### ✅ New Dependency Injection Pattern
```typescript
// Service definition (no exported singleton)
export class SomeService extends BaseService {
// service implementation
}
// Service registration in module
container.provide(SERVICE_TOKENS.SOME_SERVICE, someService)
// Consumer code
const someService = injectService(SERVICE_TOKENS.SOME_SERVICE) as SomeService
someService.doSomething()
```
## Services to Migrate
### Priority 1: High Impact Services
#### 1. InvoiceService
- **File**: `src/core/services/invoiceService.ts`
- **Current**: `export const invoiceService = new InvoiceService()`
- **Token**: Need to add `INVOICE_SERVICE: Symbol('invoiceService')`
- **Usage**: Market store and components for payment processing
- **Impact**: Core business logic, high usage
- **Dependencies**: Likely uses LnbitsAPI and other services
#### 2. NostrmarketService
- **File**: `src/modules/market/services/nostrmarketService.ts`
- **Current**: `export const nostrmarketService = new NostrmarketService()`
- **Token**: Need to add `NOSTRMARKET_SERVICE: Symbol('nostrmarketService')`
- **Usage**: Market module composables and components
- **Impact**: Key module service, should follow DI pattern
- **Dependencies**: Likely uses RelayHub and other Nostr services
### Priority 2: Medium Impact Services
#### 3. PaymentMonitorService
- **File**: `src/modules/market/services/paymentMonitor.ts`
- **Current**: `export const paymentMonitor = new PaymentMonitorService()`
- **Token**: `SERVICE_TOKENS.PAYMENT_MONITOR` (already exists)
- **Status**: Partially migrated - token exists but service not properly registered
- **Usage**: Market store
- **Impact**: Background service, less direct component usage
#### 4. LnbitsAPI
- **File**: `src/lib/api/lnbits.ts`
- **Current**: `export const lnbitsAPI = new LnbitsAPI()`
- **Token**: Need to add `LNBITS_API: Symbol('lnbitsAPI')`
- **Usage**: AuthService and other core services
- **Impact**: API layer foundation, widely used but lower-level
## Migration Steps
### Step 1: Add SERVICE_TOKENS
Update `src/core/di-container.ts` to add missing tokens:
```typescript
export const SERVICE_TOKENS = {
// ... existing tokens ...
// New service tokens
INVOICE_SERVICE: Symbol('invoiceService'),
NOSTRMARKET_SERVICE: Symbol('nostrmarketService'),
LNBITS_API: Symbol('lnbitsAPI'),
// PAYMENT_MONITOR already exists
} as const
```
### Step 2: Register Services in Modules
#### Base Module Registration
For core services like InvoiceService and LnbitsAPI:
```typescript
// src/modules/base/index.ts
import { InvoiceService } from '@/core/services/invoiceService'
import { LnbitsAPI } from '@/lib/api/lnbits'
const invoiceService = new InvoiceService()
const lnbitsAPI = new LnbitsAPI()
container.provide(SERVICE_TOKENS.INVOICE_SERVICE, invoiceService)
container.provide(SERVICE_TOKENS.LNBITS_API, lnbitsAPI)
```
#### Market Module Registration
For market-specific services:
```typescript
// src/modules/market/index.ts
import { NostrmarketService } from './services/nostrmarketService'
import { PaymentMonitorService } from './services/paymentMonitor'
const nostrmarketService = new NostrmarketService()
const paymentMonitor = new PaymentMonitorService()
container.provide(SERVICE_TOKENS.NOSTRMARKET_SERVICE, nostrmarketService)
container.provide(SERVICE_TOKENS.PAYMENT_MONITOR, paymentMonitor)
```
### Step 3: Update Service Classes
Convert services to extend BaseService for dependency injection:
```typescript
// Before
export class SomeService {
constructor() {
// direct imports/dependencies
}
}
// After
export class SomeService extends BaseService {
protected readonly metadata = {
name: 'SomeService',
dependencies: ['AuthService', 'RelayHub'] // declare dependencies
}
// Access dependencies through DI
private get authService(): AuthService {
return this.dependencies.get('AuthService') as AuthService
}
}
```
### Step 4: Update Consumer Code
Replace direct imports with dependency injection:
```typescript
// Before
import { invoiceService } from '@/core/services/invoiceService'
// After
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
const invoiceService = injectService(SERVICE_TOKENS.INVOICE_SERVICE) as InvoiceService
```
### Step 5: Remove Exported Singletons
After migration, remove the exported singleton from service files:
```typescript
// Remove this line
export const someService = new SomeService()
```
## Migration Order
1. **LnbitsAPI** - Foundation API layer, needed by other services
2. **InvoiceService** - High impact business logic
3. **PaymentMonitorService** - Complete partial migration
4. **NostrmarketService** - Module-specific service
## Validation Steps
For each migrated service:
1. ✅ Service registered in appropriate module
2. ✅ SERVICE_TOKEN added to di-container.ts
3. ✅ All consumers updated to use injectService()
4. ✅ Exported singleton removed from service file
5. ✅ Service extends BaseService if applicable
6. ✅ Dependencies declared in metadata
7. ✅ No build errors or TypeScript warnings
8. ✅ Functionality testing - service works as expected
## Benefits of Migration
### Architecture Benefits
- **Single Source of Truth**: Services managed by DI container
- **Loose Coupling**: Components depend on interfaces, not concrete classes
- **Testability**: Easy to inject mock services for testing
- **Module Boundaries**: Clear service ownership and lifecycle
### Maintainability Benefits
- **Centralized Registration**: All services registered in module definitions
- **Consistent Patterns**: Same DI pattern across all services
- **Service Lifecycle**: Container manages service instantiation and disposal
- **Dependency Visibility**: Service dependencies clearly declared
### Performance Benefits
- **No Duplicate Instances**: Container ensures singleton behavior
- **Lazy Loading**: Services only created when needed
- **Efficient Memory Usage**: Single instances shared across modules
## Testing Strategy
Each migrated service should include:
1. **Unit Tests**: Test service logic with mocked dependencies
2. **Integration Tests**: Test service with real dependencies via DI
3. **Mock Service Creation**: For testing consumers
Example mock setup:
```typescript
// Test setup
const mockInvoiceService = {
createInvoice: vi.fn(),
checkPayment: vi.fn()
}
container.provide(SERVICE_TOKENS.INVOICE_SERVICE, mockInvoiceService)
```
## Risk Mitigation
- **Incremental Migration**: One service at a time
- **Backward Compatibility**: Keep old imports temporarily during migration
- **Comprehensive Testing**: Validate each service after migration
- **Rollback Plan**: Git commits allow easy rollback if issues arise
## Migration Status
### ✅ Completed Migrations
#### 1. InvoiceService (Completed)
- **Status**: ✅ Successfully migrated
- **Token**: `SERVICE_TOKENS.INVOICE_SERVICE`
- **Registration**: Base module (`src/modules/base/index.ts`)
- **Usage**: Market store (`src/modules/market/stores/market.ts`)
**Before:**
```typescript
// ❌ Legacy singleton pattern
import { invoiceService } from '@/core/services/invoiceService'
const invoice = await invoiceService.createInvoice(order, adminKey)
```
**After:**
```typescript
// ✅ Dependency injection pattern
const invoiceService = injectService(SERVICE_TOKENS.INVOICE_SERVICE) as InvoiceService
const invoice = await invoiceService.createInvoice(order, adminKey)
```
### 🔄 In Progress
#### 2. NostrmarketService (Pending)
- **Status**: 🔄 Ready for migration
- **File**: `src/modules/market/services/nostrmarketService.ts`
- **Token**: Need to add `NOSTRMARKET_SERVICE`
- **Registration**: Market module
#### 3. PaymentMonitorService (Partially Migrated)
- **Status**: 🔄 Token exists, needs completion
- **Token**: `SERVICE_TOKENS.PAYMENT_MONITOR` (already exists)
- **Registration**: Market module
#### 4. LnbitsAPI (Pending)
- **Status**: 🔄 Ready for migration
- **File**: `src/lib/api/lnbits.ts`
- **Token**: Need to add `LNBITS_API`
- **Registration**: Base module
## Practical Example: Market Module Using InvoiceService
Here's a real-world example of how the Market module now uses the migrated InvoiceService through dependency injection:
### Market Store Implementation
```typescript
// src/modules/market/stores/market.ts
import { defineStore } from 'pinia'
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
import type { InvoiceService } from '@/core/services/invoiceService'
export const useMarketStore = defineStore('market', () => {
// Inject InvoiceService via dependency injection
const invoiceService = injectService(SERVICE_TOKENS.INVOICE_SERVICE) as InvoiceService
const createOrderInvoice = async (order: Order, adminKey: string) => {
try {
// Use injected service to create Lightning invoice
const invoice = await invoiceService.createInvoice(order, adminKey, {
tag: "nostrmarket",
order_id: order.id,
merchant_pubkey: order.sellerPubkey,
expiry: 3600
})
console.log('Invoice created:', {
paymentHash: invoice.payment_hash,
bolt11: invoice.bolt11,
amount: invoice.amount
})
return invoice
} catch (error) {
console.error('Failed to create order invoice:', error)
throw error
}
}
return {
createOrderInvoice,
// ... other store methods
}
})
```
### Benefits Demonstrated
1. **Service Injection**: `injectService(SERVICE_TOKENS.INVOICE_SERVICE)` provides the service
2. **Type Safety**: TypeScript knows the exact service type with `as InvoiceService`
3. **Testability**: Easy to mock `INVOICE_SERVICE` for testing
4. **Modularity**: Market module depends on base module's InvoiceService
5. **Single Source**: One InvoiceService instance shared across the app
### Service Registration (Base Module)
```typescript
// src/modules/base/index.ts
import { InvoiceService } from '@/core/services/invoiceService'
const invoiceService = new InvoiceService()
export const baseModule: ModulePlugin = {
async install(_app: App) {
// Register InvoiceService in DI container
container.provide(SERVICE_TOKENS.INVOICE_SERVICE, invoiceService)
// Other service registrations...
}
}
```
## Success Metrics
### Completed Services
- ✅ **InvoiceService**: Successfully migrated to DI pattern
- ✅ **AuthService**: Previously migrated to DI pattern
### Remaining Tasks
- 🔄 **NostrmarketService**: Needs migration
- 🔄 **PaymentMonitorService**: Complete partial migration
- 🔄 **LnbitsAPI**: Needs migration
### Overall Progress
- ✅ 2/5 services migrated to dependency injection
- ✅ Zero build errors
- ✅ Consistent DI patterns established
- ✅ Documentation updated with examples