# Architecture Hexagonale - Crafted SaaS

## Vue d'Ensemble

Cette architecture implémente le pattern **Hexagonal Architecture** (aussi appelé Ports & Adapters), conçu pour:

1. Isoler la business logic de l'infrastructure
2. Rendre le domain testable sans dépendances externes
3. Permettre le partage du domain entre web et mobile
4. Faciliter le remplacement des technologies externes

## Les 3 Couches

### 1. Domain Layer (Cœur)

**Localisation**: `packages/domain/`

**Responsabilité**: Business logic pure, ZERO dépendance externe

**Contient**:
- **Entities**: Modèles riches avec comportement (User, Product)
- **Value Objects**: Objets immuables (Email, Money)
- **Use-Cases**: Orchestration de la logique métier
- **Repository Interfaces**: Contrats (IUserRepository)
- **Domain Errors**: Erreurs métier
- **Result Pattern**: Gestion d'erreurs fonctionnelle

**Règles strictes**:
```json
{
  "dependencies": [],              // ZERO deps
  "maxFunctionLength": 20,         // Max 20 lignes
  "maxParameters": 2,              // Max 2 params
  "maxComplexity": 5,              // Cyclomatic < 5
  "testCoverage": 100              // 100% obligatoire
}
```

**Exemple: User Entity**
```typescript
// ✅ Rich domain entity avec comportement
export class User {
  private constructor(private props: UserProps) {}

  static create(props: Omit<UserProps, 'createdAt' | 'updatedAt'>): Result<User, ValidationError> {
    if (!props.name || props.name.trim().length < 2) {
      return failure(new ValidationError('Name must be at least 2 characters'));
    }
    return success(new User({ ...props, createdAt: new Date(), updatedAt: new Date() }));
  }

  updateName(name: string): Result<void, ValidationError> {
    if (!name || name.trim().length < 2) {
      return failure(new ValidationError('Name must be at least 2 characters'));
    }
    this.props.name = name;
    this.props.updatedAt = new Date();
    return success(undefined);
  }
}
```

### 2. Application Layer

**Localisation**: `packages/api/`

**Responsabilité**: Orchestration ultra-fine, exposition API

**Contient**:
- **tRPC Routers**: Exposition des use-cases via API
- **Validation**: Schémas Zod pour l'input
- **Error Mapping**: Conversion domain errors → HTTP errors

**Règles**:
```json
{
  "dependencies": ["domain"],
  "maxFunctionLength": 10,         // Max 10 lignes
  "maxParameters": 2
}
```

**Exemple: User Router**
```typescript
// ✅ Ultra-thin router (< 10 lignes)
export const userRouter = router({
  create: publicProcedure
    .input(createUserSchema)
    .mutation(async ({ input }) => {
      const useCase = new CreateUser(userRepository);
      const result = await useCase.execute(input);

      if (isFailure(result)) {
        throw new TRPCError({ code: 'BAD_REQUEST', message: result.error.message });
      }

      return result.value.toJSON();
    }),
});
```

### 3. Infrastructure Layer

**Localisation**: `packages/infrastructure/`

**Responsabilité**: Implémentations concrètes des ports

**Contient**:
- **Database Repositories**: Implémentations Prisma
- **Payment Clients**: Stripe, Lemon Squeezy
- **Email Services**: Resend
- **Storage**: Platform-specific implementations

**Règles**:
```json
{
  "dependencies": ["domain"],
  "maxFunctionLength": 30,
  "maxParameters": 3,
  "testCoverage": 80
}
```

**Exemple: PrismaUserRepository**
```typescript
// ✅ Implémente l'interface du domain
export class PrismaUserRepository implements IUserRepository {
  constructor(private prisma: PrismaClient) {}

  async findById(id: string): Promise<Result<User | null, DomainError>> {
    try {
      const data = await this.prisma.user.findUnique({ where: { id } });
      if (!data) return success(null);
      return this.toDomain(data);
    } catch (error) {
      return failure(new DomainError('Database error', 'DB_ERROR'));
    }
  }

  private toDomain(data: { id: string; email: string; name: string }): Result<User, DomainError> {
    const emailResult = Email.create(data.email);
    if (isFailure(emailResult)) return emailResult;

    return User.create({ id: data.id, email: emailResult.value, name: data.name });
  }
}
```

## Flux de Données

### Create User Flow

```
1. UI (Web/Mobile)
   → Formulaire: { email: "test@example.com", name: "John" }

2. tRPC Router (Application Layer)
   → Validation Zod
   → Instanciation CreateUser use-case
   → Appel use-case.execute(input)

3. CreateUser Use-Case (Domain Layer)
   → Création Email value object
   → Validation business rules
   → Création User entity
   → Appel userRepository.save(user)

4. IUserRepository (Domain Interface)
   → Port défini dans le domain

5. PrismaUserRepository (Infrastructure)
   → Conversion domain → Prisma
   → Sauvegarde en DB
   → Conversion Prisma → domain

6. Retour jusqu'à l'UI
   → Result<User, Error>
```

## Dependency Flow

```
        UI Layer
           ↓
     Application Layer
      (tRPC Routers)
           ↓
      Domain Layer    ← Le cœur, ZERO deps
      (Use-Cases)
           ↓
    Repository Interface
           ↑
   Infrastructure Layer
  (Prisma, Stripe, etc.)
```

**Règle d'or**: Les dépendances pointent TOUJOURS vers le domain, jamais l'inverse.

## Result Pattern

Au lieu d'utiliser `throw` dans le domain, on utilise le **Result pattern**:

```typescript
// ❌ Antipattern: throw dans le domain
export class Email {
  static create(email: string): Email {
    if (!isValid(email)) {
      throw new Error('Invalid email'); // ❌ Side-effect
    }
    return new Email(email);
  }
}

// ✅ Correct: Result pattern
export class Email {
  static create(email: string): Result<Email, ValidationError> {
    if (!isValid(email)) {
      return failure(new ValidationError('Invalid email')); // ✅ Explicit
    }
    return success(new Email(email));
  }
}
```

**Avantages**:
- Gestion d'erreur explicite
- Type-safe (TypeScript sait qu'il y a un error)
- Testable sans try-catch
- Composable (chainable avec map/flatMap)

## Testing Strategy

### Domain Layer: Spec-First

1. **Comprendre** la business rule
2. **Écrire le test** (spécification du comportement)
3. **Implémenter** pour satisfaire le test

```typescript
// Test = Spécification
describe('User Entity', () => {
  it('should fail with name less than 2 characters', () => {
    const email = Email.create('test@example.com');
    const result = User.create({ id: '123', email: email.value, name: 'J' });

    expect(isFailure(result)).toBe(true);
    if (isFailure(result)) {
      expect(result.error.message).toBe('Name must be at least 2 characters');
    }
  });
});
```

### Infrastructure Layer: Integration Tests

```typescript
describe('PrismaUserRepository', () => {
  it('should save and retrieve user', async () => {
    const repository = new PrismaUserRepository(prisma);
    const user = User.create({ id: '123', email: emailResult.value, name: 'John' });

    await repository.save(user.value);
    const result = await repository.findById('123');

    expect(isSuccess(result)).toBe(true);
  });
});
```

## Platform-Agnostic Design

Le domain layer fonctionne sur **toutes les plateformes**:

```typescript
// ✅ Fonctionne sur:
// - Node.js (server)
// - Browser (web)
// - React Native (mobile)
// - Deno, Bun, etc.

const emailResult = Email.create('test@example.com');
const userResult = User.create({
  id: crypto.randomUUID(),
  email: emailResult.value,
  name: 'John'
});
```

**Pas d'import de**:
- ❌ Prisma
- ❌ Next.js
- ❌ React
- ❌ React Native
- ❌ Node.js specific APIs

**Seulement**:
- ✅ Pure TypeScript
- ✅ Standard JavaScript APIs (crypto, Date, etc.)

## Évolution de l'Architecture

### Ajouter une nouvelle Entity

1. Créer l'entity dans `packages/domain/src/entities/`
2. Écrire les tests dans `packages/domain/__tests__/entities/`
3. Créer l'interface repository dans `packages/domain/src/repositories/`
4. Implémenter dans `packages/infrastructure/`

### Ajouter un nouveau Use-Case

1. Créer le use-case dans `packages/domain/src/use-cases/`
2. Écrire les tests (spec-first)
3. Implémenter pour satisfaire les tests
4. Exposer via tRPC router dans `packages/api/`

### Changer de Database

1. Créer nouveau repository: `MongoUserRepository`
2. Implémenter `IUserRepository`
3. Remplacer l'injection de dépendance
4. **Le domain reste inchangé** ✅

## ADR (Architecture Decision Records)

Toutes les décisions architecturales majeures sont documentées dans `/docs/adr/`:

- ADR-001: Pourquoi Hexagonal Architecture
- ADR-002: Pourquoi Result Pattern
- ADR-003: Pourquoi ZERO deps dans domain
- ADR-004: Pourquoi Spec-First Testing

## Conclusion

Cette architecture garantit:

✅ **Testabilité**: Domain testable sans infrastructure
✅ **Maintenabilité**: Séparation claire des responsabilités
✅ **Évolutivité**: Facile d'ajouter features sans régression
✅ **Portabilité**: Domain partagé entre web et mobile
✅ **Longévité**: Architecture qui scale sur 10+ ans
