# BFF Integration Layer Patterns

## Overview

The BFF (Backend for Frontend) integration layer encapsulates all external system interactions and infrastructure concerns. This document defines the patterns and best practices for creating integration services.

---

## Architecture Principles

### Domain Layer vs Integration Layer

**Domain Layer** (`packages/domain/`):
- Business types and validation schemas
- Raw provider types (data structures from external systems)
- Transformation mappers (raw → domain)
- Business validation logic
- **No infrastructure concerns**

**Integration Layer** (`apps/bff/src/integrations/`):
- Query builders (SOQL, GraphQL, etc.)
- Connection services
- Integration services
- HTTP/API clients
- **No business logic**

---

## Integration Service Pattern

### Purpose

Integration services encapsulate all interactions with a specific external system feature, providing a clean abstraction for the application layer.

### Structure

```typescript
@Injectable()
export class SalesforceOrderService {
  constructor(
    private readonly sf: SalesforceConnection,
    @Inject(Logger) private readonly logger: Logger
  ) {}

  async getOrderById(orderId: string): Promise<OrderDetails | null> {
    // 1. Build query (infrastructure concern)
    const soql = this.buildOrderQuery(orderId);
    
    // 2. Execute query
    const rawData = await this.sf.query(soql);
    
    // 3. Use domain mapper (single transformation!)
    return DomainProviders.Salesforce.transformOrder(rawData);
  }
}
```

### Key Characteristics

1. **Encapsulation**: All system-specific logic hidden from application layer
2. **Single Transformation**: Uses domain mappers, no additional mapping
3. **Returns Domain Types**: Application layer works with domain types only
4. **Infrastructure Details**: Query building, field selection, etc. stay here

---

## Query Builder Pattern

### Location

Query builders belong in **integration layer utils**, not in domain.

**Correct**: `apps/bff/src/integrations/salesforce/utils/order-query-builder.ts`  
**Wrong**: `packages/domain/orders/providers/salesforce/query.ts` ❌

### Example

```typescript
/**
 * Build field list for Order queries
 * Infrastructure concern - not business logic
 */
export function buildOrderSelectFields(
  additional: string[] = []
): string[] {
  const fields = [
    "Id",
    "AccountId",
    "Status",
    // ... all Salesforce field names
  ];
  
  return UNIQUE([...fields, ...additional]);
}
```

---

## Data Flow

### Clean Architecture Flow

```
┌─────────────────────────────────────────┐
│ Controller (HTTP)                       │
│  - API endpoints                        │
│  - Request/Response formatting          │
└──────────────┬──────────────────────────┘
               │
┌──────────────▼──────────────────────────┐
│ Orchestrator (Application)              │
│  - Workflow coordination                │
│  - Uses integration services            │
│  - Works with domain types              │
└──────────────┬──────────────────────────┘
               │
    ┌──────────┴──────────┐
    │                     │
┌───▼───────────┐  ┌──────▼──────────────┐
│ Domain        │  │ Integration         │
│ (Business)    │  │ (Infrastructure)    │
│               │  │                     │
│ • Types       │  │ • SF OrderService   │
│ • Schemas     │  │ • Query Builders    │
│ • Mappers ────┼──┤ • Connections       │
│ • Validators  │  │ • API Clients       │
└───────────────┘  └─────────────────────┘
```

### Single Transformation Principle

**One transformation path**:
```
Query (BFF) → Raw Data → Domain Mapper → Domain Type → Use Directly
             └────────── Single transformation ──────────┘
```

**No double transformation**:
```
❌ Query → Raw Data → Domain Mapper → Domain Type → BFF Mapper → ???
```

---

## Integration Service Examples

### Example 1: Salesforce Order Service

```typescript
import { SalesforceConnection } from "./salesforce-connection.service";
import { buildOrderSelectFields } from "../utils/order-query-builder";
import { Providers } from "@customer-portal/domain/orders";

@Injectable()
export class SalesforceOrderService {
  constructor(
    private readonly sf: SalesforceConnection,
    @Inject(Logger) private readonly logger: Logger
  ) {}

  async getOrderById(orderId: string): Promise<OrderDetails | null> {
    // Build query using integration layer utils
    const fields = buildOrderSelectFields(["Account.Name"]).join(", ");
    const soql = `SELECT ${fields} FROM Order WHERE Id = '${orderId}'`;

    // Execute query
    const result = await this.sf.query(soql);
    if (!result.records?.[0]) return null;

    // Use domain mapper - single transformation!
    return Providers.Salesforce.transformSalesforceOrderDetails(
      result.records[0],
      []
    );
  }
}
```

### Example 2: WHMCS Client Service

```typescript
import { WhmcsConnection } from "./whmcs-connection.service";
import { Providers } from "@customer-portal/domain/customer";

@Injectable()
export class WhmcsClientService {
  constructor(
    private readonly whmcs: WhmcsConnection,
    @Inject(Logger) private readonly logger: Logger
  ) {}

  async getClientById(clientId: number): Promise<CustomerProfile | null> {
    // Build request parameters
    const params = { clientid: clientId };

    // Execute API call
    const rawClient = await this.whmcs.call("GetClientsDetails", params);
    if (!rawClient) return null;

    // Use domain mapper - single transformation!
    return Providers.Whmcs.transformWhmcsClient(rawClient);
  }
}
```

---

## When to Create an Integration Service

### Create When

✅ You need to query/update data from an external system  
✅ The logic involves system-specific query construction  
✅ Multiple orchestrators need the same external data  
✅ You want to encapsulate provider-specific complexity

### Don't Create When

❌ Simple pass-through without any system-specific logic  
❌ One-time queries that won't be reused  
❌ Logic that belongs in domain (business rules)

---

## Orchestrator Usage Pattern

### Before (Direct Queries - Wrong)

```typescript
@Injectable()
export class OrderOrchestrator {
  constructor(private readonly sf: SalesforceConnection) {}

  async getOrder(orderId: string) {
    // ❌ Building queries in orchestrator
    const soql = `SELECT Id, Status FROM Order WHERE Id = '${orderId}'`;
    const result = await this.sf.query(soql);
    
    // ❌ Orchestrator knows about Salesforce structure
    return DomainMapper.transform(result.records[0]);
  }
}
```

### After (Integration Service - Correct)

```typescript
@Injectable()
export class OrderOrchestrator {
  constructor(
    private readonly salesforceOrderService: SalesforceOrderService
  ) {}

  async getOrder(orderId: string) {
    // ✅ Clean delegation to integration service
    return this.salesforceOrderService.getOrderById(orderId);
    // ✅ Receives domain type, uses directly
  }
}
```

---

## Mapper Usage Pattern

### Direct Domain Mapper Usage (Correct)

```typescript
import { Providers } from "@customer-portal/domain/orders";

// ✅ Use domain mapper directly - no wrapper service
const whmcsItems = Providers.Whmcs.mapFulfillmentOrderItems(items);
```

### Redundant Service Wrapper (Wrong)

```typescript
// ❌ Don't create service wrappers that just delegate
@Injectable()
export class OrderWhmcsMapper {
  mapOrderItemsToWhmcs(items: OrderItem[]) {
    // Just wraps domain mapper - no value added!
    return Providers.Whmcs.mapFulfillmentOrderItems(items);
  }
}
```

---

## Module Configuration

### Integration Module

```typescript
@Module({
  imports: [ConfigModule],
  providers: [
    SalesforceConnection,
    SalesforceOrderService,
    SalesforceAccountService,
  ],
  exports: [
    SalesforceConnection,
    SalesforceOrderService, // Export for use in application layer
  ],
})
export class SalesforceModule {}
```

### Application Module

```typescript
@Module({
  imports: [
    SalesforceModule, // Import integration modules
    WhmcsModule,
  ],
  providers: [
    OrderOrchestrator, // Uses integration services
  ],
})
export class OrdersModule {}
```

---

## Testing Integration Services

### Unit Test Pattern

```typescript
describe("SalesforceOrderService", () => {
  let service: SalesforceOrderService;
  let mockConnection: jest.Mocked<SalesforceConnection>;

  beforeEach(() => {
    mockConnection = {
      query: jest.fn(),
    } as any;
    
    service = new SalesforceOrderService(mockConnection, mockLogger);
  });

  it("should transform raw SF data using domain mapper", async () => {
    const mockRawOrder = { Id: "123", Status: "Active" };
    mockConnection.query.mockResolvedValue({
      records: [mockRawOrder],
    });

    const result = await service.getOrderById("123");

    // Verify domain mapper was used (single transformation)
    expect(result).toBeDefined();
    expect(result.id).toBe("123");
  });
});
```

---

## Benefits of This Pattern

### Architecture Cleanliness
- ✅ Clear separation of concerns
- ✅ Domain stays pure (no infrastructure)
- ✅ Integration complexity encapsulated

### Code Quality
- ✅ Easier to test (mock integration services)
- ✅ Easier to swap providers (change integration layer only)
- ✅ No duplication (single transformation path)

### Developer Experience
- ✅ Clear patterns to follow
- ✅ No confusion about where code belongs
- ✅ Self-documenting architecture

---

## Migration Checklist

When adding new external system integration:

- [ ] Create integration service in `apps/bff/src/integrations/{provider}/services/`
- [ ] Move query builders to `apps/bff/src/integrations/{provider}/utils/`
- [ ] Define raw types in domain `packages/domain/{feature}/providers/{provider}/raw.types.ts`
- [ ] Define mappers in domain `packages/domain/{feature}/providers/{provider}/mapper.ts`
- [ ] Export integration service from provider module
- [ ] Use integration service in orchestrators
- [ ] Use domain mappers directly (no wrapper services)
- [ ] Test integration service independently

---

## Related Documentation

- [Domain Package README](../../packages/domain/README.md)
- [ORDERS-ARCHITECTURE-REVIEW.md](../ORDERS-ARCHITECTURE-REVIEW.md)
- [Schema-First Approach](../../packages/domain/SCHEMA-FIRST-COMPLETE.md)

---

**Last Updated**: October 2025  
**Status**: ✅ Active Pattern

