# Module Standardization: Shared vs Domain-Specific

Quick reference for what to reuse, what to customize, and what to build fresh for each new module.

---

## What Gets Reused (Cross-Module)

### 1. Architecture Pattern (100% Reuse)
- **3-layer structure:** domain → application → infrastructure
- **Port-based dependency injection** (5 required ports)
- **Factory pattern** for repository selection (JSON/Postgres)
- **Static createDefault()** method on service
- **Repository contract tests** for adapter parity

**Where:** Every module follows this exact pattern.

---

### 2. Core Interfaces (Template, Customize Names)

#### Repository Interface Pattern
```typescript
// Pattern (applies to every module)
export interface I{DomainName}{EntityType}Repository {
  find{EntityType}ById(id: string): Promise<{EntityType} | null>;
  list{EntityTypes}(): Promise<{EntityType}[]>;
  // Or list with basic filters
  create{EntityType}(record: Create{EntityType}Record): Promise<{EntityType}>;
  // Domain-specific methods (0-2 more)
}

// Examples:
// Purchases: IPurchaseOrderRepository
// Sales: ISalesInvoiceRepository
// Inventory: IStockMovementRepository
```

**Customize:** Add 1-2 domain-specific query methods (e.g., `findPurchaseOrderBySupplierInvoice()`)

---

#### Port Interface Pattern
```typescript
// 1. ID Generator (Same for all)
export interface I{Domain}IdGeneratorPort {
  generate{Entity}Id(): string;
}

// 2. Accounting Port (Similar signature, customize names)
export interface I{Domain}AccountingPort {
  post{Entity}JournalEntry(input: {Entity}JournalInput): Promise<void>;
}

// 3. Side Effects Port (Highly domain-specific)
export interface I{Domain}SideEffectsPort {
  // Define 2-4 async operations specific to domain
  async operation1(input): Promise<void>;
  async operation2(input): Promise<OutputType>;
}

// 4. Reference Data Port (Similar read-only pattern)
export interface I{Domain}ReferenceDataPort {
  getSettings(): Record<string, unknown>;
  getDefaultBaseCurrencyCode(): string;
  getTaxRate{DomainSpecific}(id: string): number;
  convertCurrency(...): number;
  async getLookupData(): Promise<Data[]>;
  // ... other lookups
}
```

---

### 3. Application Service Skeleton

Pattern:
```typescript
export class {Entity}ApplicationService {
  constructor(
    private readonly repository: I{Entity}Repository,
    private readonly sideEffects: I{Domain}SideEffectsPort,
    private readonly referenceData: I{Domain}ReferenceDataPort = createDefault...(),
    private readonly accounting: I{Domain}AccountingPort = createDefault...(),
    private readonly idGenerator: I{Domain}IdGeneratorPort = createDefault...()
  ) {}

  static createDefault(): {Entity}ApplicationService {
    return new {Entity}ApplicationService(...create all adapters...);
  }

  async {verb}{Entity}(command: {Entity}Command, actor): Promise<Result> {
    // 1. Validate
    // 2. Generate ID
    // 3. Persist
    // 4. Execute side effects
    // 5. Post accounting
    // 6. Return result
  }
}
```

**Customize:** Implementation of validation, persistence, side effects, accounting (flow is the same)

---

### 4. Testing Template

Pattern (3-layer testing):
1. **Domain tests:** Pure functions, no mocks needed
2. **Repository contract tests:** Both JSON + Postgres adapters pass identical contract
3. **Service tests:** Mocked ports, full orchestration

```typescript
// Domain test pattern: Same for all
test('{verb} {entity} with {scenario}...', () => {
  const result = domain{Function}({input});
  assert.equal(result.field, expectedValue);
});

// Contract test pattern: Same for all
async function runRepositoryContractSuite(name, createRepo) {
  test(`${name}: create + getById + list contract`, async () => {
    const repo = createRepo();
    const payload = {..., id: 'test-id'};
    const created = await repo.create(payload);
    assert.equal(created.id, payload.id);
    // ... more assertions
  });
}

// Service test pattern: Same for all (just swap mock implementations)
test('{verb} {entity} -> expected results', async () => {
  const service = new {Entity}ApplicationService(repoMock, sideMock, refMock, acctMock, idMock);
  const result = await service.{verb}{Entity}(command, actor);
  assert.deepEqual(sideMocks.calls, ['expected', 'calls']);
});
```

---

### 5. Folder Structure (100% Consistent)

```
lib/modules/{domain}/
├── domain/
│   ├── {entity}.model.ts          # Commands, types, enums (CUSTOMIZE)
│   └── {entity}.domain.ts         # Pure logic (CUSTOMIZE)
├── application/
│   ├── {entity}.repository.ts     # Interface + record type (TEMPLATE + customize queries)
│   ├── {entity}-accounting.port.ts         # Interface only (TEMPLATE)
│   ├── {entity}-side-effects.port.ts       # Interface only (TEMPLATE)
│   ├── {entity}-reference-data.port.ts     # Interface only (TEMPLATE)
│   ├── {entity}-id-generator.port.ts       # Interface only (SHARED, no customization)
│   ├── {entity}-application-service.ts     # Implementation (TEMPLATE + customize methods)
│   └── {entity}.mapper.ts               # DTO converter (CUSTOMIZE)
└── infrastructure/
    ├── json-{entity}.repository.ts          # Adapter (CUSTOMIZE for domain)
    ├── postgres-{entity}.repository.ts      # Adapter (CUSTOMIZE for domain)
    ├── {entity}.repository.factory.ts       # Factory (TEMPLATE, copy/paste)
    ├── default-{entity}-accounting.port.ts       # Adapter wrapper (TEMPLATE)
    ├── default-{entity}-side-effects.port.ts     # Adapter wrapper (TEMPLATE)
    ├── default-{entity}-reference-data.port.ts   # Adapter wrapper (TEMPLATE)
    ├── default-{entity}-id-generator.port.ts     # Adapter wrapper (TEMPLATE)
    └── legacy-{entity}-*.ts                # Optional: extracted legacy logic
```

---

## What Gets Customized

### 1. Domain Logic (100% Unique)

**Example: Tax Calculation**
- Purchases: Input tax (recoverable), line-level or invoice-level
- Sales: Output tax (payable), same methods
- Inventory: No tax (movement only)
- Payroll: Withholding tax (complex calculation)

**Pattern:** Same architecture, completely different domain functions.

---

### 2. Side Effects (100% Domain-Specific)

**Purchases:**
```typescript
async endorseIncomingChecksForSupplier() { }
async applyInventoryForPostedOrder() { }
async createShelfDistributionTask() { }
async syncMaterialPricesFromPurchase() { }
```

**Sales:**
```typescript
async createSalesShipment() { }
async deductInventoryForPostedInvoice() { }
async syncMaterialCostFromInvoice() { }
```

**Inventory:**
```typescript
async createInternalTransfer() { }
async updateStockLevels() { }
async triggerReorderAlert() { }
```

**Pattern:** Always async, always swallows errors, no return values (fire-and-forget).

---

### 3. Repository Queries (1-2 Domain-Specific)

**Purchases:**
```typescript
interface IPurchaseOrderRepository {
  // Core CRUD
  findSupplierById()
  findCustomerById()
  createSupplierFromCustomer()
  listPurchaseOrders()
  getPurchaseOrderById()
  createPurchaseOrder()
  addSupplierBalance()
  
  // Domain-specific
  findPurchaseOrderBySupplierInvoice() // Custom query
}
```

**Sales:**
```typescript
interface ISalesInvoiceRepository {
  // Core CRUD (customers, not suppliers)
  findCustomerById()
  listSalesInvoices()
  getSalesInvoiceById()
  createSalesInvoice()
  addCustomerBalance()
  
  // Domain-specific
  findSalesInvoiceByCustomerPO() // Different custom query
}
```

**Pattern:** Keep CRUD minimal, add 0-2 domain-specific lookups.

---

### 4. Entity Attributes

**Purchases:**
- `supplierId`, `supplierName`, `supplierInvoiceNumber`
- `postingStatus`, `workflowStatus`
- `amountDue` (what we owe supplier)

**Sales:**
- `customerId`, `customerName`, `customerPONumber`
- `postingStatus`, `fulfillmentStatus` (domain-specific workflow)
- `amountDue` (what customer owes us)

**Pattern:** Same structure, different field names for domain semantics.

---

## Shared Infrastructure (No Customization)

### 1. Legacy Adapter Pattern (Copy/Paste)

```typescript
// For ANY port adapter
export class Default{Domain}{Purpose}Port implements I{Domain}{Purpose}Port {
  async operation(input): Promise<void> {
    try {
      legacyHelper(input.field1, input.field2, ...);
    } catch (error) {
      console.error('Failed to ...:', error);
      // Swallow error
    }
  }
}

export function createDefault{Domain}{Purpose}Port(): I{Domain}{Purpose}Port {
  return new Default{Domain}{Purpose}Port();
}
```

**Never customize:** Error handling, logging, swallowing strategy. These are architectural decisions.

---

### 2. Repository Factory (Template)

```typescript
export function create{Entity}Repository(): I{Entity}Repository {
  if (isPostgresProviderEnabled()) {
    return new Postgres{Entity}Repository();
  }
  return new Json{Entity}Repository();
}
```

**100% Reuse:** Copy exactly, change class names only.

---

### 3. ID Generator Default (Template)

```typescript
export class Default{Domain}IdGeneratorPort implements I{Domain}IdGeneratorPort {
  generate{Entity}Id(): string {
    return nextPrefixedId('{prefix}');
  }
}

export function createDefault{Domain}IdGeneratorPort(): I{Domain}IdGeneratorPort {
  return new Default{Domain}IdGeneratorPort();
}
```

**Customize:** Only the prefix code (`'po'` → `'si'` → `'sm'`, etc.)

---

### 4. Test Mocks Pattern (Template)

```typescript
function createRepositoryMock(): I{Entity}Repository { ... }
function createSideEffectsMock(): I{Domain}SideEffectsPort { ... }
function createReferenceDataMock(): I{Domain}ReferenceDataPort { ... }
function createAccountingMock(): I{Domain}AccountingPort { ... }
function createIdGeneratorMock(): I{Domain}IdGeneratorPort { ... }

test('Test case...', async () => {
  const service = new {Entity}ApplicationService(repo, side, ref, acct, idGen);
  const result = await service.method(command, actor);
  assert.equal('success' in result, true);
});
```

**Pattern:** Always 5 mocks, always same assertion style.

---

## Quick Decision Tree

```
Building new module? Start here:

Q1: Do I need to follow clean architecture?
↓ YES (always for production modules in ERP)
├─ Use MODULE_ARCHITECTURE.md as blueprint

Q2: Which pattern more similar to my domain?
├─ "Transactional aggregate" (invoice-like)?
│ ├─ BASE: Purchases module
│ ├─ CUSTOMIZE: Domain types, side effects, repo queries
│ └─ EXAMPLE: Sales, Quotation, PurchaseReturns
├─ "Reference data" (master file)?
│ ├─ BASE: Library pattern (simpler)
│ └─ EXAMPLE: Chart of Accounts, Tax Groups, Employees
└─ "Operational flow" (multi-step process)?
   ├─ BASE: Purchases module + orchestration
   └─ EXAMPLE: Payroll cycle, Month-end close

Q3: What's my entry point?
├─ "Create single entity" → Start with createDefault()
├─ "Update entity" → Add updateDefault() method
└─ "Query entities" → Enhance listDefault(), add filters

Q4: How many side effects?
├─ 0-1 effects → Skip SideEffectsPort, put in service
├─ 2-4 effects → Create SideEffectsPort (Purchases pattern)
└─ 5+ effects → Consider splitting into sub-modules

Q5: Do I need accounting integration?
├─ YES (revenue/expense/asset/liability) → Add AccountingPort
└─ NO (internal operation only) → Omit it; service handles direct calls
```

---

## Domain-Specific Customization Examples

### Example 1: Inventory Module
```typescript
// Domain-specific side effects
async executeStockMovement() { }
async updateBinLocations() { }
async triggerAutoReorder() { }

// Domain-specific repository method
async getStockByLocation(materialId, warehouseId, shelfId)

// Domain-specific types
export type StockMovementType = 'receipt' | 'shipment' | 'adjustment' | 'transfer';
export type LockStatus = 'available' | 'reserved' | 'locked' | 'locked-by-return';
```

### Example 2: Accounting Module
```typescript
// Domain-specific side effects
async balanceJournalEntry() { }
async detectImbalance() { }
async archivePeriod() { }

// Domain-specific repository methods
async getAccountsByType(type: 'asset' | 'liability')
async sumByAccountForPeriod(accountId, startDate, endDate)

// Domain-specific types
export type AccountType = 'asset' | 'liability' | 'equity' | 'revenue' | 'expense';
export type ReferenceType = 'purchase' | 'sale' | 'payroll' | 'adjustment';
```

### Example 3: Payroll Module
```typescript
// Domain-specific side effects
async generatePaySlips() { }
async calculateTaxWithholding() { }
async postToAccounting() { }
async createBankFile() { }

// Domain-specific repository methods
async getEmployeesByDepartment(deptId)
async getSalaryReviewByPeriod(startDate, endDate)

// Domain-specific types
export type PaymentFrequency = 'weekly' | 'biweekly' | 'monthly';
export type EmploymentType = 'full-time' | 'part-time' | 'contract';
```

---

## Common Pitfalls & Solutions

| Pitfall | Why It's Wrong | Solution |
|---------|---|---|
| Port logic in service | Violates SRP | Keep service thin; put logic in ports |
| Sync side effects | Blocks transaction | Make side effects async (fire-and-forget) |
| Repository generates ID | Breaks testability | Service generates ID, passes to repo |
| Throwing exceptions | Breaks port contracts | Return discriminated unions (error \| success) |
| Port per method | Explosion of ports | 5 core ports covers 80% of needs |
| Deep nesting of ports | Unmaintainable | Max 1 level (port → adapter → legacy) |
| Repository knows about accounting | Tight coupling | Use port at service level only |
| Skipping contract tests | Silent failures | Always test both JSON + Postgres |
| Magic numbers/strings | Brittleness | Use enums or constants from domain |
| Mixing concerns in adapters | Hard to test | Each adapter → single port only |

---

## Rollout Order (Recommended)

1. **Purchases Module** ✅ (Done - reference implementation)
2. **Sales Module** (Highest ROI - transactional, closely mirrors purchases)
3. **Inventory Module** (Reference data + operational, less complex)
4. **Payroll Module** (Complex domain logic, multi-step orchestration)
5. **Accounting Module** (Read-heavy, reporting, analysis)

Each new module gets 80% of architecture for free; focus on domain uniqueness.

---

## Integration Points

### Where Modules Connect

```
Sales Module
  ├─ Posts to Accounting Module (revenue journal)
  ├─ Triggers Inventory Module (stock deduction)
  └─ Uses Reference Data (currency, tax rates)

Purchases Module
  ├─ Posts to Accounting Module (expense journal)
  ├─ Triggers Inventory Module (stock receipt)
  └─ Uses Reference Data (currency, tax rates)

Accounting Module
  ├─ Consumes from Sales (revenue posting)
  ├─ Consumes from Purchases (expense posting)
  ├─ Publishes Period Close events
  └─ Read by Reports/Analytics

Inventory Module
  ├─ Triggered by Sales (deduction)
  ├─ Triggered by Purchases (receipt)
  ├─ Detects low stock → Reorder alert
  └─ Provides stock status to Sales/Purchases
```

**Pattern:** Modules communicate via ports, not direct imports. Action layer orchestrates if cross-module.

---

## Testing Efficiency

### Per-Module Testing (Standard)
```bash
npm run test:purchases        # All tests for Purchases
npm run test:purchases:domain # Domain logic only (~50ms)
npm run test:purchases:contracts # Both adapters (~100ms)
npm run test:purchases:service # Full service mocked (~50ms)
```

**Total:** ~200ms per module (no DB, mocked)

### Cross-Module Testing (Action Layer)
```bash
npm run test:actions # Wires modules together, integration flows
```

**When:** After each module. Before: test in isolation.

---

## Deployment Safety

✅ **Safe to Deploy Per-Module:**
- New module (no breaking changes, all tests pass)
- New service method (backward compatible, new action only)
- Refactored adapter (contract tests pass, behavior unchanged)

❌ **Requires Coordination:**
- Changing port interface (affects service + tests)
- Changing entity attributes (DB schema migration needed)
- Deleting module (check for cross-module dependencies)

**Rule:** Always run full `npm run test:purchases` before deploy; then run new module tests separately if confident.
