# Kohinos - Technical Specifications & Business Logic

**Version**: 6.4.26
**Last Updated**: 2025-11-24
**PHP**: 8.1+
**Symfony**: 6.4.26
**Database**: MariaDB 10.11.8

---

## Table of Contents

1. [Architecture Overview](#architecture-overview)
2. [Technology Stack](#technology-stack)
3. [Domain Model & Business Logic](#domain-model--business-logic)
4. [Core Business Rules](#core-business-rules)
5. [Architectural Patterns](#architectural-patterns)
6. [Security Architecture](#security-architecture)
7. [Testing Strategy](#testing-strategy)
8. [Performance & Optimization](#performance--optimization)

---

## Architecture Overview

Kohinos is a **comprehensive local complementary currency management system** (MLCC - Monnaie Locale Complémentaire Citoyenne) that enables:

- Management of electronic local currency (eMLc) accounts
- Automated SEPA direct debit flows for recurring subscriptions and purchases
- Solidarity Social Security for Food (SSA - Sécurité Sociale Alimentaire) with multi-fund support
- Transaction management between adherents, service providers, and exchange counters
- Payment integration with HelloAsso and PayZen

### Key Architectural Principles

1. **Domain-Driven Design (DDD)**: Business logic organized by bounded contexts
2. **Service-Oriented Architecture**: Clear separation of concerns with dedicated services
3. **Event-Driven**: Centralized event dispatching for system actions
4. **CQRS Pattern**: Separation of read and write operations for complex queries
5. **Repository Pattern**: Data access abstraction with custom query methods

---

## Technology Stack

### Backend

#### Core Framework
- **Symfony 6.4.26**: Full-stack web framework with MicroKernelTrait
- **PHP 8.1+**: Modern PHP with typed properties, attributes, union types
- **Doctrine ORM**: Database abstraction with extensive entity relationships
- **Twig**: Template engine with custom extensions

#### Key Bundles
- **Sonata Admin Bundle v4**: Administrative interface
- **API Platform v3**: REST/GraphQL API with attribute-based configuration
- **Lexik JWT Authentication**: JWT token authentication
- **Stof Doctrine Extensions**: Soft delete, timestampable, blameable

#### Payment & External Services
- **HelloAsso API**: Primary payment processor integration
- **PayZen**: Alternative payment gateway
- **SEPA**: Direct debit automation

#### Development Tools
- **GrumPHP**: Git hooks for code quality (PHP CS Fixer, PHPUnit, YAML lint)
- **PHP CS Fixer**: PSR-12 code style enforcement
- **PHPUnit 9.5**: Unit and integration testing
- **PHPStan Level 6**: Static analysis
- **Symfony Profiler**: Development debugging

### Frontend

#### Core Technologies
- **Bootstrap 5.2**: UI framework with custom Kohinos theme
- **jQuery 3.6**: DOM manipulation and AJAX
- **Webpack Encore**: Asset bundling and optimization
- **Sass/SCSS**: CSS preprocessing

#### Libraries
- **Chart.js**: Data visualization
- **Leaflet**: Interactive maps
- **CKEditor 4.12.1**: Rich text editing
- **Select2**: Enhanced select inputs
- **Font Awesome**: Icon library

#### PWA Features
- **Service Worker** (Workbox): Offline functionality
- **Web App Manifest**: App metadata
- **Push Notifications**: User engagement

### Database

- **MariaDB 10.11.8**: Primary database
- **SQLite (in-memory)**: Test environment
- **Doctrine Migrations**: Schema versioning
- **Custom DQL Functions**: Specialized queries

---

## Domain Model & Business Logic

### Core Entities (78+ total)

#### 1. User Management Domain

##### User
Central authentication entity with role-based access control.

**Key Fields**:
- `email`: Unique identifier
- `roles`: Array of roles (ROLE_USER, ROLE_ADMIN, ROLE_SUPER_ADMIN)
- `password`: Encrypted password hash
- `enabled`: Account status
- `lastLogin`: Last authentication timestamp

**Business Rules**:
- Email must be unique across the system
- Password must meet minimum security requirements (8+ chars, mixed case, numbers)
- Users can have multiple roles simultaneously
- Disabled users cannot authenticate but data is preserved (soft delete)

##### Adherent
Member of the local currency system.

**Key Fields**:
- `user`: OneToOne relationship to User
- `nom`, `prenom`: Personal information
- `dateNaissance`: Birth date for age verification
- `accepteEmails`: Email notification preference
- `compte`: OneToOne to AccountAdherent

**Business Rules**:
- Must be 18+ years old to create account
- Can have multiple SEPA mandates but only one active per time
- Can participate in SSA if linked to a SolidoumeParameter
- Account balance can be negative (overdraft) with limits

##### Prestataire
Service provider accepting local currency.

**Key Fields**:
- `user`: OneToOne relationship to User
- `nomEntreprise`: Business name
- `siret`: French business identifier
- `categoriePrestataire`: Business category
- `compte`: OneToOne to AccountPrestataire

**Business Rules**:
- SIRET must be valid and unique
- Can convert eMLc to euros at exchange rate defined in GlobalParameter
- Cannot participate in SSA (adherents only)
- Can have recurring purchase requests from adherents

##### Comptoir
Exchange counter for cash/eMLc transactions.

**Key Fields**:
- `nom`: Counter name
- `lieu`: Location
- `responsable`: User managing the counter
- `compte`: OneToOne to AccountComptoir

**Business Rules**:
- Must have a physical location
- Can perform cash-in/cash-out operations
- Transactions require receipt generation
- Balance must be reconciled daily

#### 2. Account Management Domain

##### Account (Abstract Base Class)
Base class for all account types with common balance management.

**Key Fields**:
- `balance`: Current balance in eMLc
- `libelle`: Account label/description
- `operations`: OneToMany to Operation entities

**Derived Types**:
- `AccountAdherent`: Adherent account (can be negative)
- `AccountPrestataire`: Provider account (must stay positive)
- `AccountComptoir`: Counter account (cash management)
- `AccountGroupe`: Group/organization account
- `AccountSiege`: System main account
- `AccountSsa`: SSA fund account
- `AccountSolidoume`: Solidarity fund account

**Business Rules**:
- All balance modifications must create an Operation record
- Balance changes must be atomic (database transactions)
- Each operation must have a corresponding reverse operation for double-entry bookkeeping
- Negative balances only allowed for AccountAdherent with limits

##### Operation
Individual accounting operation (debit or credit).

**Key Fields**:
- `typeOpera`: Operation type (CREDITACHAT, DEBITACHAT, CREDITVIREMENT, etc.)
- `montant`: Amount in eMLc
- `flux`: ManyToOne to Flux (links paired operations)
- `account`: ManyToOne to Account
- `dateOpera`: Operation timestamp
- `hashPrevious`: SHA-256 hash of previous operation (blockchain-style integrity)
- `hashCurrent`: SHA-256 hash of current operation

**Business Rules**:
- Operations are immutable once created
- Each operation must belong to exactly one Flux
- Hash chain ensures operation integrity (tamper detection)
- Operations cannot be deleted, only reversed with compensating operations

##### Flux
Transaction linking two operations (double-entry bookkeeping).

**Key Fields**:
- `operationIn`: Credit operation (ManyToOne)
- `operationOut`: Debit operation (ManyToOne)
- `typeFlux`: Transaction type (ACHAT_EMLC, VIREMENT, COTISATION, etc.)
- `dateFlux`: Transaction timestamp
- `adherent`/`prestataire`/`comptoir`: Parties involved

**Business Rules**:
- Must link exactly two operations: one debit, one credit
- Total debits must equal total credits (balance)
- Flux is created atomically with both operations in a database transaction
- Cannot be modified after creation, only compensated with reverse Flux

#### 3. SEPA Payment Domain

##### SepaMandate
SEPA direct debit authorization.

**Key Fields**:
- `iban`: IBAN (34 chars max, validated)
- `bic`: BIC/SWIFT code (11 chars max, optional for French IBANs)
- `accountHolderName`: Account holder name
- `mandateReference`: Unique UMR (generated on creation after first flush)
- `status`: pending, active, cancelled
- `adherent`/`prestataire`: Entity owning the mandate
- `signedAt`: Signature date
- `validatedAt`: Treasurer validation date

**Business Rules**:
- IBAN format must be validated (checksum verification)
- Only one active mandate per adherent/prestataire at a time
- Mandates require treasurer validation before activation
- Cancelled mandates cannot be reactivated (must create new one)
- UMR format: `KOHINOS-YYYYMMDD-XXXXXXXX` (where X is first 8 chars of UUID without dashes)
- UMR is generated immediately on creation (after first flush to get UUID), not on validation
- Example: `KOHINOS-20251215-A3B4C5D6`

**State Machine**:
```
pending → (treasurer validates) → active
pending → (user cancels) → cancelled
active → (user cancels) → cancelled [+ all recurring requests cancelled]
active → (IBAN change) → cancelled [+ all recurring requests cancelled] → new pending created
```

**Cancellation Side Effects**:
When a mandate transitions to `cancelled` state (either by user action or IBAN change):
- All associated `RecurringMlcSubscriptionRequest` entities are cancelled
- All associated `RecurringSsaSubscriptionRequest` entities are cancelled
- All associated `RecurringPurchaseRequest` entities are cancelled
- The operation is atomic and logged
- User receives detailed feedback on what was cancelled

##### RecurringMlcSubscriptionRequest
Annual MLC membership subscription request.

**Key Fields**:
- `amount`: Annual subscription amount in euros
- `paymentMethod`: MOYEN_VIREMENT, MOYEN_EMLC, or MOYEN_SEPA
- `year`: Civil year (2025, 2026...)
- `status`: pending, validated, cancelled
- `sepaMandate`: Required if paymentMethod is MOYEN_SEPA
- `autoDebitDate`: Debit date (9th January for eMLc, configurable for SEPA)

**Business Rules**:
- One subscription per adherent/prestataire per year
- SEPA payment requires active mandate + treasurer validation
- eMLc payment: automatic debit on 9th January at midnight
- Amount can be modified before validation
- Validated subscriptions cannot be cancelled (must contact support)

**Debit Logic**:
```php
// For SEPA (euros)
if ($request->getPaymentMethod() === oyenEnum::MOYEN_SEPA) {
    // Debit on SEPA_ANNUAL_DEBIT_DATE (from GlobalParameter)
    // Executed by kohinos:sepa:process-monthly command
}

// For eMLc
if ($request->getPaymentMethod() === MoyenEnum::MOYEN_EMLC) {
    // Automatic debit on 9th January
    // Executed by kohinos:mlc:process-annual-subscriptions command
}
```

##### RecurringSsaSubscriptionRequest
Monthly SSA (food security) subscription request.

**Key Fields**:
- `amount`: Monthly contribution in euros or eMLc
- `paymentMethod`: MOYEN_SEPA or MOYEN_EMLC
- `solidoumeParameter`: SSA fund (ManyToOne, REQUIRED)
- `debitDay`: Day of month (1-28) for SEPA debit
- `status`: pending, validated, cancelled
- `sepaMandate`: Required if paymentMethod is MOYEN_SEPA

**Business Rules**:
- **ADHERENTS ONLY** (prestataires cannot participate in SSA)
- Must be linked to a specific SSA fund (solidoumeParameter)
- Debit day limited to 1-28 to avoid short month issues
- SEPA debits processed monthly on configured day
- eMLc debits processed monthly based on SSA fund configuration
- Can have pending modification requests (amount or debit day changes)

**Modification Workflow**:
```
1. User requests modification (newAmountRequested, modificationRequestedAt set)
2. Treasurer reviews and approves/rejects
3. If approved: amount/debitDay updated, modification fields cleared
4. If rejected: modification fields cleared, email sent to user
```

##### RecurringPurchaseRequest
Monthly recurring purchase of local currency.

**Key Fields**:
- `monthlyAmount`: Amount in euros to purchase monthly
- `debitDay`: Day of month (1-28) for debit
- `status`: pending, active, cancelled
- `lastProcessedMonth`: Format YYYYMM (prevents duplicate processing)
- `sepaMandate`: Required (ManyToOne)
- `adherent`/`prestataire`: Entity making recurring purchases

**Business Rules**:
- Requires active SEPA mandate (euros only)
- Debit day: 1-28 of each month
- Anti-duplicate: check `lastProcessedMonth` before processing
- Validation required before first debit
- Can request modifications (amount or debit day)

**Processing Logic**:
```php
public function shouldProcessForMonth(string $currentMonth): bool
{
    if ($this->status !== self::STATUS_ACTIVE) {
        return false;
    }

    // Format: YYYYMM (ex: 202501 for January 2025)
    return $this->lastProcessedMonth !== $currentMonth;
}

public function markAsProcessed(string $currentMonth): void
{
    $this->lastProcessedMonth = $currentMonth;
    // Persist to database
}
```

#### 4. SSA (Solidarity Social Security for Food) Domain

##### SolidoumeParameter
Configuration for an SSA fund (multi-fund support).

**Key Fields**:
- `name`: Fund name (e.g., "SSA Grenoble", "SSA Lyon")
- `debitDay`: Default debit day for eMLc subscriptions (1-28)
- `redistributionDay`: Day of month for redistribution (1-28)
- `enabled`: Active/inactive status
- `montantMensuelRemboursable`: Monthly reimbursable amount per participant
- `seuilAlerte`: Alert threshold for low fund balance
- `items`: OneToMany to SolidoumeItem (participants)

**Business Rules**:
- Each SSA fund is independent (separate accounting)
- Can be disabled without deleting participant data
- Redistribution occurs monthly on configured day
- Alert triggered when fund balance < seuilAlerte
- Multiple funds can coexist for different geographical regions

##### SolidoumeItem
SSA participant record (adherent's participation in a fund).

**Key Fields**:
- `adherent`: ManyToOne to Adherent
- `solidoumeParameter`: ManyToOne to SolidoumeParameter
- `amount`: Monthly contribution amount
- `dateStarting`: Participation start date
- `dateEnd`: End date (null if active)
- `debitDate`: Last successful debit date
- `status`: active, suspended, terminated

**Business Rules**:
- One participation per adherent per SSA fund
- Contribution amount can be modified (requires validation)
- Participation can be suspended (temporary) or terminated (permanent)
- Debit failures: 3 consecutive failures → automatic suspension
- Reactivation: requires payment of missed months + new debit success

##### PendingPaymentData
Queue for pending SSA payments and notifications.

**Key Fields**:
- `adherent`: ManyToOne to Adherent
- `solidoumeItem`: ManyToOne to SolidoumeItem
- `amount`: Amount to process
- `type`: reminder, taking, redistribution
- `status`: pending, processed, failed
- `scheduledFor`: Scheduled execution date
- `attempts`: Number of processing attempts
- `lastError`: Last error message (for debugging)

**Business Rules**:
- Created by SSA command for asynchronous processing
- Max 3 attempts before marking as failed
- Processed payments are soft-deleted after retention period (GlobalParameter)
- Used for retry logic on transient failures

#### 5. Payment Integration Domain

##### HelloAssoPayment
HelloAsso payment tracking.

**Key Fields**:
- `orderId`: HelloAsso order ID
- `paymentId`: HelloAsso payment ID
- `amount`: Payment amount in euros
- `status`: pending, completed, failed, refunded
- `adherent`/`prestataire`: Payer
- `metadata`: JSON field with additional data
- `callbackReceived`: Webhook received flag

**Business Rules**:
- Payment status updated via webhook callback
- Failed payments can be retried (user action required)
- Refunds create compensating Flux operations
- Payment data retention: configured in GlobalParameter

##### GlobalParameter
System-wide configuration parameters.

**Key Fields**:
- `parameterKey`: Unique key (e.g., SEPA_ANNUAL_DEBIT_DATE)
- `parameterValue`: Value (string, serialized for complex types)
- `description`: Human-readable description
- `type`: string, integer, boolean, date, json

**Key Parameters**:
- `SEPA_ANNUAL_DEBIT_DATE`: Date for annual SEPA subscriptions (format: MM-DD)
- `SEPA_CONTACT_EMAIL`: Email for SEPA-related communications
- `PAYMENT_DATA_RETENTION_DAYS`: Days to retain old payment data
- `OVERDRAFT_LIMIT_ADHERENT`: Maximum negative balance for adherents
- `CONVERSION_RATE_EMLC_EURO`: Conversion rate eMLc ↔ Euro

---

## Core Business Rules

### 1. Double-Entry Bookkeeping

**Rule**: Every transaction must create two operations: one debit and one credit.

```php
// Example: Adherent purchases 50 eMLc from Comptoir
Flux::create([
    'typeFlux' => 'ACHAT_EMLC',
    'operationOut' => Operation::create([
        'typeOpera' => 'DEBITACHAT',
        'montant' => 50.00,
        'account' => $comptoir->getCompte(), // Comptoir loses eMLc
    ]),
    'operationIn' => Operation::create([
        'typeOpera' => 'CREDITACHAT',
        'montant' => 50.00,
        'account' => $adherent->getCompte(), // Adherent gains eMLc
    ]),
]);

// Invariant: operationOut->montant === operationIn->montant
```

### 2. Operation Hash Chain (Blockchain-style Integrity)

**Rule**: Each operation contains a hash of the previous operation to ensure data integrity.

```php
class Operation
{
    public function calculateHash(): string
    {
        $data = sprintf(
            '%s|%s|%s|%s',
            $this->id,
            $this->montant,
            $this->dateOpera->format('Y-m-d H:i:s'),
            $this->hashPrevious
        );

        return hash('sha256', $data);
    }

    // Verification
    public function verifyIntegrity(): bool
    {
        return $this->hashCurrent === $this->calculateHash();
    }
}
```

### 3. SEPA Debit Day Constraints

**Rule**: Debit day must be between 1 and 28 to avoid February issues.

```php
class RecurringPurchaseRequest
{
    public function setDebitDay(int $day): void
    {
        if ($day < 1 || $day > 28) {
            throw new \InvalidArgumentException(
                'Debit day must be between 1 and 28 to handle all months'
            );
        }
        $this->debitDay = $day;
    }
}
```

### 4. SSA Redistribution Logic

**Rule**: Redistribution allocates funds based on participation amounts and fund balance.

```php
// Simplified algorithm
foreach ($participants as $participant) {
    $availableForParticipant = min(
        $participant->getMontantMensuelRemboursable(),
        $fundBalance / count($participants)
    );

    if ($availableForParticipant > 0) {
        // Create Flux: SSA Account → Participant Account
        $this->createRedistributionFlux($participant, $availableForParticipant);
        $fundBalance -= $availableForParticipant;
    }
}
```

### 5. SEPA Mandate State Machine

**Rule**: Mandates follow strict state transitions.

```
[pending] →(validate)→ [active] →(change IBAN)→ [cancelled]
    ↓(reject)             ↓(cancel mandate)
[cancelled]           [cancelled]
```

**Important**: When a mandate is cancelled (by user or due to IBAN change):
- All associated recurring MLC subscriptions are automatically cancelled
- All associated recurring SSA subscriptions are automatically cancelled
- All associated recurring purchases are automatically cancelled
- The cancellation is atomic - either all succeed or all fail
- User is shown a comprehensive modal listing all affected payments before confirmation
- Use `SepaMandateService::cancelMandateWithRecurringRequests()` for proper cleanup

### 6. Modification Request Approval Workflow

**Rule**: Subscription/purchase modifications require treasurer approval.

```php
class RecurringSsaSubscriptionRequest
{
    public function requestModification(float $newAmount): void
    {
        if ($this->status !== self::STATUS_VALIDATED) {
            throw new \LogicException('Can only modify validated subscriptions');
        }

        $this->newAmountRequested = $newAmount;
        $this->modificationRequestedAt = new \DateTime();
    }

    public function approveModification(): void
    {
        if (!$this->hasModificationRequest()) {
            throw new \LogicException('No pending modification');
        }

        $this->amount = $this->newAmountRequested;
        $this->newAmountRequested = null;
        $this->modificationRequestedAt = null;
    }
}
```

### 7. Account Balance Rules

**Rule**: Balance constraints vary by account type.

| Account Type | Negative Balance | Notes |
|-------------|------------------|-------|
| AccountAdherent | ✅ Allowed (with limit) | Limit defined in GlobalParameter |
| AccountPrestataire | ❌ Not allowed | Must stay >= 0 |
| AccountComptoir | ❌ Not allowed | Cash management, must reconcile |
| AccountSsa | ❌ Not allowed | Fund cannot go negative |

### 8. SSA Participation Rules

**Rule**: Only adherents can participate in SSA, not prestataires.

```php
// In SSA subscription controller
if (!$user->getAdherent()) {
    throw new AccessDeniedException('SSA is only available for adherents');
}
```

### 9. Payment Method Validation

**Rule**: SEPA payment methods require active mandate.

```php
if ($paymentMethod === self::MOYEN_SEPA && !$sepaMandate) {
    throw new \InvalidArgumentException('SEPA payment requires a mandate');
}

if ($sepaMandate && $sepaMandate->getStatus() !== SepaMandate::STATUS_ACTIVE) {
    throw new \InvalidArgumentException('SEPA mandate must be active');
}
```

### 10. Anti-Duplicate Processing

**Rule**: Use idempotency keys to prevent duplicate processing.

```php
// For recurring purchases
$currentMonth = (new \DateTime())->format('Ym'); // 202501

if ($request->getLastProcessedMonth() === $currentMonth) {
    // Already processed this month, skip
    return;
}

// Process...

$request->markAsProcessed($currentMonth);
```

---

## Architectural Patterns

### 1. Service Layer Pattern

**Pattern**: Business logic encapsulated in dedicated services.

```
Controller → Service → Repository → Entity
```

**Example**:
```php
class RecurringSubscriptionService
{
    public function __construct(
        private EntityManagerInterface $em,
        private SepaMandateService $mandateService,
        private NotificationService $notificationService,
        private LoggerInterface $logger
    ) {}

    public function createSubscription(
        Adherent $adherent,
        float $amount,
        string $paymentMethod
    ): RecurringSsaSubscriptionRequest {
        // Validation
        $this->validateAmount($amount);
        $this->validatePaymentMethod($paymentMethod, $adherent);

        // Business logic
        $subscription = new RecurringSsaSubscriptionRequest();
        $subscription->setAdherent($adherent);
        $subscription->setAmount($amount);
        $subscription->setPaymentMethod($paymentMethod);

        $this->em->persist($subscription);
        $this->em->flush();

        // Side effects
        $this->notificationService->notifyNewSubscription($subscription);
        $this->logger->info('Subscription created', ['id' => $subscription->getId()]);

        return $subscription;
    }
}
```

### 2. Repository Pattern

**Pattern**: Data access abstraction with custom query methods.

```php
class RecurringPurchaseRequestRepository extends ServiceEntityRepository
{
    /**
     * Find all active requests to process for a given month.
     */
    public function findActiveForMonth(\DateTime $date): array
    {
        $monthKey = $date->format('Ym');

        return $this->createQueryBuilder('r')
            ->where('r.status = :active')
            ->andWhere('r.debitDay <= :day')
            ->andWhere('r.lastProcessedMonth != :month OR r.lastProcessedMonth IS NULL')
            ->setParameter('active', RecurringPurchaseRequest::STATUS_ACTIVE)
            ->setParameter('day', (int) $date->format('d'))
            ->setParameter('month', $monthKey)
            ->getQuery()
            ->getResult();
    }
}
```

### 3. Event Dispatcher Pattern

**Pattern**: Decouple actions from side effects using events.

```php
// Event
class FluxCreatedEvent extends Event
{
    public function __construct(
        private Flux $flux
    ) {}

    public function getFlux(): Flux
    {
        return $this->flux;
    }
}

// Listener
class FluxNotificationListener
{
    public function onFluxCreated(FluxCreatedEvent $event): void
    {
        $flux = $event->getFlux();

        if ($this->shouldNotify($flux)) {
            $this->mailer->send($this->buildEmail($flux));
        }
    }
}

// Dispatcher
$event = new FluxCreatedEvent($flux);
$this->eventDispatcher->dispatch($event, FluxCreatedEvent::NAME);
```

### 4. Strategy Pattern (SSA Redistribution)

**Pattern**: Different redistribution algorithms based on fund configuration.

```php
interface RedistributionStrategyInterface
{
    public function redistribute(
        SolidoumeParameter $ssa,
        array $participants,
        float $availableAmount
    ): array;
}

class EqualShareStrategy implements RedistributionStrategyInterface
{
    public function redistribute(
        SolidoumeParameter $ssa,
        array $participants,
        float $availableAmount
    ): array {
        $perParticipant = $availableAmount / count($participants);
        // ...
    }
}

class ProportionalStrategy implements RedistributionStrategyInterface
{
    public function redistribute(
        SolidoumeParameter $ssa,
        array $participants,
        float $availableAmount
    ): array {
        // Distribute based on contribution amounts
    }
}
```

### 5. Factory Pattern

**Pattern**: Complex object creation encapsulated.

```php
class SepaMandateFactory
{
    public function createForAdherent(
        Adherent $adherent,
        string $iban,
        ?string $bic,
        string $accountHolderName
    ): SepaMandate {
        $mandate = new SepaMandate();
        $mandate->setAdherent($adherent);
        $mandate->setIban($this->formatIban($iban));
        $mandate->setBic($bic);
        $mandate->setAccountHolderName($accountHolderName);
        $mandate->setStatus(SepaMandate::STATUS_PENDING);
        $mandate->setCreatedAt(new \DateTime());

        return $mandate;
    }

    private function formatIban(string $iban): string
    {
        return strtoupper(str_replace(' ', '', $iban));
    }
}
```

### 6. Command Pattern (Symfony Console)

**Pattern**: Encapsulate operations as commands.

```php
#[AsCommand(
    name: 'kohinos:sepa:process-monthly',
    description: 'Process monthly SEPA debits'
)]
class SepaProcessingCommand extends Command
{
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);

        try {
            $results = $this->sepaService->processMonthlyDebits();

            $io->success(sprintf(
                'Processed %d debits successfully',
                $results['processed']
            ));

            return Command::SUCCESS;
        } catch (\Exception $e) {
            $io->error($e->getMessage());
            return Command::FAILURE;
        }
    }
}
```

---

## Security Architecture

### Authentication

1. **Custom Authenticator**: `AppAuthenticator` class handling login
2. **JWT Tokens**: API authentication via Lexik JWT bundle
3. **Session Management**:
   - Development: 1 year timeout
   - Production: 2 hours timeout
4. **Password Requirements**:
   - Minimum 8 characters
   - Must contain uppercase, lowercase, and numbers
   - Hashed with bcrypt (cost factor 13)

### Authorization

**Role Hierarchy**:
```yaml
ROLE_USER:
  - Basic authenticated user

ROLE_ADMIN:
  - Inherits ROLE_USER
  - Access to admin dashboard
  - Can manage entities

ROLE_SUPER_ADMIN:
  - Inherits ROLE_ADMIN
  - System configuration access
  - User management
  - Full database access
```

**Voters**: Custom authorization logic

```php
class FluxVoter extends Voter
{
    protected function supports(string $attribute, mixed $subject): bool
    {
        return $subject instanceof Flux && in_array($attribute, ['VIEW', 'EDIT']);
    }

    protected function voteOnAttribute(
        string $attribute,
        mixed $subject,
        TokenInterface $token
    ): bool {
        $user = $token->getUser();
        $flux = $subject;

        // User can view their own flux
        if ($attribute === 'VIEW') {
            return $flux->getAdherent()?->getUser() === $user
                || $flux->getPrestataire()?->getUser() === $user
                || $this->security->isGranted('ROLE_ADMIN');
        }

        // Only admins can edit
        return $this->security->isGranted('ROLE_ADMIN');
    }
}
```

### CSRF Protection

- All forms include CSRF tokens
- Tokens validated on submission
- Token regeneration after login

### Input Validation

```php
class RecurringPurchaseFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('monthlyAmount', MoneyType::class, [
                'constraints' => [
                    new NotBlank(),
                    new GreaterThan(['value' => 0]),
                    new LessThanOrEqual(['value' => 1000]),
                ],
            ])
            ->add('debitDay', ChoiceType::class, [
                'choices' => range(1, 28),
                'constraints' => [
                    new NotBlank(),
                    new Range(['min' => 1, 'max' => 28]),
                ],
            ]);
    }
}
```

---

## Testing Strategy

### Test Pyramid

```
        /\
       /  \     E2E Tests (5%)
      /----\    Integration Tests (15%)
     /------\   Unit Tests (80%)
    /--------\
```

### Unit Tests

**Coverage**: 80%+ of service and entity logic

**Example**:
```php
class SsaReminderServiceTest extends TestCase
{
    private SsaReminderService $service;
    private EntityManagerInterface $em;
    private MailerInterface $mailer;

    protected function setUp(): void
    {
        $this->em = $this->createMock(EntityManagerInterface::class);
        $this->mailer = $this->createMock(MailerInterface::class);

        $this->service = new SsaReminderService($this->em, $this->mailer);
    }

    public function testExecuteRemindersWithEmptyItems(): void
    {
        $ssa = $this->createMock(SolidoumeParameter::class);
        $ssa->method('getName')->willReturn('Test SSA');

        $repo = $this->createMock(EntityRepository::class);
        $repo->method('findBy')->willReturn([]);

        $this->em->method('getRepository')->willReturn($repo);

        $results = $this->service->executeReminders($ssa);

        $this->assertEquals(0, $results['reminders_sent']);
    }
}
```

### Integration Tests

**Coverage**: Full workflow testing with database

```php
class SsaCompleteWorkflowTest extends KernelTestCase
{
    public function testCompleteSSAWorkflow(): void
    {
        // 1. Create adherent
        $adherent = $this->createAdherent();

        // 2. Create SSA fund
        $ssa = $this->createSsaFund();

        // 3. Subscribe adherent
        $subscription = $this->subscriptionService->subscribe($adherent, $ssa, 20.00);
        $this->em->flush();

        // 4. Process debit
        $results = $this->ssaCommand->executeTaking($ssa);

        // 5. Verify debit
        $this->assertEquals(1, $results['taken']);
        $this->assertEquals(-20.00, $adherent->getCompte()->getBalance());
    }
}
```

### Functional Tests

**Coverage**: Controller actions and form submissions

```php
class RecurringSubscriptionControllerTest extends WebTestCase
{
    public function testSubscriptionForm(): void
    {
        $client = static::createClient();
        $client->loginUser($this->createUser());

        $crawler = $client->request('GET', '/ssa/subscribe');

        $form = $crawler->selectButton('Subscribe')->form([
            'subscription[amount]' => '25.00',
            'subscription[paymentMethod]' => 'MOYEN_EMLC',
        ]);

        $client->submit($form);

        $this->assertResponseRedirects('/ssa/dashboard');
        $this->assertEmailIsSent(); // Confirmation email
    }
}
```

### Test Data Providers

**Pattern**: Centralized test fixtures

```php
class TestDataProvider
{
    public static function createAdherent(EntityManagerInterface $em): Adherent
    {
        $user = new User();
        $user->setEmail('test@example.com');
        $user->setPassword('hashed_password');

        $adherent = new Adherent();
        $adherent->setUser($user);
        $adherent->setNom('Dupont');
        $adherent->setPrenom('Jean');

        $account = new AccountAdherent();
        $account->setBalance(0);
        $account->setAdherent($adherent);

        $em->persist($user);
        $em->persist($adherent);
        $em->persist($account);

        return $adherent;
    }
}
```

---

## Performance & Optimization

### Database Optimization

1. **Indexes**:
   ```sql
   CREATE INDEX idx_flux_date ON flux (dateFlux);
   CREATE INDEX idx_operation_account ON operation (account_id, dateOpera);
   CREATE INDEX idx_sepa_mandate_status ON sepa_mandate (status);
   ```

2. **Query Optimization**:
   - Use Doctrine Query Builder for complex queries
   - Implement pagination for large result sets
   - Use `partial` selects for limited fields

3. **Eager Loading**:
   ```php
   $qb = $this->createQueryBuilder('f')
       ->select('f', 'oin', 'oout')
       ->leftJoin('f.operationIn', 'oin')
       ->leftJoin('f.operationOut', 'oout');
   ```

### Caching Strategy

1. **Doctrine Query Cache**: 1 hour TTL for static data
2. **Template Caching**: Production only
3. **Asset Caching**: Versioned filenames (cache busting)

### Async Processing

1. **Symfony Messenger**: Background jobs for emails and heavy operations
2. **Cron Jobs**: Scheduled SEPA and SSA processing

---

## Deployment & Operations

### Environment Configuration

- **Development**: Full error reporting, profiler enabled, 1-year sessions
- **Production**: Error logging only, profiler disabled, 2-hour sessions, HTTPS enforced

### Monitoring

- **Logs**: Monolog with rotation (7 days retention)
- **Alerts**: Critical errors sent via email to SEPA_CONTACT_EMAIL
- **Health Checks**: `/health` endpoint for monitoring

### Backup Strategy

- **Database**: Daily automated dumps (30 days retention)
- **Files**: Weekly backups of uploads and media

---

**Maintained by**: Kohinos Development Team
**Documentation**: See [DOCUMENTATION.md](DOCUMENTATION.md) for detailed guides
**Architecture Decisions**: See [docs/adr/](docs/adr/) for ADR records
