# BFF Database Mappers

## Overview

Database mappers in the BFF layer are responsible for transforming **Prisma entities** (database schema) into **domain types** (business entities). These are **infrastructure concerns** and belong in the BFF layer, not the domain layer.

## Key Principle

**Domain layer should NOT know about Prisma or any ORM**. Database schema is an implementation detail of the BFF layer.

```
┌─────────────┐         ┌─────────────┐         ┌─────────────┐
│   Prisma    │  ────>  │  DB Mapper  │  ────>  │   Domain    │
│  (DB Type)  │         │   (BFF)     │         │    Type     │
└─────────────┘         └─────────────┘         └─────────────┘
```

## Location

All DB mappers are centralized in:

```
apps/bff/src/infra/mappers/
```

## Naming Convention

All DB mapper functions follow this pattern:

```typescript
mapPrisma{EntityName}ToDomain(entity: Prisma{Entity}): Domain{Type}
```

### Examples:

- `mapPrismaUserToDomain()` - User entity → AuthenticatedUser
- `mapPrismaMappingToDomain()` - IdMapping entity → UserIdMapping
- `mapPrismaOrderToDomain()` - Order entity → OrderDetails (if we had one)

## When to Create a DB Mapper

Create a DB mapper when you need to:

1. ✅ Convert Prisma query results to domain types
2. ✅ Transform database timestamps to ISO strings
3. ✅ Handle nullable database fields → domain optional fields
4. ✅ Map database column names to domain property names (if different)

Do NOT create a DB mapper when:

1. ❌ Transforming external API responses (use domain provider mappers)
2. ❌ Transforming between domain types (handle in domain layer)
3. ❌ Applying business logic (belongs in domain)

## Example: User Mapper

**File**: `apps/bff/src/infra/mappers/user.mapper.ts`

```typescript
import type { User as PrismaUser } from "@prisma/client";
import type { AuthenticatedUser } from "@customer-portal/domain/auth";

/**
 * Maps Prisma User entity to Domain AuthenticatedUser type
 *
 * NOTE: Profile fields must be fetched from WHMCS - this only maps auth state
 */
export function mapPrismaUserToDomain(user: PrismaUser): AuthenticatedUser {
  return {
    id: user.id,
    email: user.email,
    role: user.role,

    // Transform database booleans
    mfaEnabled: !!user.mfaSecret,
    emailVerified: user.emailVerified,

    // Transform Date objects to ISO strings
    lastLoginAt: user.lastLoginAt?.toISOString(),
    createdAt: user.createdAt.toISOString(),
    updatedAt: user.updatedAt.toISOString(),

    // Profile fields are null - must be fetched from WHMCS
    firstname: null,
    lastname: null,
    fullname: null,
    companyname: null,
    phonenumber: null,
    language: null,
    currencyCode: null,
    address: undefined,
  };
}
```

## Example: ID Mapping Mapper

**File**: `apps/bff/src/infra/mappers/mapping.mapper.ts`

```typescript
import type { IdMapping as PrismaIdMapping } from "@prisma/client";
import type { UserIdMapping } from "@bff/modules/id-mappings/domain/index.js";

/**
 * Maps Prisma IdMapping entity to Domain UserIdMapping type
 */
export function mapPrismaMappingToDomain(mapping: PrismaIdMapping): UserIdMapping {
  return {
    id: mapping.userId, // Use userId as id since it's the primary key
    userId: mapping.userId,
    whmcsClientId: mapping.whmcsClientId,
    sfAccountId: mapping.sfAccountId, // Keep null as-is (don't convert to undefined)
    createdAt: mapping.createdAt,
    updatedAt: mapping.updatedAt,
  };
}
```

## Usage Pattern

### In Services

```typescript
import { mapPrismaMappingToDomain } from "@bff/infra/mappers";

@Injectable()
export class MappingsService {
  async findByUserId(userId: string): Promise<UserIdMapping | null> {
    const dbMapping = await this.prisma.idMapping.findUnique({
      where: { userId },
    });

    if (!dbMapping) return null;

    // Use centralized DB mapper
    return mapPrismaMappingToDomain(dbMapping);
  }
}
```

### Batch Mapping

```typescript
const dbMappings = await this.prisma.idMapping.findMany({
  where: whereClause,
});

// Map each entity
const mappings = dbMappings.map(mapping => mapPrismaMappingToDomain(mapping));
```

## Contrast with Domain Provider Mappers

### DB Mappers (BFF Infrastructure)

```typescript
// Location: apps/bff/src/infra/mappers/
// Purpose: Prisma → Domain

mapPrismaUserToDomain(prismaUser: PrismaUser): AuthenticatedUser {
  // Transforms database entity to domain type
}
```

### Provider Mappers (Domain Layer)

```typescript
// Location: packages/domain/{domain}/providers/{provider}/mapper.ts
// Purpose: External API Response → Domain

Providers.Salesforce.transformOrder(
  sfOrder: SalesforceOrderRecord
): OrderDetails {
  // Transforms external provider data to domain type
}
```

## Key Differences

| Aspect               | DB Mappers                    | Provider Mappers                      |
| -------------------- | ----------------------------- | ------------------------------------- |
| **Location**         | `apps/bff/src/infra/mappers/` | `packages/domain/{domain}/providers/` |
| **Input**            | Prisma entity                 | External API response                 |
| **Layer**            | BFF Infrastructure            | Domain                                |
| **Purpose**          | Hide ORM implementation       | Transform business data               |
| **Import in Domain** | ❌ Never                      | ✅ Internal use only                  |
| **Import in BFF**    | ✅ Yes                        | ✅ Yes                                |

## Best Practices

### ✅ DO

- Keep mappers simple and focused
- Use TypeScript types from Prisma client
- Handle null/undefined consistently
- Transform Date objects to ISO strings
- Document special cases in comments
- Centralize all DB mappers in `/infra/mappers/`

### ❌ DON'T

- Add business logic to mappers
- Call external services from mappers
- Import DB mappers in domain layer
- Create one-off mappers scattered in services
- Transform external API data here (use domain provider mappers)

## Testing

DB mappers are pure functions and easy to test:

```typescript
describe("mapPrismaUserToDomain", () => {
  it("should map Prisma user to domain user", () => {
    const prismaUser: PrismaUser = {
      id: "user-123",
      email: "test@example.com",
      role: "USER",
      mfaSecret: "secret",
      emailVerified: true,
      lastLoginAt: new Date("2025-01-01"),
      createdAt: new Date("2024-01-01"),
      updatedAt: new Date("2024-01-01"),
      // ... other fields
    };

    const result = mapPrismaUserToDomain(prismaUser);

    expect(result.id).toBe("user-123");
    expect(result.mfaEnabled).toBe(true);
    expect(result.lastLoginAt).toBe("2025-01-01T00:00:00.000Z");
  });
});
```

## Summary

DB mappers are:

- ✅ **Infrastructure concerns** (BFF layer)
- ✅ **Pure transformation functions** (Prisma → Domain)
- ✅ **Centralized** in `/infra/mappers/`
- ✅ **Consistently named** `mapPrismaXxxToDomain()`
- ❌ **Never used in domain layer**
- ❌ **Never contain business logic**

This clear separation ensures the domain layer remains pure and independent of infrastructure choices.
