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/:
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