Aller au contenu

Budget Lines Management (BUDMGT) — Technical Architecture

Module: BUDMGT (Application Plane) Version: 1.0 Date: 2026-02-28 Author: ARCHITECT (Claude Code) Input documents: - docs/prd/budget-lines-management-prd.md (PRD+FSD v1.0) - docs/architecture/platform-saas-blueprint.md (Blueprint v2) - docs/architecture/PROGRESS.md (Cross-module decisions ADR-01 through ADR-12) - docs/architecture/control-plane-arch.md (Control Plane reference)


1. System Context

1.1 Module Positioning

BUDMGT is an Application Plane module — it lives in com.altarys.papillon.modules.budget and handles tenant business data (budget lines, categories, transfers). It is NOT a Control Plane module.

┌──────────────────────────────────────────────────────────────────────────┐
│                         ALTARYS PLATFORM                                │
│                                                                         │
│  ┌────────────────────────────┐    ┌─────────────────────────────────┐  │
│  │    CONTROL PLANE            │    │     APPLICATION PLANE            │  │
│  │                             │    │                                  │  │
│  │  tenant/ ─────────────────────→  │  budget/ ◀── THIS MODULE        │  │
│  │    ModuleActivated     │    │    BudgetMutationApi (sync)      │  │
│  │    TenantSettings           │    │    BudgetThresholdReached (async)│  │
│  │                             │    │                                  │  │
│  │  identity/ ─────────────────────→│                                  │  │
│  │    Auth, RBAC, permissions  │    │  commitment/ (COMMIT) ──────────→│  │
│  │                             │    │    CommitmentApproved             │  │
│  │  organization/ ─────────────────→│    CommitmentCancelled            │  │
│  │    OrgUnit, Employee        │    │                                  │  │
│  │                             │    │  expense/ (EXPMGT) ─────────────→│  │
│  │  notification/ ◀───────────────  │    ExpenseApproved               │  │
│  │    BudgetThresholdReached   │    │                                  │  │
│  │    BudgetTransferRequested  │    │                                  │  │
│  └────────────────────────────┘    └─────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────────────┘

1.2 Module Activation

BUDMGT is not sellable standalone. It auto-activates when EXPMGT or COMMIT is active:

ModuleActivated(EXPMGT) ──→ Check: BUDMGT active? If not → activate BUDMGT → seed categories
ModuleActivated(COMMIT) ──→ Check: BUDMGT active? If not → activate BUDMGT → seed categories
ModuleDeactivated(EXPMGT) ─→ Check: COMMIT still active? If not → deactivate BUDMGT
ModuleDeactivated(COMMIT) ─→ Check: EXPMGT still active? If not → deactivate BUDMGT

1.3 Key Constraints (Locked by ADRs)

Constraint Source Implication for BUDMGT
ScopedValue for TenantContext ADR-01 Every service method accesses TenantContext.current()
Spring Data JDBC only CLAUDE.md No lazy loading, explicit tenant_id in all queries
Cursor-based pagination ADR-12 All list endpoints use keyset pagination
Optimistic locking for offline sync ADR-05 version column on BudgetLine for frontend conflict detection
BigDecimal for financials CLAUDE.md #4 All monetary fields are BigDecimal, XOF = 0 decimals
Application Events for cross-module CLAUDE.md #10 No direct method calls FROM other modules TO budget internals
Shadcn/ui + Tailwind ADR-06 Frontend components use design system

2. Data Model

2.1 Entity-Relationship Diagram

erDiagram
    Budget ||--o{ BudgetLine : "has lines"
    Budget ||--o{ BudgetTransfer : "has transfers"
    BudgetCategory ||--o{ BudgetLine : "categorizes"
    BudgetLine ||--o{ BudgetTransfer : "source"
    BudgetLine ||--o{ BudgetTransfer : "destination"
    BudgetLine ||--o{ BudgetOverrideRequest : "override for"
    BudgetLine ||--o{ ThresholdAlert : "triggered on"
    BudgetSettings ||--|| Tenant : "one per tenant"

    Budget {
        uuid id PK
        uuid tenant_id FK
        date fiscal_year_start
        date fiscal_year_end
        date effective_start
        string label
        enum status "DRAFT|ACTIVE|CLOSED"
        uuid validated_by FK
        timestamp validated_at
        uuid closed_by FK
        timestamp closed_at
        uuid created_by FK
        timestamp created_at
        timestamp updated_at
        int version
    }

    BudgetCategory {
        uuid id PK
        uuid tenant_id FK
        string name
        string code
        string syscohada_account_prefix
        boolean is_system
        enum status "ACTIVE|INACTIVE"
        int sort_order
        timestamp created_at
        timestamp updated_at
        int version
    }

    BudgetLine {
        uuid id PK
        uuid tenant_id FK
        uuid budget_id FK
        uuid category_id FK
        string label
        bigdecimal allocation
        bigdecimal committed
        bigdecimal spent
        uuid org_unit_id FK "nullable"
        string syscohada_account
        enum enforcement_level "nullable"
        int warn_threshold_pct "nullable"
        int alert_threshold_pct "nullable"
        int action_threshold_pct "nullable"
        enum status "ACTIVE|INACTIVE"
        timestamp created_at
        timestamp updated_at
        int version
    }

    BudgetTransfer {
        uuid id PK
        uuid tenant_id FK
        uuid budget_id FK
        uuid source_line_id FK
        uuid destination_line_id FK
        bigdecimal amount
        text justification
        enum status "PENDING|APPROVED|REJECTED|CANCELLED"
        uuid requested_by FK
        timestamp requested_at
        uuid approved_by FK "nullable"
        timestamp decided_at "nullable"
        text rejection_reason "nullable"
        timestamp created_at
    }

    BudgetOverrideRequest {
        uuid id PK
        uuid tenant_id FK
        uuid budget_line_id FK
        enum operation_type "RESERVE|CONSUME"
        bigdecimal requested_amount
        bigdecimal available_at_request
        string source_entity_type
        uuid source_entity_id
        enum status "PENDING_OVERRIDE|APPROVED|REJECTED"
        uuid requested_by FK
        timestamp requested_at
        uuid decided_by FK "nullable"
        timestamp decided_at "nullable"
        timestamp created_at
    }

    BudgetSettings {
        uuid id PK
        uuid tenant_id FK "UNIQUE"
        enum default_enforcement_level
        int warn_threshold_pct
        int alert_threshold_pct
        int action_threshold_pct
        timestamp created_at
        timestamp updated_at
    }

    ThresholdAlert {
        uuid id PK
        uuid tenant_id FK
        uuid budget_line_id FK
        enum threshold_level "WARN|ALERT|ACTION"
        int threshold_pct
        int consumption_pct_at_trigger
        timestamp triggered_at
        boolean resolved
        timestamp resolved_at "nullable"
    }

2.2 Tenant Isolation per Entity

All BUDMGT entities are tenant-scoped and stored in the Tenant DB (not Platform DB). Budget data belongs to the tenant — this is critical for BYO-DB where the tenant's data lives on their own infrastructure.

Entity Storage Tenant Column Notes
Budget Tenant DB tenant_id One per fiscal year per tenant
BudgetCategory Tenant DB tenant_id Pre-seeded + custom per tenant
BudgetLine Tenant DB tenant_id Core entity, high-write
BudgetTransfer Tenant DB tenant_id Approval workflow
BudgetOverrideRequest Tenant DB tenant_id Cross-module approval
BudgetSettings Tenant DB tenant_id One row per tenant
ThresholdAlert Tenant DB tenant_id Notification tracking

2.3 Indexing Strategy

-- Primary lookup: budget lines for a tenant's active budget
CREATE INDEX idx_budget_line_tenant_budget ON budget_line(tenant_id, budget_id, status);

-- Dashboard aggregation by category
CREATE INDEX idx_budget_line_category ON budget_line(tenant_id, budget_id, category_id) WHERE status = 'ACTIVE';

-- Department-scoped lookups (COMMIT/EXPMGT need this)
CREATE INDEX idx_budget_line_org_unit ON budget_line(tenant_id, budget_id, org_unit_id) WHERE status = 'ACTIVE' AND org_unit_id IS NOT NULL;

-- Budget uniqueness (one per fiscal year per tenant)
CREATE UNIQUE INDEX uq_budget_tenant_fiscal ON budget(tenant_id, fiscal_year_start);

-- Active budget fast lookup (most common query)
CREATE UNIQUE INDEX uq_budget_tenant_active ON budget(tenant_id) WHERE status = 'ACTIVE';

-- Budget line label uniqueness within a budget
CREATE UNIQUE INDEX uq_budget_line_label ON budget_line(tenant_id, budget_id, label);

-- Category code uniqueness
CREATE UNIQUE INDEX uq_budget_category_code ON budget_category(tenant_id, code);

-- Transfer pending lookups (for approvers)
CREATE INDEX idx_budget_transfer_pending ON budget_transfer(tenant_id, budget_id, status) WHERE status = 'PENDING';

-- Override pending lookups
CREATE INDEX idx_budget_override_pending ON budget_override_request(tenant_id, status) WHERE status = 'PENDING_OVERRIDE';

-- Threshold alert tracking (check if already triggered)
CREATE INDEX idx_threshold_alert_line ON threshold_alert(tenant_id, budget_line_id, threshold_level, resolved);

3. API Design

3.1 Namespace

Prefix Scope
/api/v1/budget/ All BUDMGT user-facing REST endpoints

Internal module-to-module calls use the BudgetMutationApi Java interface (not REST).

3.2 Endpoints

Budgets

Method Path Description Auth Notes
POST /api/v1/budget/budgets Create a new budget (DRAFT) BUDGET_CREATE One per fiscal year
GET /api/v1/budget/budgets List budgets for tenant BUDGET_VIEW_ALL or BUDGET_VIEW_DEPARTMENT Cursor-based pagination
GET /api/v1/budget/budgets/current Get the current ACTIVE budget BUDGET_VIEW_ALL or BUDGET_VIEW_DEPARTMENT Shortcut endpoint
GET /api/v1/budget/budgets/{id} Get budget by ID BUDGET_VIEW_ALL or BUDGET_VIEW_DEPARTMENT
POST /api/v1/budget/budgets/{id}/validate Validate DRAFT → ACTIVE BUDGET_VALIDATE DG only
POST /api/v1/budget/budgets/{id}/close Close ACTIVE → CLOSED BUDGET_CLOSE Year-end

Budget Lines

Method Path Description Auth Notes
POST /api/v1/budget/budgets/{budgetId}/lines Create a budget line BUDGET_CREATE Budget must be DRAFT
GET /api/v1/budget/budgets/{budgetId}/lines List lines for a budget BUDGET_VIEW_ALL or BUDGET_VIEW_DEPARTMENT Filter by category, department. Cursor-based.
GET /api/v1/budget/budgets/{budgetId}/lines/{id} Get line details with balance BUDGET_VIEW_ALL or BUDGET_VIEW_DEPARTMENT Returns computed available
PUT /api/v1/budget/budgets/{budgetId}/lines/{id} Update line (DRAFT only) BUDGET_CREATE Allocation edit only in DRAFT
POST /api/v1/budget/budgets/{budgetId}/lines/{id}/deactivate Deactivate a line BUDGET_CREATE Blocked if committed > 0

Budget Categories

Method Path Description Auth Notes
POST /api/v1/budget/categories Create custom category BUDGET_CREATE
GET /api/v1/budget/categories List categories BUDGET_VIEW_ALL Active + inactive
PUT /api/v1/budget/categories/{id} Update category BUDGET_CREATE Can rename system categories
POST /api/v1/budget/categories/{id}/deactivate Deactivate category BUDGET_CREATE Blocked if active lines reference it

Budget Transfers

Method Path Description Auth Notes
POST /api/v1/budget/transfers Request a transfer BUDGET_TRANSFER_REQUEST
GET /api/v1/budget/transfers List transfers BUDGET_VIEW_ALL Filter by status, budget
GET /api/v1/budget/transfers/{id} Get transfer details BUDGET_VIEW_ALL
POST /api/v1/budget/transfers/{id}/approve Approve a transfer BUDGET_TRANSFER_APPROVE
POST /api/v1/budget/transfers/{id}/reject Reject a transfer BUDGET_TRANSFER_APPROVE Body: { rejectionReason }
POST /api/v1/budget/transfers/{id}/cancel Cancel a pending transfer BUDGET_TRANSFER_REQUEST Requester only

Budget Override Requests

Method Path Description Auth Notes
GET /api/v1/budget/overrides List pending overrides BUDGET_OVERRIDE_APPROVE For approver dashboard
GET /api/v1/budget/overrides/{id} Get override details BUDGET_OVERRIDE_APPROVE
POST /api/v1/budget/overrides/{id}/approve Approve override BUDGET_OVERRIDE_APPROVE Triggers reserve/consume
POST /api/v1/budget/overrides/{id}/reject Reject override BUDGET_OVERRIDE_APPROVE

Dashboard & Export

Method Path Description Auth Notes
GET /api/v1/budget/dashboard Budget consumption summary BUDGET_VIEW_ALL or BUDGET_VIEW_DEPARTMENT Aggregate by category + department
GET /api/v1/budget/dashboard/export Excel/CSV export BUDGET_EXPORT Content-Disposition: attachment

Settings

Method Path Description Auth Notes
GET /api/v1/budget/settings Get tenant budget settings BUDGET_CONFIGURE
PUT /api/v1/budget/settings Update settings BUDGET_CONFIGURE Thresholds, default enforcement

3.3 Request/Response Contracts

Create Budget Request

{
  "label": "Budget 2026",
  "fiscalYearStart": "2026-01-01",
  "fiscalYearEnd": "2026-12-31",
  "effectiveStart": "2026-01-01"
}

Budget Response

{
  "data": {
    "id": "019...",
    "label": "Budget 2026",
    "fiscalYearStart": "2026-01-01",
    "fiscalYearEnd": "2026-12-31",
    "effectiveStart": "2026-01-01",
    "status": "ACTIVE",
    "validatedBy": { "id": "019...", "fullName": "Kouamé Jean" },
    "validatedAt": "2026-02-15T10:30:00Z",
    "totals": {
      "allocation": 50000000,
      "committed": 12000000,
      "spent": 8000000,
      "available": 30000000,
      "consumptionPct": 40
    },
    "lineCount": 15,
    "createdAt": "2026-01-05T08:00:00Z",
    "updatedAt": "2026-02-15T10:30:00Z"
  }
}

Budget Line Response

{
  "data": {
    "id": "019...",
    "budgetId": "019...",
    "category": { "id": "019...", "code": "CHPERS", "name": "Charges de personnel" },
    "label": "Salaires cadres",
    "allocation": 20000000,
    "committed": 5000000,
    "spent": 3000000,
    "available": 12000000,
    "consumptionPct": 40,
    "orgUnit": { "id": "019...", "name": "Direction Technique" },
    "syscohadaAccount": "6611",
    "enforcementLevel": "REQUIRE_OVERRIDE",
    "thresholds": {
      "warn": 80,
      "alert": 95,
      "action": 100,
      "source": "LINE_OVERRIDE"
    },
    "status": "ACTIVE",
    "version": 3
  }
}

Dashboard Response

{
  "data": {
    "budgetId": "019...",
    "fiscalYear": "2026",
    "status": "ACTIVE",
    "totals": {
      "allocation": 50000000,
      "committed": 12000000,
      "spent": 8000000,
      "available": 30000000,
      "consumptionPct": 40
    },
    "byCategory": [
      {
        "categoryCode": "CHPERS",
        "categoryName": "Charges de personnel",
        "allocation": 20000000,
        "committed": 5000000,
        "spent": 3000000,
        "available": 12000000,
        "consumptionPct": 40,
        "lineCount": 3
      }
    ],
    "byDepartment": [
      {
        "orgUnitId": "019...",
        "orgUnitName": "Direction Technique",
        "allocation": 15000000,
        "committed": 4000000,
        "spent": 2000000,
        "available": 9000000,
        "consumptionPct": 40,
        "lineCount": 5
      }
    ],
    "globalLines": {
      "allocation": 10000000,
      "committed": 3000000,
      "spent": 1000000,
      "available": 6000000,
      "lineCount": 4
    }
  }
}

Budget Mutation API Response (Internal Java Interface)

public sealed interface BudgetMutationResult {
    record Reserved(UUID budgetLineId, BigDecimal newCommitted, BigDecimal newAvailable)
        implements BudgetMutationResult {}

    record Consumed(UUID budgetLineId, BigDecimal newSpent, BigDecimal newAvailable)
        implements BudgetMutationResult {}

    record Released(UUID budgetLineId, BigDecimal newCommitted, BigDecimal newAvailable)
        implements BudgetMutationResult {}

    record OverrideRequired(UUID overrideRequestId, UUID budgetLineId,
                            BigDecimal availableAmount, BigDecimal requestedAmount,
                            String enforcementLevel)
        implements BudgetMutationResult {}

    record HardBlocked(UUID budgetLineId, BigDecimal availableAmount,
                       BigDecimal requestedAmount, String budgetLineName)
        implements BudgetMutationResult {}
}

3.4 Error Responses

All errors follow the platform standard envelope:

{
  "errors": [
    {
      "code": "BUDGET_INSUFFICIENT",
      "message": "Budget insuffisant sur la ligne 'Fournitures de bureau'. Disponible : 150 000 XOF. Demandé : 200 000 XOF.",
      "field": null,
      "meta": {
        "budgetLineId": "019...",
        "available": 150000,
        "requested": 200000
      }
    }
  ]
}
HTTP Status Error Code Scenario
400 VALIDATION_ERROR Invalid input (decimal XOF, negative amounts, etc.)
403 FORBIDDEN Missing permission or department mismatch
404 NOT_FOUND Budget/line/transfer not found in tenant scope
409 OPTIMISTIC_LOCK_CONFLICT Frontend version mismatch (offline sync)
422 BUDGET_NOT_ACTIVE Mutation on DRAFT or CLOSED budget
422 BUDGET_INSUFFICIENT HARD_BLOCK enforcement triggered
422 BUDGET_OVERRIDE_REQUIRED REQUIRE_OVERRIDE enforcement triggered
422 CATEGORY_IN_USE Deactivating category with active lines
422 LINE_HAS_COMMITMENTS Deactivating line with committed > 0
422 TRANSFER_INSUFFICIENT_SOURCE Transfer amount > source available

4. Multi-Tenant Strategy

4.1 Tenant Resolution

BUDMGT follows the platform pattern (ADR-01):

HTTP Request → TenantResolutionFilter → ScopedValue<TenantContext>
                                    BudgetService.method()
                                    TenantContext.current().tenantId()
                                    BudgetRepository.findByTenantAndId(tenantId, id)

4.2 Data Isolation per Profile

Profile How It Works for BUDMGT
Profile A (Shared DB) All budget tables have tenant_id column. Every query includes WHERE tenant_id = :tenantId. Partial indexes include tenant_id. RLS as defense-in-depth.
Profile B (Schema-per-tenant) Budget tables exist in each tenant's schema. SET search_path TO tenant_{id} before each connection. tenant_id column still present but redundant (belt-and-suspenders).
Profile C (DB-per-tenant) Budget tables in tenant's dedicated database. DataSource routing per request. tenant_id column still present for consistency.
Profile D (BYO-DB) Budget tables in tenant-provided database. Flyway migrations run against external DB on activation. tenant_id column present. Connection pooled per BYO endpoint.

4.3 Shared vs. Tenant-Specific Configuration

Data Location Rationale
Budget categories (system + custom) Tenant DB Each tenant customizes independently
Budget settings (thresholds, enforcement) Tenant DB Per-tenant configuration
Budget data (budgets, lines, transfers) Tenant DB Core tenant business data
SYSCOHADA account chart (reference) Platform DB (via Control Plane) Shared across all OHADA tenants — read-only reference
Module activation status Platform DB (tenant_modules) Control Plane owns module gating

4.4 Module Gating

Before processing any BUDMGT API request:

@Component
public class BudgetModuleGateFilter implements HandlerInterceptor {

    private final TenantModuleService tenantModuleService;

    @Override
    public boolean preHandle(HttpServletRequest request, ...) {
        UUID tenantId = TenantContext.current().tenantId();
        if (!tenantModuleService.isModuleActive(tenantId, "BUDMGT")) {
            throw new ModuleNotActiveException("BUDMGT", tenantId);
        }
        return true;
    }
}

5. Offline & Sync Architecture

5.1 Offline Strategy

BUDMGT follows FR-PV-062: financial operations require connectivity. This means:

Data Offline Available? Mode
Budget dashboard (totals, by category, by department) ✅ Yes Read-only cache
Budget line list + balances ✅ Yes Read-only cache
Budget categories ✅ Yes Read-only cache
Budget transfers (submit/approve) ❌ No Online only
Budget mutations (reserve/consume/release) ❌ No Online only — these are system-initiated
Budget creation/editing ❌ No Online only
Settings changes ❌ No Online only
Excel/CSV export ❌ No Online only

5.2 Local Storage (IndexedDB)

// IndexedDB stores for offline budget data
interface BudgetOfflineStore {
  // Store: 'budget-dashboard'
  // Key: tenantId
  // Value: DashboardResponse + lastSyncedAt timestamp
  dashboard: {
    tenantId: string;
    data: DashboardResponse;
    lastSyncedAt: string; // ISO timestamp
  };

  // Store: 'budget-lines'
  // Key: [tenantId, budgetId]
  // Value: BudgetLineResponse[] + lastSyncedAt
  lines: {
    tenantId: string;
    budgetId: string;
    data: BudgetLineResponse[];
    lastSyncedAt: string;
  };

  // Store: 'budget-categories'
  // Key: tenantId
  // Value: BudgetCategoryResponse[]
  categories: {
    tenantId: string;
    data: BudgetCategoryResponse[];
    lastSyncedAt: string;
  };
}

5.3 Sync Protocol

Online → Fetch budget data → Store in IndexedDB → Display
Offline → Read from IndexedDB → Display with staleness banner
Back Online → Re-fetch → Update IndexedDB → Remove staleness banner
  • Sync trigger: On app startup, on navigation to budget module, on connectivity restored
  • Staleness indicator: "Données hors ligne — dernière mise à jour : [timestamp]"
  • Mutation attempt offline: Toast message "Opération impossible hors ligne. Connectez-vous pour effectuer des modifications budgétaires."
  • No offline write queue: Budget mutations are always online-only (no conflict resolution needed)

6. Sequence Diagrams

6.1 Budget Reserve Flow (COMMIT → BUDMGT)

sequenceDiagram
    participant C as Commitment Module
    participant B as BudgetMutationApi
    participant R as BudgetLineRepository
    participant DB as PostgreSQL
    participant E as EventPublisher

    C->>B: reserve(tenantId, budgetLineId, amount, commitmentId)
    B->>R: findByIdForUpdate(tenantId, budgetLineId)
    R->>DB: SELECT ... FROM budget_line WHERE tenant_id = ? AND id = ? FOR UPDATE
    DB-->>R: BudgetLine (locked row)
    R-->>B: BudgetLine

    alt Budget not ACTIVE
        B-->>C: Error: BUDGET_NOT_ACTIVE
    else Available >= Amount (happy path)
        B->>B: Check thresholds (warn/alert/action)
        B->>R: updateCommitted(lineId, newCommitted, newVersion)
        R->>DB: UPDATE budget_line SET committed = ?, version = version + 1 WHERE ...
        DB-->>R: 1 row updated
        B->>E: publish(BudgetThresholdReached) [if threshold crossed]
        B-->>C: Reserved(lineId, newCommitted, newAvailable)
    else Available < Amount AND enforcement = WARN
        B->>R: updateCommitted(lineId, newCommitted, newVersion)
        R->>DB: UPDATE ...
        B->>E: publish(BudgetThresholdReached(level=ACTION))
        B-->>C: Reserved(lineId, newCommitted, newAvailable)
    else Available < Amount AND enforcement = REQUIRE_OVERRIDE
        B->>R: createOverrideRequest(...)
        R->>DB: INSERT INTO budget_override_request ...
        B->>E: publish(BudgetOverrideRequested)
        B-->>C: OverrideRequired(overrideId, lineId, available, requested)
    else Available < Amount AND enforcement = HARD_BLOCK
        B-->>C: HardBlocked(lineId, available, requested, lineName)
    end

6.2 Budget Transfer Workflow

sequenceDiagram
    participant U as Comptable (UI)
    participant API as Budget REST API
    participant S as BudgetTransferService
    participant R as BudgetTransferRepository
    participant LR as BudgetLineRepository
    participant DB as PostgreSQL
    participant E as EventPublisher

    U->>API: POST /api/v1/budget/transfers {sourceLineId, destLineId, amount, justification}
    API->>S: createTransfer(request)
    S->>LR: findById(sourceLineId)
    LR->>DB: SELECT ... WHERE tenant_id = ? AND id = ?
    DB-->>LR: sourceLine

    alt Amount > sourceLine.available
        S-->>API: Error 422: TRANSFER_INSUFFICIENT_SOURCE
        API-->>U: "Montant insuffisant..."
    else Valid
        S->>R: save(transfer with status=PENDING)
        R->>DB: INSERT INTO budget_transfer ...
        S->>E: publish(BudgetTransferRequested)
        S-->>API: TransferResponse(status=PENDING)
        API-->>U: 201 Created

        Note over U: Later — DG reviews transfer
        U->>API: POST /api/v1/budget/transfers/{id}/approve
        API->>S: approveTransfer(id)
        S->>LR: findByIdForUpdate(sourceLineId)
        S->>LR: findByIdForUpdate(destLineId)
        Note over S: Both lines locked in consistent order (by id)
        S->>LR: updateAllocation(sourceLineId, allocation - amount)
        S->>LR: updateAllocation(destLineId, allocation + amount)
        S->>R: updateStatus(transferId, APPROVED, approvedBy)
        S->>E: publish(BudgetTransferDecided(APPROVED))
        S-->>API: TransferResponse(status=APPROVED)
        API-->>U: 200 OK
    end

6.3 Override Approval Flow

sequenceDiagram
    participant CM as Commitment Module
    participant B as BudgetMutationApi
    participant DG as DG (UI)
    participant API as Budget REST API
    participant OS as OverrideService
    participant E as EventPublisher

    CM->>B: reserve(tenantId, lineId, amount, commitmentId)
    B-->>CM: OverrideRequired(overrideId, lineId, available, requested)
    CM->>CM: Set commitment status = PENDING_BUDGET_OVERRIDE

    Note over DG: Notification: "Dépassement budgétaire à approuver"
    DG->>API: GET /api/v1/budget/overrides (pending list)
    DG->>API: POST /api/v1/budget/overrides/{id}/approve

    API->>OS: approveOverride(overrideId)
    OS->>B: executeReserve(lineId, amount) [with FOR UPDATE lock]
    B->>B: Update committed, increment version
    OS->>E: publish(BudgetOverrideApproved(overrideId, commitmentId))

    Note over CM: Listens to BudgetOverrideApproved
    CM->>CM: Set commitment status = APPROVED

6.4 Year-End Closure

sequenceDiagram
    participant U as DG (UI)
    participant API as Budget REST API
    participant S as BudgetService
    participant R as BudgetLineRepository
    participant DB as PostgreSQL
    participant E as EventPublisher

    U->>API: POST /api/v1/budget/budgets/{id}/close
    API->>S: closeBudget(budgetId)

    S->>R: countLinesWithOpenCommitments(budgetId)
    R->>DB: SELECT COUNT(*) FROM budget_line WHERE committed > 0 AND budget_id = ? AND tenant_id = ?
    DB-->>R: count

    alt count > 0
        S-->>API: Warning: {openCommitmentCount, totalCommittedAmount}
        API-->>U: Confirmation required: "[N] lignes ont des engagements..."

        U->>API: POST /api/v1/budget/budgets/{id}/close?confirmed=true
        API->>S: closeBudget(budgetId, confirmed=true)
    end

    S->>DB: UPDATE budget SET status = 'CLOSED', closed_by = ?, closed_at = NOW() WHERE ...
    S->>DB: UPDATE threshold_alert SET resolved = true WHERE budget_line_id IN (...)
    S->>E: publish(BudgetClosed(tenantId, budgetId, fiscalYear))
    S-->>API: BudgetResponse(status=CLOSED)
    API-->>U: 200 OK

7. Security Architecture

7.1 Permissions

BUDMGT defines the following permissions, registered in the platform RBAC system:

Permission Code Description Granted To
BUDGET_CREATE Create/edit budgets and lines (DRAFT only) DG, Comptable
BUDGET_VALIDATE Validate DRAFT → ACTIVE DG
BUDGET_CLOSE Close ACTIVE → CLOSED DG, Comptable (delegable)
BUDGET_VIEW_ALL View all budget data (company-wide) DG, Comptable, Finance Director
BUDGET_VIEW_DEPARTMENT View budget lines scoped to own department Manager
BUDGET_TRANSFER_REQUEST Submit transfer requests Comptable, Finance Director
BUDGET_TRANSFER_APPROVE Approve/reject transfers DG, Finance Director
BUDGET_OVERRIDE_APPROVE Approve/reject over-budget overrides DG, Finance Director
BUDGET_CONFIGURE Manage settings and categories DG, Comptable
BUDGET_EXPORT Export budget data (Excel/CSV) DG, Comptable, Finance Director

7.2 Department Scoping for Managers

When a user has BUDGET_VIEW_DEPARTMENT but NOT BUDGET_VIEW_ALL:

public List<BudgetLine> getLinesForUser(UUID tenantId, UUID budgetId, UUID userId) {
    Set<UUID> managedOrgUnits = organizationApi.getManagedOrgUnits(tenantId, userId);
    if (managedOrgUnits.isEmpty()) {
        return List.of(); // No department → no budget visibility
    }
    return budgetLineRepository.findByBudgetAndOrgUnits(tenantId, budgetId, managedOrgUnits);
}

7.3 Audit Trail

Every budget mutation produces an AuditEntry (via the platform audit module):

Action Entity Type Before/After Captured
BUDGET_CREATED Budget — / full budget
BUDGET_VALIDATED Budget status=DRAFT / status=ACTIVE
BUDGET_CLOSED Budget status=ACTIVE / status=CLOSED
BUDGET_LINE_CREATED BudgetLine — / full line
BUDGET_LINE_UPDATED BudgetLine changed fields only
BUDGET_LINE_DEACTIVATED BudgetLine status=ACTIVE / status=INACTIVE
BUDGET_RESERVED BudgetLine old committed / new committed
BUDGET_CONSUMED BudgetLine old spent / new spent
BUDGET_RELEASED BudgetLine old committed / new committed
TRANSFER_REQUESTED BudgetTransfer — / full transfer
TRANSFER_APPROVED BudgetTransfer status=PENDING / status=APPROVED + line changes
TRANSFER_REJECTED BudgetTransfer status=PENDING / status=REJECTED
OVERRIDE_APPROVED BudgetOverrideRequest status=PENDING / status=APPROVED
OVERRIDE_REJECTED BudgetOverrideRequest status=PENDING / status=REJECTED
SETTINGS_UPDATED BudgetSettings old thresholds / new thresholds
CATEGORY_CREATED BudgetCategory — / full category
CATEGORY_UPDATED BudgetCategory changed fields
CATEGORY_DEACTIVATED BudgetCategory status=ACTIVE / status=INACTIVE

7.4 Input Validation

public record CreateBudgetLineRequest(
    @NotBlank @Size(max = 200) String label,
    @NotNull UUID categoryId,
    @NotNull @DecimalMin("0") @Digits(integer = 15, fraction = 0) BigDecimal allocation,
    @Nullable UUID orgUnitId,
    @Nullable @Size(max = 20) String syscohadaAccount,
    @Nullable EnforcementLevel enforcementLevel,
    @Nullable @Min(1) @Max(200) Integer warnThresholdPct,
    @Nullable @Min(1) @Max(200) Integer alertThresholdPct,
    @Nullable @Min(1) @Max(200) Integer actionThresholdPct
) {
    // Custom validation: threshold ordering
    // warn < alert < action (if all provided)
}

8. Performance Considerations

8.1 Critical Path: Balance Check + Reserve

The synchronous BudgetMutationApi.reserve() call is on the critical path of commitment approval. Target: < 100ms (p95).

Performance budget: - ScopedValue lookup: ~0ns - SELECT FOR UPDATE: ~1-5ms (indexed, single row) - Business logic (threshold check): ~0ms (in-memory) - UPDATE committed: ~1-5ms - Audit entry INSERT: ~1-5ms - Event publish (async): ~0ms (non-blocking) - Total: ~5-15ms — well within 100ms target

8.2 Dashboard Aggregation

On-the-fly SUM over up to 500 rows:

-- Per-category summary: ~2ms with covering index
SELECT c.code, c.name,
       SUM(bl.allocation) as allocation,
       SUM(bl.committed) as committed,
       SUM(bl.spent) as spent
FROM budget_line bl
JOIN budget_category c ON c.id = bl.category_id AND c.tenant_id = bl.tenant_id
WHERE bl.tenant_id = :tenantId
  AND bl.budget_id = :budgetId
  AND bl.status = 'ACTIVE'
GROUP BY c.code, c.name;

8.3 Caching

What Cache TTL Invalidation
BudgetSettings (per tenant) Caffeine (L1) 5 min On settings update
Module gating (BUDMGT active?) Redis (per ADR-04) Inherited from platform On module status change
Budget line data No cache Always fresh (financial data)
Category list Caffeine (L1) 10 min On category CRUD

No caching for budget line balances — they must always be fresh for the balance check API.

8.4 Connection Pooling

BUDMGT uses the platform's dual DataSource (ADR-09). Budget data flows through the Tenant DataSource. No additional pooling configuration needed.


9. Migration Strategy

9.1 Flyway Migrations

All migrations live in backend/src/main/resources/db/migration/budget/:

V001__create_budget_settings.sql
V002__create_budget_category.sql
V003__create_budget.sql
V004__create_budget_line.sql
V005__create_budget_transfer.sql
V006__create_budget_override_request.sql
V007__create_threshold_alert.sql
V008__create_indexes.sql

9.2 Profile-Specific Behavior

Profile Migration Execution
Profile A (Shared DB) Migrations run once against the shared DB. All tables include tenant_id.
Profile B (Schema-per-tenant) Migrations run per schema during tenant provisioning.
Profile C (DB-per-tenant) Migrations run per tenant DB during provisioning.
Profile D (BYO-DB) Migrations run against the tenant-provided DB during onboarding. Connection validated first.

9.3 Reversibility

Every migration includes a corresponding undo script in db/migration/budget/undo/:

U001__drop_budget_settings.sql
U002__drop_budget_category.sql
...

9.4 Category Seeding

Category seeding is NOT a Flyway migration. It's application-level logic triggered by ModuleActivated:

@Component
public class BudgetModuleActivationListener {

    @EventListener
    public void onModuleActivated(ModuleActivated event) {
        if (!"BUDMGT".equals(event.moduleCode())) return;
        budgetCategorySeedService.seedIfNotExists(event.tenantId());
        budgetSettingsSeedService.seedIfNotExists(event.tenantId());
    }
}

10. Integration Points

10.1 Internal Java API (Synchronous)

BUDMGT exposes a public interface consumed by COMMIT and EXPMGT within the same JVM:

// com.altarys.papillon.modules.budget.api.BudgetMutationApi
public interface BudgetMutationApi {

    /**
     * Reserve budget for a commitment.
     * Called by COMMIT module on commitment approval.
     */
    BudgetMutationResult reserve(UUID tenantId, UUID budgetLineId,
                                  BigDecimal amount, UUID commitmentId);

    /**
     * Consume budget for an expense.
     * If linkedCommitmentId is provided, atomically releases the commitment
     * and consumes the expense amount.
     */
    BudgetMutationResult consume(UUID tenantId, UUID budgetLineId,
                                  BigDecimal amount, UUID expenseId,
                                  @Nullable UUID linkedCommitmentId);

    /**
     * Release previously reserved budget.
     * Called by COMMIT module on commitment cancellation.
     */
    BudgetMutationResult release(UUID tenantId, UUID budgetLineId,
                                  BigDecimal amount, UUID commitmentId);

    /**
     * Check budget line balance without mutating.
     * Returns current allocation, committed, spent, available, enforcement level.
     */
    BudgetBalanceResponse getBalance(UUID tenantId, UUID budgetLineId);

    /**
     * Find active budget lines for a given department (org unit).
     * Includes lines scoped to the org unit, its ancestors, and company-global lines.
     */
    List<BudgetLineInfo> findLinesForDepartment(UUID tenantId, UUID orgUnitId);
}

10.2 Events Published by BUDMGT

Event Class Payload Consumers
BudgetThresholdReached tenantId, budgetLineId, budgetLineName, thresholdLevel, consumptionPct, availableAmount notification
BudgetExhausted tenantId, budgetLineId, budgetLineName, enforcementLevel notification, commitment, expense
BudgetOverrideRequested tenantId, budgetLineId, overrideRequestId, requestedAmount, availableAmount, sourceEntityType, sourceEntityId notification
BudgetOverrideApproved tenantId, overrideRequestId, budgetLineId, amount, sourceEntityType, sourceEntityId commitment (to unblock PENDING_BUDGET_OVERRIDE), expense
BudgetOverrideRejected tenantId, overrideRequestId, budgetLineId, sourceEntityType, sourceEntityId commitment (to cancel), expense
BudgetTransferRequested tenantId, transferId, sourceLineName, destLineName, amount, requestedByName notification
BudgetTransferDecided tenantId, transferId, decision, decidedByName notification
BudgetValidated tenantId, budgetId, fiscalYear, validatedByName notification
BudgetClosed tenantId, budgetId, fiscalYear, closedAt commitment (flag open commitments)

10.3 Events Consumed by BUDMGT

Event Class Publisher BUDMGT Action
ModuleActivated(BUDMGT) tenant Seed categories + settings
ModuleActivated(EXPMGT\|COMMIT) tenant Auto-activate BUDMGT if not active
ModuleDeactivated(EXPMGT\|COMMIT) tenant Auto-deactivate BUDMGT if neither active
OrgUnitDeactivated organization Notify comptable about affected budget lines

10.4 Dependencies Summary

graph LR
    subgraph "Control Plane"
        T[tenant]
        I[identity]
        O[organization]
        N[notification]
        A[audit]
    end

    subgraph "Application Plane"
        B[budget]
        CM[commitment]
        EX[expense]
    end

    T -->|ModuleActivated| B
    I -->|Auth, RBAC| B
    O -->|OrgUnit data| B
    B -->|BudgetThresholdReached| N
    B -->|AuditEntry| A
    CM -->|BudgetMutationApi.reserve| B
    CM -->|BudgetMutationApi.release| B
    EX -->|BudgetMutationApi.consume| B
    B -->|BudgetOverrideApproved| CM
    B -->|BudgetClosed| CM

11. Vertical Slice Decomposition

Dependency Graph with Concurrency Tiers

Tier 0 (serial foundation):
  Slice 1: Budget Settings + Category Seeding

Tier 1 (parallel after Tier 0 merges):
  Slice 2: Budget CRUD + Lifecycle
  Slice 3: Budget Category CRUD

Tier 2 (parallel after Tier 1 merges — Slice 2 specifically):
  Slice 4: Budget Line CRUD

Tier 3 (parallel after Slice 4 merges):
  Slice 5: Budget Mutation API (reserve/consume/release)
  Slice 6: Budget Dashboard + Export
  Slice 7: Budget Transfer Workflow

Tier 4 (parallel after Slice 5 merges):
  Slice 8: Threshold Alerts + Enforcement
  Slice 9: Override Request Workflow

Tier 5 (parallel after all Tier 4 merges):
  Slice 10: Offline Budget View
  Slice 11: Year-End Closure

Slice 1: Budget Settings + Module Activation Seeding

User Scenario: When a tenant activates BUDMGT (via EXPMGT or COMMIT activation), the system auto-seeds default settings and budget categories.

Scope: - Flyway migrations: budget_settings table, budget_category table - ModuleActivated listener - BudgetCategorySeedService (idempotent seeding of 9 SYSCOHADA categories) - BudgetSettingsSeedService (default thresholds: 80/95/100, enforcement: WARN) - GET /api/v1/budget/settings endpoint - PUT /api/v1/budget/settings endpoint - Unit + integration tests (tenant isolation, idempotent seeding)

Files to create: - backend/src/main/java/com/altarys/modules/budget/ (module package) - BudgetSettings.java (entity) - BudgetCategory.java (entity) - BudgetSettingsRepository.java - BudgetCategoryRepository.java - BudgetCategorySeedService.java - BudgetSettingsSeedService.java - BudgetModuleActivationListener.java - BudgetSettingsController.java - dto/BudgetSettingsResponse.java - dto/UpdateBudgetSettingsRequest.java - backend/src/main/resources/db/migration/budget/V001__create_budget_settings.sql - backend/src/main/resources/db/migration/budget/V002__create_budget_category.sql - backend/src/test/java/com/altarys/modules/budget/ - BudgetCategorySeedServiceTest.java - BudgetSettingsControllerTest.java - BudgetModuleActivationListenerTest.java

Estimated Complexity: M Dependencies: Control Plane (ModuleActivated)


Slice 2: Budget CRUD + Lifecycle

User Scenario: A comptable creates a budget for the fiscal year, the DG validates it.

Scope: - Flyway migration: budget table - Budget entity + repository - POST /api/v1/budget/budgets (create DRAFT) - GET /api/v1/budget/budgets (list with cursor pagination) - GET /api/v1/budget/budgets/current (get ACTIVE budget) - GET /api/v1/budget/budgets/{id} - POST /api/v1/budget/budgets/{id}/validate (DRAFT → ACTIVE, DG only) - Mid-year budget creation support (effective_start ≠ fiscal_year_start) - Coexistence validation (max one ACTIVE + one DRAFT) - BudgetValidated publication - Permission checks: BUDGET_CREATE, BUDGET_VALIDATE - Frontend: Budget list page, create budget wizard, validate action

Files to create: - backend/.../budget/Budget.java (entity) - backend/.../budget/BudgetRepository.java - backend/.../budget/BudgetService.java - backend/.../budget/BudgetController.java - backend/.../budget/dto/CreateBudgetRequest.java - backend/.../budget/dto/BudgetResponse.java - backend/.../budget/event/BudgetValidated.java - backend/src/main/resources/db/migration/budget/V003__create_budget.sql - frontend/src/modules/budget/pages/BudgetListPage.tsx - frontend/src/modules/budget/components/CreateBudgetWizard.tsx - Tests: BudgetServiceTest.java, BudgetControllerTest.java

Estimated Complexity: M Dependencies: Slice 1 (settings exist)


Slice 3: Budget Category CRUD

User Scenario: A comptable views pre-seeded categories, creates a custom category, renames a system category, deactivates a category.

Scope: - POST /api/v1/budget/categories (create custom) - GET /api/v1/budget/categories (list active + inactive) - PUT /api/v1/budget/categories/{id} (update/rename) - POST /api/v1/budget/categories/{id}/deactivate - Deactivation guard: blocked if active budget lines reference category - Permission check: BUDGET_CREATE, BUDGET_VIEW_ALL - Frontend: Category management page

Files to create: - backend/.../budget/BudgetCategoryService.java - backend/.../budget/BudgetCategoryController.java - backend/.../budget/dto/CreateCategoryRequest.java - backend/.../budget/dto/UpdateCategoryRequest.java - backend/.../budget/dto/CategoryResponse.java - frontend/src/modules/budget/pages/CategoryManagementPage.tsx - Tests: BudgetCategoryServiceTest.java, BudgetCategoryControllerTest.java

Estimated Complexity: S Dependencies: Slice 1 (categories table exists)


Slice 4: Budget Line CRUD

User Scenario: A comptable adds budget lines to a DRAFT budget — with label, category, allocation, optional department scope, optional SYSCOHADA account mapping.

Scope: - Flyway migration: budget_line table + indexes - BudgetLine entity + repository - POST /api/v1/budget/budgets/{budgetId}/lines (create) - GET /api/v1/budget/budgets/{budgetId}/lines (list with cursor pagination, filters) - GET /api/v1/budget/budgets/{budgetId}/lines/{id} (detail with computed available) - PUT /api/v1/budget/budgets/{budgetId}/lines/{id} (update, DRAFT only) - POST /api/v1/budget/budgets/{budgetId}/lines/{id}/deactivate - Deactivation guard: blocked if committed > 0 - Validation: XOF whole numbers, allocation >= 0, unique label per budget - SYSCOHADA account searchable dropdown (Classe 6x + 2x) - Department scoping via org_unit_id - Permission checks: BUDGET_CREATE, BUDGET_VIEW_ALL, BUDGET_VIEW_DEPARTMENT - Frontend: Budget line list, create/edit line form, SYSCOHADA account picker

Files to create: - backend/.../budget/BudgetLine.java - backend/.../budget/BudgetLineRepository.java - backend/.../budget/BudgetLineService.java - backend/.../budget/BudgetLineController.java - backend/.../budget/dto/CreateBudgetLineRequest.java - backend/.../budget/dto/UpdateBudgetLineRequest.java - backend/.../budget/dto/BudgetLineResponse.java - backend/src/main/resources/db/migration/budget/V004__create_budget_line.sql - frontend/src/modules/budget/pages/BudgetLinePage.tsx - frontend/src/modules/budget/components/BudgetLineForm.tsx - frontend/src/modules/budget/components/SyscohadaAccountPicker.tsx - Tests: BudgetLineServiceTest.java, BudgetLineControllerTest.java, tenant isolation tests

Estimated Complexity: L Dependencies: Slice 2 (budget exists), Slice 3 (categories exist)


Slice 5: Budget Mutation API (Reserve / Consume / Release)

User Scenario: When COMMIT approves a commitment, it calls BudgetMutationApi.reserve(). When EXPMGT approves an expense linked to a commitment, it calls consume() which atomically releases + consumes. When COMMIT cancels a commitment, it calls release().

Scope: - BudgetMutationApi interface (public module API) - BudgetMutationService (implementation) - BudgetMutationResult sealed interface (Reserved | Consumed | Released | OverrideRequired | HardBlocked) - Pessimistic locking: SELECT ... FOR UPDATE on budget_line - Atomic commitment-to-expense conversion (release + consume in one transaction) - Balance check: getBalance(tenantId, budgetLineId) - Department lookup: findLinesForDepartment(tenantId, orgUnitId) - Basic enforcement: check available vs. requested (threshold alerts in Slice 8) - Unit tests with concurrent access simulation - Integration tests verifying atomicity

Files to create: - backend/.../budget/api/BudgetMutationApi.java (public interface) - backend/.../budget/api/BudgetMutationResult.java - backend/.../budget/api/BudgetBalanceResponse.java - backend/.../budget/api/BudgetLineInfo.java - backend/.../budget/internal/BudgetMutationService.java - Tests: BudgetMutationServiceTest.java (unit), BudgetMutationConcurrencyTest.java (integration)

Estimated Complexity: L Dependencies: Slice 4 (budget lines exist)


Slice 6: Budget Dashboard + Export

User Scenario: A DG opens the budget dashboard and sees total allocation/committed/spent/available, broken down by category and department. They export the data as Excel/CSV.

Scope: - GET /api/v1/budget/dashboard (on-the-fly SQL aggregation) - GET /api/v1/budget/dashboard/export (Excel/CSV download) - Aggregation queries: by category, by department, global lines - Department-scoped dashboard for managers (BUDGET_VIEW_DEPARTMENT) - Export using Apache POI (Excel) or OpenCSV - Frontend: Dashboard page with summary cards, category breakdown chart, department breakdown table - Responsive: works on mobile (Papillon) and desktop (Enterprise)

Files to create: - backend/.../budget/BudgetDashboardService.java - backend/.../budget/BudgetDashboardController.java - backend/.../budget/BudgetExportService.java - backend/.../budget/dto/DashboardResponse.java - frontend/src/modules/budget/pages/BudgetDashboardPage.tsx - frontend/src/modules/budget/components/BudgetSummaryCards.tsx - frontend/src/modules/budget/components/CategoryBreakdownChart.tsx - frontend/src/modules/budget/components/DepartmentBreakdownTable.tsx - Tests: BudgetDashboardServiceTest.java

Estimated Complexity: M Dependencies: Slice 4 (budget lines exist)


Slice 7: Budget Transfer Workflow

User Scenario: A comptable requests a budget transfer from one line to another. The DG approves or rejects. On approval, allocations update atomically.

Scope: - Flyway migration: budget_transfer table - BudgetTransfer entity + repository - POST /api/v1/budget/transfers (submit request) - GET /api/v1/budget/transfers (list with filters) - GET /api/v1/budget/transfers/{id} (detail) - POST /api/v1/budget/transfers/{id}/approve (with pessimistic locking on both lines) - POST /api/v1/budget/transfers/{id}/reject - POST /api/v1/budget/transfers/{id}/cancel - Lock ordering: always lock lines by UUID ascending to prevent deadlocks - Source available validation at submission time - DG self-approval (auto-approve with audit trail) - Events: BudgetTransferRequested, BudgetTransferDecided - Frontend: Transfer request form, pending transfers list, approve/reject actions

Files to create: - backend/.../budget/BudgetTransfer.java - backend/.../budget/BudgetTransferRepository.java - backend/.../budget/BudgetTransferService.java - backend/.../budget/BudgetTransferController.java - backend/.../budget/dto/CreateTransferRequest.java - backend/.../budget/dto/TransferResponse.java - backend/.../budget/dto/RejectTransferRequest.java - backend/.../budget/event/BudgetTransferRequested.java - backend/.../budget/event/BudgetTransferDecided.java - backend/src/main/resources/db/migration/budget/V005__create_budget_transfer.sql - frontend/src/modules/budget/pages/TransferListPage.tsx - frontend/src/modules/budget/components/TransferRequestForm.tsx - frontend/src/modules/budget/components/TransferApprovalCard.tsx - Tests: BudgetTransferServiceTest.java, deadlock prevention test

Estimated Complexity: L Dependencies: Slice 4 (budget lines exist)


Slice 8: Threshold Alerts + Enforcement

User Scenario: When a budget line's consumption crosses 80% (warn), 95% (alert), or 100% (action), the system sends notifications and applies the configured enforcement level.

Scope: - Flyway migration: threshold_alert table - ThresholdAlert entity + repository - Threshold calculation: consumption_pct = (committed + spent) / allocation * 100 - Three-level enforcement logic within BudgetMutationService - Notification deduplication: track which thresholds have triggered per line per year - Threshold reset when available increases back below (FR-BUD-039) - BudgetThresholdReached, BudgetExhausted publication - Tenant-level default thresholds with per-line overrides - Edge case: allocation = 0 → immediate action threshold - Frontend: Visual indicators on budget lines (green/yellow/orange/red)

Files to create: - backend/.../budget/ThresholdAlert.java - backend/.../budget/ThresholdAlertRepository.java - backend/.../budget/ThresholdService.java - backend/.../budget/event/BudgetThresholdReached.java - backend/.../budget/event/BudgetExhausted.java - backend/src/main/resources/db/migration/budget/V007__create_threshold_alert.sql - frontend/src/modules/budget/components/BudgetLineStatusIndicator.tsx - Tests: ThresholdServiceTest.java (all enforcement paths)

Estimated Complexity: M Dependencies: Slice 5 (mutation API exists)


Slice 9: Override Request Workflow

User Scenario: A commitment exceeds available budget on a REQUIRE_OVERRIDE line. The system creates a pending override request. DG approves → budget reserves. DG rejects → commitment is cancelled.

Scope: - Flyway migration: budget_override_request table - BudgetOverrideRequest entity + repository - Override creation within BudgetMutationService (when enforcement = REQUIRE_OVERRIDE) - GET /api/v1/budget/overrides (pending list for approvers) - POST /api/v1/budget/overrides/{id}/approve - POST /api/v1/budget/overrides/{id}/reject - On approval: execute the deferred reserve/consume with FOR UPDATE lock - Events: BudgetOverrideRequested, BudgetOverrideApproved, BudgetOverrideRejected - Cross-module: COMMIT listens to BudgetOverrideApproved / BudgetOverrideRejected - Frontend: Override pending list (in approver dashboard), approve/reject actions

Files to create: - backend/.../budget/BudgetOverrideRequest.java - backend/.../budget/BudgetOverrideRequestRepository.java - backend/.../budget/BudgetOverrideService.java - backend/.../budget/BudgetOverrideController.java - backend/.../budget/dto/OverrideResponse.java - backend/.../budget/event/BudgetOverrideRequested.java - backend/.../budget/event/BudgetOverrideApproved.java - backend/.../budget/event/BudgetOverrideRejected.java - backend/src/main/resources/db/migration/budget/V006__create_budget_override_request.sql - frontend/src/modules/budget/pages/OverrideListPage.tsx - frontend/src/modules/budget/components/OverrideApprovalCard.tsx - Tests: BudgetOverrideServiceTest.java

Estimated Complexity: M Dependencies: Slice 5 (mutation API), Slice 8 (enforcement logic)


Slice 10: Offline Budget View

User Scenario: A comptable loads the budget dashboard while online. Later, offline, they can view the cached balances with a staleness indicator. Mutations are blocked offline.

Scope: - IndexedDB stores: budget-dashboard, budget-lines, budget-categories - Service Worker caching strategy for budget GET endpoints - Staleness banner: "Données hors ligne — dernière mise à jour : [timestamp]" - Mutation blocking: toast on any POST/PUT attempt offline - Sync on connectivity restoration: re-fetch and update IndexedDB - Frontend: Offline-aware hooks, staleness UI components

Files to create: - frontend/src/modules/budget/hooks/useBudgetOffline.ts - frontend/src/modules/budget/stores/budgetOfflineStore.ts - frontend/src/modules/budget/components/OfflineStalenessBar.tsx - frontend/src/modules/budget/sw/budget-cache-strategy.ts - Tests: offline simulation tests

Estimated Complexity: M Dependencies: Slice 6 (dashboard exists), Slice 4 (line list exists)


Slice 11: Year-End Closure

User Scenario: At fiscal year end, the DG closes the budget. The system warns about open commitments, freezes balances, and resolves all threshold alerts.

Scope: - POST /api/v1/budget/budgets/{id}/close (with confirmation flow) - Open commitment warning (count + total amount) - Confirmation required if open commitments exist - On closure: status → CLOSED, resolve all threshold alerts - BudgetClosed publication (COMMIT listens to flag open commitments) - Historical read-only access to closed budgets - Frontend: Close budget action with confirmation dialog

Files to create: - backend/.../budget/BudgetClosureService.java - backend/.../budget/event/BudgetClosed.java - frontend/src/modules/budget/components/CloseBudgetDialog.tsx - Tests: BudgetClosureServiceTest.java

Estimated Complexity: S Dependencies: Slice 5 (mutations must exist to verify closure blocks them), Slice 8 (threshold alerts to resolve)


12. Technical Risks & Mitigations

# Risk Likelihood Impact Mitigation
R1 Double-spending: Two concurrent commitments reserve budget simultaneously, exceeding available balance Medium High Pessimistic locking (SELECT FOR UPDATE) guarantees serialized access. Integration tests with concurrent threads verify correctness.
R2 Deadlock on transfer approval: Two transfers involving the same lines, locking in different order Low Medium Always lock budget lines in ascending UUID order. Single consistent lock ordering eliminates deadlock possibility.
R3 Stale balance on department-scoped lookups: COMMIT queries budget lines for a department, employee was reassigned between query and reserve Low Low Reserve API takes budgetLineId (not department). COMMIT is responsible for selecting the correct line. Re-validation happens inside the reserve transaction.
R4 Module activation race: EXPMGT and COMMIT activate simultaneously, both triggering BUDMGT seeding Low Low seedIfNotExists() is idempotent. Uses INSERT ... ON CONFLICT DO NOTHING.
R5 BYO-DB migration failure: Flyway migration fails on a tenant-provided database (incompatible PostgreSQL version, missing extensions) Medium Medium Pre-flight check validates PG version ≥ 14, required extensions. Migration runs in a transaction — full rollback on failure. Tenant notified of incompatibility.
R6 Override approval delay: DG doesn't approve override, commitment stays PENDING_BUDGET_OVERRIDE indefinitely Medium Medium Notification escalation (reminder after 24h, 72h). Dashboard shows pending overrides prominently.
R7 Year-end closure with massive open commitments: Large enterprise with 100+ open commitments at closure Low Low Warning dialog lists count + total amount. DG must acknowledge. No auto-cancellation — COMMIT module flags open commitments for manual resolution.
R8 Performance at scale: Enterprise tenant with 500+ budget lines, 10,000+ mutations/day Low (MVP) Medium SELECT FOR UPDATE on single row: <5ms. Dashboard SUM on 500 rows: <2ms. Scale target is comfortable for PostgreSQL. Monitor via Micrometer with tenant_id tag.

13. Package Structure

backend/src/main/java/com/altarys/modules/budget/
├── api/                              # Public module API (consumed by COMMIT, EXPMGT)
│   ├── BudgetMutationApi.java
│   ├── BudgetMutationResult.java
│   ├── BudgetBalanceResponse.java
│   └── BudgetLineInfo.java
├── internal/                         # Internal implementation (not accessible to other modules)
│   ├── BudgetMutationService.java
│   ├── ThresholdService.java
│   ├── BudgetCategorySeedService.java
│   └── BudgetSettingsSeedService.java
├── Budget.java                       # Entities
├── BudgetLine.java
├── BudgetCategory.java
├── BudgetTransfer.java
├── BudgetOverrideRequest.java
├── BudgetSettings.java
├── ThresholdAlert.java
├── EnforcementLevel.java             # Enum
├── BudgetStatus.java                 # Enum
├── BudgetRepository.java             # Repositories
├── BudgetLineRepository.java
├── BudgetCategoryRepository.java
├── BudgetTransferRepository.java
├── BudgetOverrideRequestRepository.java
├── BudgetSettingsRepository.java
├── ThresholdAlertRepository.java
├── BudgetService.java                # Services
├── BudgetLineService.java
├── BudgetTransferService.java
├── BudgetOverrideService.java
├── BudgetClosureService.java
├── BudgetDashboardService.java
├── BudgetExportService.java
├── BudgetController.java             # REST Controllers
├── BudgetLineController.java
├── BudgetCategoryController.java
├── BudgetTransferController.java
├── BudgetOverrideController.java
├── BudgetDashboardController.java
├── BudgetSettingsController.java
├── BudgetModuleActivationListener.java  # Event listeners
├── dto/                              # Request/Response DTOs
│   ├── CreateBudgetRequest.java
│   ├── CreateBudgetLineRequest.java
│   ├── CreateCategoryRequest.java
│   ├── CreateTransferRequest.java
│   ├── UpdateBudgetSettingsRequest.java
│   ├── UpdateBudgetLineRequest.java
│   ├── UpdateCategoryRequest.java
│   ├── RejectTransferRequest.java
│   ├── BudgetResponse.java
│   ├── BudgetLineResponse.java
│   ├── CategoryResponse.java
│   ├── TransferResponse.java
│   ├── OverrideResponse.java
│   ├── BudgetSettingsResponse.java
│   └── DashboardResponse.java
└── event/                            # Events published by BUDMGT
    ├── BudgetValidated.java
    ├── BudgetClosed.java
    ├── BudgetThresholdReached.java
    ├── BudgetExhausted.java
    ├── BudgetOverrideRequested.java
    ├── BudgetOverrideApproved.java
    ├── BudgetOverrideRejected.java
    ├── BudgetTransferRequested.java
    └── BudgetTransferDecided.java

frontend/src/modules/budget/
├── pages/
│   ├── BudgetListPage.tsx
│   ├── BudgetLinePage.tsx
│   ├── BudgetDashboardPage.tsx
│   ├── CategoryManagementPage.tsx
│   ├── TransferListPage.tsx
│   └── OverrideListPage.tsx
├── components/
│   ├── CreateBudgetWizard.tsx
│   ├── BudgetLineForm.tsx
│   ├── SyscohadaAccountPicker.tsx
│   ├── BudgetSummaryCards.tsx
│   ├── CategoryBreakdownChart.tsx
│   ├── DepartmentBreakdownTable.tsx
│   ├── BudgetLineStatusIndicator.tsx
│   ├── TransferRequestForm.tsx
│   ├── TransferApprovalCard.tsx
│   ├── OverrideApprovalCard.tsx
│   ├── CloseBudgetDialog.tsx
│   └── OfflineStalenessBar.tsx
├── hooks/
│   ├── useBudget.ts
│   ├── useBudgetLines.ts
│   ├── useBudgetDashboard.ts
│   ├── useBudgetTransfers.ts
│   ├── useBudgetOffline.ts
│   └── useBudgetCategories.ts
├── stores/
│   └── budgetOfflineStore.ts
├── api/
│   └── budgetApi.ts
└── sw/
    └── budget-cache-strategy.ts

End of Technical Architecture for Budget Lines Management (BUDMGT) v1.0