# Task 09 — Posting Engine

## Goal
Build the Posting Engine for accounting journal entries.
This task will take a draft journal entry and convert it into a posted accounting transaction, while updating period balances safely.

---

## Scope

This task covers:
- posting draft journal entries
- validating debit/credit balance
- validating fiscal period status
- preventing editing after posting
- updating account period balances
- reverse-safe foundations

This task does NOT cover:
- reverse entry implementation
- financial reports
- exchange gain/loss logic
- advanced audit logs

---

## Database

### Existing tables used
- journal_entries
- journal_entry_lines
- accounts
- journals
- branches
- fiscal_years
- fiscal_periods
- cost_centers

### New table required
Table: `account_period_balances`

Fields:
- id
- branch_id (FK to branches.id)
- fiscal_year_id (FK to fiscal_years.id)
- fiscal_period_id (FK to fiscal_periods.id)
- account_id (FK to accounts.id)
- cost_center_id (nullable, FK to cost_centers.id)
- cost_center_key (unsignedBigInteger default 0)
- opening_debit (decimal 18,2 default 0)
- opening_credit (decimal 18,2 default 0)
- period_debit (decimal 18,2 default 0)
- period_credit (decimal 18,2 default 0)
- closing_debit (decimal 18,2 default 0)
- closing_credit (decimal 18,2 default 0)
- created_at
- updated_at

Indexes:
- index branch_id
- index fiscal_year_id
- index fiscal_period_id
- index account_id
- index cost_center_id
- unique(branch_id, fiscal_period_id, account_id, cost_center_key)

Notes:
- cost_center_key is required for MySQL-safe uniqueness when cost_center_id is nullable
- if cost_center_id is null, cost_center_key must be 0

---

## Relations

### AccountPeriodBalance model relations
- branch() -> belongsTo Branch
- fiscalYear() -> belongsTo FiscalYear
- fiscalPeriod() -> belongsTo FiscalPeriod
- account() -> belongsTo Account
- costCenter() -> belongsTo CostCenter

### JournalEntry future relation update
- balances are not direct children, but posting updates AccountPeriodBalance rows

---

## Business Rules

1. Only `draft` journal entries can be posted.
2. Posted entries become immutable.
3. Total debit must equal total credit.
4. Each line must contain either debit or credit.
5. Journal entry must contain at least one line.
6. Fiscal period must be open.
7. Only postable accounts can receive journal lines.
8. Posting must use `DB::transaction()`.
9. Posting must use row locking where balances are updated.
10. Posting must update or create rows in `account_period_balances`.
11. Opening balance should be derived from previous fiscal period if balance row does not exist yet.
12. If cost center is null, aggregate on cost_center_key = 0.
13. No entry editing after posting.

---

## Posting Logic

### Step-by-step posting flow
1. Load journal entry and lines.
2. Validate entry status is `draft`.
3. Validate fiscal period is `open`.
4. Validate all lines.
5. Validate totals: debit == credit.
6. Start DB transaction.
7. For each line:
   - get or create account_period_balances row with row lock
   - bring opening from previous period if needed
   - increase period_debit / period_credit
   - recalculate closing_debit / closing_credit
8. Update journal entry:
   - status = posted
   - posted_at
   - posted_by
9. Commit transaction.

---

## Balance Calculation Rules

Use net movement:
- opening_net = opening_debit - opening_credit
- period_net = period_debit - period_credit
- closing_net = opening_net + period_net

If closing_net >= 0:
- closing_debit = closing_net
- closing_credit = 0

If closing_net < 0:
- closing_debit = 0
- closing_credit = abs(closing_net)

---

## Files

### Model
- app/Models/Finance/AccountPeriodBalance.php

### Service
- app/Services/Finance/Posting/JournalPostingService.php
- app/Services/Finance/FiscalPeriodService.php (extend if needed)

### Controller
- app/Http/Controllers/Admin/Finance/JournalEntryPostController.php

### Routes
- add posting route in `routes/admin.php`

### Lang files
- lang/ar/finance.php
- lang/en/finance.php

---

## Permissions

- finance.entries.post

---

## Validation / Safety Rules

- posting route must be protected by permission `finance.entries.post`
- posting must reject draft entries with invalid totals
- posting must reject closed periods
- posting must reject non-postable accounts

---

## UI

No full UI module required in this task.
Only add Post action button later in journal entries list/show when needed.

---

## Lang Keys

Suggested keys:
- post_entry
- posted
- cannot_post_entry
- period_closed
- unbalanced_entry
- non_postable_account

---

## Seeder

Update RolePermissionSeeder with:
- finance.entries.post

---

## Steps

### Step 1
Create migration for `account_period_balances` table.

### Step 2
Create `AccountPeriodBalance` model.

### Step 3
Create `JournalPostingService`.

### Step 4
Extend or create `FiscalPeriodService` support for `assertOpen()` and period lookup if needed.

### Step 5
Create `JournalEntryPostController`.

### Step 6
Add posting route in `routes/admin.php`.

### Step 7
Update lang files.

### Step 8
Update `RolePermissionSeeder` with `finance.entries.post`.

---

## Notes for AI

- Follow docs/ai/AI_MASTER_PROJECT.md
- Follow docs/ai/ARCHITECTURE_RULES.md
- Follow docs/ai/DATABASE_SCHEMA.md
- Follow docs/ai/DATABASE_RELATION_DIAGRAM.md
- Follow docs/ai/MODULE_STRUCTURE.md
- Execute one step at a time
- Do not modify unrelated files
- Do not generate reverse logic in this task
- Do not generate tests in this task
- Wait for confirmation after each step

