# Task 20 — Inventory → Finance Posting

## Goal
Create InventoryPostingService to generate journal entries for purchase receipts and stock adjustments.

---

## Service: `app/Services/Inventory/Finance/InventoryPostingService.php`

```php
namespace App\Services\Inventory\Finance;

use App\Models\Inventory\Purchase\PurchaseReceipt;
use App\Models\Inventory\StockMovement;
use App\Services\Finance\JournalEntryService;
use App\Models\Core\Setting;

class InventoryPostingService
{
    public function __construct(
        private JournalEntryService $journalEntryService
    ) {}

    // =====================================================
    // PURCHASE RECEIPT POSTING
    // DR: Inventory Account
    // CR: Accounts Payable
    // =====================================================

    public function postReceipt(PurchaseReceipt $receipt): void
    {
        DB::transaction(function () use ($receipt) {
            $lines = $this->buildReceiptJournalLines($receipt);
            $this->validateBalanced($lines, "Receipt #{$receipt->receipt_number}");

            $journalEntry = $this->journalEntryService->create([
                'journal_id'       => $this->getPurchaseJournalId(),
                'entry_date'       => $receipt->receipt_date,
                'branch_id'        => $receipt->branch_id,
                'fiscal_year_id'   => $this->getFiscalYearId($receipt->receipt_date),
                'fiscal_period_id' => $this->getFiscalPeriodId($receipt->receipt_date),
                'reference'        => $receipt->receipt_number,
                'description'      => "Purchase receipt: {$receipt->receipt_number}",
                'lines'            => $lines,
            ]);

            $receipt->update([
                'journal_entry_id' => $journalEntry->id,
                'status'           => 'posted',
                'posted_at'        => now(),
            ]);
        });
    }

    private function buildReceiptJournalLines(PurchaseReceipt $receipt): array
    {
        $inventoryByAccount = [];
        $totalCost = 0;

        foreach ($receipt->lines as $line) {
            if ($line->item->type !== 'stock') continue;

            $invAccountId = $this->resolveInventoryAccount($line->item);
            $lineCost     = (float) $line->total_cost;

            $inventoryByAccount[$invAccountId] = ($inventoryByAccount[$invAccountId] ?? 0) + $lineCost;
            $totalCost += $lineCost;
        }

        $lines = [];

        // DR: Inventory accounts (grouped by account)
        foreach ($inventoryByAccount as $accountId => $amount) {
            $lines[] = [
                'account_id' => $accountId,
                'debit'      => round($amount, 2),
                'credit'     => 0,
                'description'=> "Stock in: {$receipt->receipt_number}",
            ];
        }

        // CR: Accounts Payable (supplier)
        $lines[] = [
            'account_id' => $this->resolveAPAccount($receipt->supplier),
            'debit'      => 0,
            'credit'     => round($totalCost, 2),
            'description'=> "AP: {$receipt->supplier->code} — {$receipt->receipt_number}",
        ];

        return $lines;
    }

    // =====================================================
    // STOCK ADJUSTMENT POSTING
    // Positive adjustment: DR Inventory, CR Inventory Adjustment
    // Negative adjustment: DR Inventory Adjustment, CR Inventory
    // =====================================================

    public function postAdjustment(StockMovement $movement): void
    {
        DB::transaction(function () use ($movement) {
            $item         = $movement->item;
            $invAccountId = $this->resolveInventoryAccount($item);
            $adjAccountId = $this->getInventoryAdjustmentAccountId();
            $amount       = round((float) $movement->total_cost, 2);

            $isPositive = in_array($movement->movement_type, ['in', 'adjustment_positive']);

            $lines = $isPositive
                ? [
                    ['account_id' => $invAccountId, 'debit' => $amount, 'credit' => 0, 'description' => 'Inventory adjustment +'],
                    ['account_id' => $adjAccountId, 'debit' => 0, 'credit' => $amount, 'description' => 'Inventory adjustment +'],
                ]
                : [
                    ['account_id' => $adjAccountId, 'debit' => $amount, 'credit' => 0, 'description' => 'Inventory adjustment -'],
                    ['account_id' => $invAccountId, 'debit' => 0, 'credit' => $amount, 'description' => 'Inventory adjustment -'],
                ];

            $journalEntry = $this->journalEntryService->create([
                'journal_id'       => $this->getPurchaseJournalId(),
                'entry_date'       => $movement->movement_date,
                'branch_id'        => $movement->branch_id,
                'fiscal_year_id'   => $this->getFiscalYearId($movement->movement_date),
                'fiscal_period_id' => $this->getFiscalPeriodId($movement->movement_date),
                'reference'        => $movement->movement_number,
                'description'      => "Stock adjustment: {$item->code}",
                'lines'            => $lines,
            ]);

            $movement->update([
                'journal_entry_id' => $journalEntry->id,
                'posting_status'   => 'posted',
            ]);
        });
    }

    // =====================================================
    // RECEIPT REVERSAL
    // =====================================================

    public function reverseReceipt(PurchaseReceipt $receipt): void
    {
        // Reverse the original journal entry using the existing JournalEntryService::reverse()
        $this->journalEntryService->reverse($receipt->journalEntry);
        $receipt->update(['status' => 'reversed']);
    }

    // =====================================================
    // ACCOUNT RESOLUTION
    // =====================================================

    private function resolveInventoryAccount(Item $item): int
    {
        return $item->inventory_account_id
            ?? $item->category?->inventory_account_id
            ?? $this->getSystemSetting('default_inventory_account_id')
            ?? throw new \Exception("Inventory account not found for item: {$item->code}");
    }

    private function resolveAPAccount(Supplier $supplier): int
    {
        return $supplier->ap_account_id
            ?? $this->getSystemSetting('default_ap_account_id')
            ?? throw new \Exception('AP account not configured.');
    }

    private function getInventoryAdjustmentAccountId(): int
    {
        return $this->getSystemSetting('inventory_adjustment_account_id')
            ?? throw new \Exception('Inventory adjustment account not configured.');
    }

    private function validateBalanced(array $lines, string $context): void
    {
        $totalDebit  = array_sum(array_column($lines, 'debit'));
        $totalCredit = array_sum(array_column($lines, 'credit'));
        if (abs($totalDebit - $totalCredit) > 0.01) {
            throw new \Exception("Unbalanced journal for {$context}: DR={$totalDebit}, CR={$totalCredit}");
        }
    }

    private function getSystemSetting(string $key): ?int
    {
        return (int) Setting::where('key', $key)->value('value') ?: null;
    }

    private function getPurchaseJournalId(): int
    {
        return (int) Setting::where('key', 'purchase_journal_id')->value('value')
            ?? throw new \Exception('Purchase journal not configured in settings.');
    }
}
```

---

## Additional System Settings Required

| Key | Purpose |
|-----|---------|
| `purchase_journal_id` | Journal for purchase/inventory entries |
| `default_ap_account_id` | Default AP account |
| `inventory_adjustment_account_id` | Account for inventory adjustments |

---

## Test This Task

**Test 1: Purchase receipt posting**
- Receipt: 50 units of Item X @ 12.00 = 600
- Expected:
  - DR Inventory Account   600
  - CR Accounts Payable    600

**Test 2: Stock adjustment**
- Adjust up 10 units @ average_cost 10.00 = 100
- Expected:
  - DR Inventory      100
  - CR Inv Adjustment 100

**Test 3: Verify Trial Balance**
- After all postings, run trial balance
- Inventory account balance should equal stock_balances.total_value sum
