# FSD: Koperasi Bermadani v2.0 — Technical Specification

## 1. Ringkasan Teknis
- **Rebuild goals**: proper accounting foundation, double-entry GL, fiscal period locking
- **Keep what works**: domain-driven structure
- **New Architecture**: Inertia.js + React for premium SPA feel
- **Fix what's broken**: denormalized balances, synthesized reports, fragile SHU

## 2. Technology Stack

### 2.1 Backend
- Framework: Laravel 11.x
- PHP: 8.2+
- Database: MySQL 8.0+ / MariaDB 10.6+
- Queue: Laravel Queue (Redis preferred)
- Cache: Redis

### 2.2 Frontend
- Inertia.js (SPA routing)
- React (client-side UI & interactivity)
- shadcn/ui (UI component library)
- Tailwind CSS v4
- Vite (asset bundling)

### 2.3 Infrastructure
- Deployment: Self-hosted / Cloud VPS
- Web Server: Nginx
- Process Manager: Supervisor (for queues)
- Storage: Local filesystem (with S3 option)

## 3. Arsitektur Sistem

### 3.1 Domain-Driven Design
Document the domain structure:
```text
app/
├── Domains/
│   ├── Koperasi/          # Core cooperative: members, savings, loans
│   │   ├── Models/
│   │   ├── Services/
│   │   ├── Actions/
│   │   └── Events/
│   ├── Accounting/        # GL, CoA, journals, reports
│   │   ├── Models/
│   │   ├── Services/
│   │   └── Events/
│   ├── Minimarket/        # POS, products, stock
│   │   ├── Models/
│   │   ├── Services/
│   │   └── Events/
│   └── Supplier/          # Consignment, supplier management
│       ├── Models/
│       ├── Services/
│       └── Events/
├── Models/                # Aliases (extend domain models)
└── Http/                  # Controllers, Middleware

resources/
├── js/
│   ├── Pages/             # Inertia React Pages
│   └── Components/        # React components (shadcn/ui, etc)
```

### 3.2 Three-Layer Financial Architecture
This is the CORE architectural change.

```mermaid
flowchart TD
    subgraph Layer 1: Operational (Business transactions)
        ST[Simpanan Transactions]
        LP[Loan Payments]
        POS[POS Sales]
        FT[Financial Transactions]
    end

    subgraph Layer 2: General Ledger (Double-entry source of truth)
        J[Journals]
        JE[Journal Entries]
        COA[Chart of Accounts]
    end

    subgraph Layer 3: Reporting (Financial statements)
        BS[Balance Sheet]
        IS[Income Statement]
        CF[Cash Flow]
        SHU[SHU Reports]
    end

    ST -- auto-post journals --> J
    LP -- auto-post journals --> J
    POS -- auto-post journals --> J
    FT -- auto-post journals --> J

    J --> JE
    JE --> COA

    JE -. aggregate & query .-> BS
    JE -. aggregate & query .-> IS
    JE -. aggregate & query .-> CF
    JE -. aggregate & query .-> SHU
```

Every operational transaction (simpanan setor, loan payment, POS sale, etc.) MUST automatically create a balanced journal entry in Layer 2.

Layer 3 reports ONLY read from Layer 2 journals — never directly from Layer 1 tables.

### 3.3 Event-Driven Journal Posting
1. Operational transaction created (e.g., SimpananTransaction)
2. Laravel Event fired (e.g., SimpananDeposited)
3. Listener creates Journal + JournalEntries (debit/credit)
4. Journal validation: sum(debit) MUST equal sum(credit)
5. If validation fails, transaction is rolled back

## 4. Database Schema

### 4.1 NEW: Accounting Core Tables

#### `chart_of_accounts`
```sql
CREATE TABLE chart_of_accounts (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    code VARCHAR(20) NOT NULL UNIQUE,      -- e.g. '1.1.01'
    name VARCHAR(255) NOT NULL,             -- e.g. 'Kas'
    type ENUM('ASET', 'LIABILITAS', 'EKUITAS', 'PENDAPATAN', 'BEBAN') NOT NULL,
    normal_balance ENUM('DEBIT', 'CREDIT') NOT NULL,  -- natural balance side
    parent_id BIGINT UNSIGNED NULL,         -- for hierarchical accounts
    level TINYINT NOT NULL DEFAULT 1,       -- 1=header, 2=sub, 3=detail
    is_active BOOLEAN DEFAULT TRUE,
    is_system BOOLEAN DEFAULT FALSE,        -- system accounts can't be deleted
    description TEXT NULL,
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    FOREIGN KEY (parent_id) REFERENCES chart_of_accounts(id) ON DELETE SET NULL,
    INDEX idx_type (type),
    INDEX idx_code (code)
);
```

Account numbering convention:
- 1.x.xx = ASET (Debit normal)
  - 1.1.xx = Aset Lancar
  - 1.2.xx = Piutang
  - 1.3.xx = Persediaan
  - 1.4.xx = Aset Tetap
- 2.x.xx = LIABILITAS (Credit normal)
  - 2.1.xx = Kewajiban Jangka Pendek
- 3.x.xx = EKUITAS (Credit normal)
  - 3.1.xx = Modal & Simpanan
  - 3.2.xx = Cadangan & SHU
- 4.x.xx = PENDAPATAN (Credit normal)
  - 4.1.xx = Pendapatan Operasional
  - 4.2.xx = Pendapatan Non-Operasional
- 5.x.xx = BEBAN (Debit normal)
  - 5.1.xx = Beban Operasional
  - 5.2.xx = Beban Non-Operasional

Default seed accounts:
```text
1.1.01  Kas                          ASET        DEBIT
1.1.02  Bank BSI                     ASET        DEBIT
1.1.03  Bank Lainnya                 ASET        DEBIT
1.2.01  Piutang Pembiayaan Bermadani ASET        DEBIT
1.2.02  Piutang Pembiayaan BMT Itqan ASET        DEBIT
1.2.03  Cadangan Kerugian Piutang    ASET        DEBIT  (contra, negative)
1.2.04  Piutang Lain-lain            ASET        DEBIT
1.3.01  Persediaan Barang Dagangan   ASET        DEBIT
1.4.01  Peralatan Kantor             ASET        DEBIT
1.4.02  Kendaraan                    ASET        DEBIT
1.4.03  Bangunan                     ASET        DEBIT
1.4.04  Aset Tetap Lainnya           ASET        DEBIT
1.4.99  Akumulasi Penyusutan         ASET        CREDIT (contra)
2.1.01  Simpanan Sukarela Anggota    LIABILITAS  CREDIT
2.1.02  Utang Usaha                  LIABILITAS  CREDIT
2.1.03  Utang Supplier/Konsinyasi    LIABILITAS  CREDIT
2.1.04  Kewajiban Lain-lain          LIABILITAS  CREDIT
2.1.05  Simpanan Berjangka           LIABILITAS  CREDIT
3.1.01  Simpanan Pokok               EKUITAS     CREDIT
3.1.02  Simpanan Wajib               EKUITAS     CREDIT
3.2.01  Cadangan Umum                EKUITAS     CREDIT
3.2.02  Cadangan Risiko              EKUITAS     CREDIT
3.2.03  SHU Tahun Berjalan           EKUITAS     CREDIT
3.2.04  SHU Tahun Lalu               EKUITAS     CREDIT
4.1.01  Pendapatan Margin Bermadani  PENDAPATAN  CREDIT
4.1.02  Pendapatan Margin BMT Itqan  PENDAPATAN  CREDIT
4.1.03  Pendapatan Administrasi      PENDAPATAN  CREDIT
4.1.04  Pendapatan Denda             PENDAPATAN  CREDIT
4.2.01  Pendapatan Penjualan Toko    PENDAPATAN  CREDIT
4.2.02  Pendapatan Konsinyasi        PENDAPATAN  CREDIT
4.2.03  Pendapatan Lain-lain         PENDAPATAN  CREDIT
5.1.01  Beban Gaji & Tunjangan       BEBAN       DEBIT
5.1.02  Beban ATK & Perlengkapan     BEBAN       DEBIT
5.1.03  Beban Listrik & Air          BEBAN       DEBIT
5.1.04  Beban Telekomunikasi         BEBAN       DEBIT
5.1.05  Beban Penyusutan             BEBAN       DEBIT
5.1.06  Beban Kebersihan             BEBAN       DEBIT
5.1.07  Beban Transportasi           BEBAN       DEBIT
5.1.08  Beban Pemeliharaan           BEBAN       DEBIT
5.1.09  Beban Sewa                   BEBAN       DEBIT
5.1.10  Beban Asuransi               BEBAN       DEBIT
5.2.01  HPP Barang Dagangan          BEBAN       DEBIT
5.2.02  Beban Supplier Fee           BEBAN       DEBIT
5.2.03  Beban Operasional Lain       BEBAN       DEBIT
```

#### `journals`
```sql
CREATE TABLE journals (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    journal_number VARCHAR(50) NOT NULL UNIQUE,  -- format: JU-YYYYMM-NNNNN
    transaction_date DATE NOT NULL,
    description VARCHAR(500) NOT NULL,
    source_type VARCHAR(100) NULL,    -- morph type: 'simpanan_transaction', 'loan_payment', etc.
    source_id BIGINT UNSIGNED NULL,   -- morph id
    fiscal_period_id BIGINT UNSIGNED NOT NULL,
    is_posted BOOLEAN DEFAULT FALSE,
    is_reversed BOOLEAN DEFAULT FALSE,
    reversed_by_journal_id BIGINT UNSIGNED NULL,
    created_by BIGINT UNSIGNED NULL,
    posted_at TIMESTAMP NULL,
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    FOREIGN KEY (fiscal_period_id) REFERENCES fiscal_periods(id),
    FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL,
    FOREIGN KEY (reversed_by_journal_id) REFERENCES journals(id) ON DELETE SET NULL,
    INDEX idx_date (transaction_date),
    INDEX idx_period (fiscal_period_id),
    INDEX idx_source (source_type, source_id),
    INDEX idx_posted (is_posted)
);
```

#### `journal_entries`
```sql
CREATE TABLE journal_entries (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    journal_id BIGINT UNSIGNED NOT NULL,
    account_id BIGINT UNSIGNED NOT NULL,
    debit DECIMAL(15,2) NOT NULL DEFAULT 0,
    credit DECIMAL(15,2) NOT NULL DEFAULT 0,
    member_id BIGINT UNSIGNED NULL,      -- for sub-ledger tracking per member
    supplier_id BIGINT UNSIGNED NULL,    -- for sub-ledger tracking per supplier
    description VARCHAR(500) NULL,
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    FOREIGN KEY (journal_id) REFERENCES journals(id) ON DELETE CASCADE,
    FOREIGN KEY (account_id) REFERENCES chart_of_accounts(id),
    FOREIGN KEY (member_id) REFERENCES members(id) ON DELETE SET NULL,
    INDEX idx_journal (journal_id),
    INDEX idx_account (account_id),
    INDEX idx_member (member_id),
    INDEX idx_account_date (account_id, created_at),
    CHECK (debit >= 0),
    CHECK (credit >= 0),
    CHECK (debit > 0 OR credit > 0),     -- at least one must be positive
    CHECK (NOT (debit > 0 AND credit > 0)) -- can't be both positive
);
```

#### `fiscal_periods`
```sql
CREATE TABLE fiscal_periods (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    code VARCHAR(6) NOT NULL UNIQUE,       -- format: YYYYMM
    year SMALLINT NOT NULL,
    month TINYINT NOT NULL,
    start_date DATE NOT NULL,
    end_date DATE NOT NULL,
    status ENUM('OPEN', 'CLOSED', 'LOCKED') NOT NULL DEFAULT 'OPEN',
    closed_at TIMESTAMP NULL,
    closed_by BIGINT UNSIGNED NULL,
    notes TEXT NULL,
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    FOREIGN KEY (closed_by) REFERENCES users(id) ON DELETE SET NULL,
    UNIQUE INDEX idx_year_month (year, month)
);
```

### 4.2 EXISTING Tables (Kept with Modifications)

#### `members` table
Keep all columns. ADD:
- Remove reliance on `simpananPokok`, `simpananWajib`, `simpananSukarela` as source of truth
- These columns become CACHE columns, periodically reconciled from journal_entries
- Add `last_balance_sync_at` TIMESTAMP NULL

Columns (current):
- id, nik, memberNumber (YYNNNNNN), name, address, phone, email
- unitKerja, position, joinDate, status (`AKTIF`, `NONAKTIF`, `KELUAR`, `MENINGGAL`)
- simpananPokok, simpananWajib, simpananSukarela (DECIMAL - become cache)
- monthlyWajibAmount (default 50000), simpananWajibPaymentMethod
- points, memberTier (BRONZE/SILVER/GOLD/PLATINUM)
- isMemberKoperasi, photo, documents
- statusNote, created_at, updated_at

#### `simpanan_transactions` table
Keep as-is. ADD event listener to auto-post journal on creation.
Columns: id, memberId, type (POKOK/WAJIB/SUKARELA), transactionType (SETOR/TARIK/TRANSFER), amount, balanceBefore, balanceAfter, description, status, receiptNumber, billingMonth, isAutoDebit, transferToMemberId, isRead, created_at, updated_at

Journal posting rules:
- SETOR POKOK: Dr. Kas → Cr. Simpanan Pokok
- SETOR WAJIB: Dr. Kas → Cr. Simpanan Wajib  
- SETOR SUKARELA: Dr. Kas → Cr. Simpanan Sukarela
- TARIK SUKARELA: Dr. Simpanan Sukarela → Cr. Kas
- TRANSFER: Dr. Simpanan Sukarela (sender) → Cr. Simpanan Sukarela (receiver)

#### `loans` table
Keep as-is. ADD event listener for disbursement journal.
Columns: id, memberId, loanSource (BERMADANI/BMT_ITQAN), baseAmount, interestRate, interestAmount, tenor, monthlyInstallment, remainingAmount, totalAmount, disbursementDate, status, approvedBy, adminFee, simwaAmount, etc.

Journal posting rules:
- Loan Disbursement: Dr. Piutang Pembiayaan → Cr. Kas
- Admin Fee: Dr. Kas → Cr. Pendapatan Administrasi

#### `loan_payments` table
Keep as-is. ADD event listener.
Columns: id, loanId, amount, principalAmount, interestAmount, penalty_amount (DECIMAL default 0), paymentDate, paymentMethod, receiptNumber

Journal posting rules:
- Payment: Dr. Kas → Cr. Piutang Pembiayaan (principal portion) + Cr. Pendapatan Margin (interest portion)
- Late penalty: Dr. Kas → Cr. 4.1.04 Pendapatan Denda (amount = penalty_amount)

#### `transactions` table (POS)
Keep as-is. ADD event listener.
Journal posting rules:
- Cash Sale: Dr. Kas → Cr. Pendapatan Penjualan
- Simpanan Deduction Sale: Dr. Simpanan Sukarela → Cr. Pendapatan Penjualan
- COGS: Dr. HPP Barang Dagangan → Cr. Persediaan

#### `financial_transactions` table (Manual)
Keep as-is. ADD event listener.
Journal posting rules:
- INCOME: Dr. Kas/Bank → Cr. [specific revenue account based on category mapping]
- EXPENSE: Dr. [specific expense account based on category mapping] → Cr. Kas/Bank

#### `bank_transactions` table
Keep as-is. Journal posting handled via financial_transactions or manual journal.

#### `fixed_assets` table
Keep as-is from recent creation.
Journal posting rules:
- Acquisition: Dr. Aset Tetap → Cr. Kas
- Monthly Depreciation: Dr. Beban Penyusutan → Cr. Akumulasi Penyusutan

#### `simpanan_berjangka` (Phase 2: Term Deposits)
```sql
CREATE TABLE simpanan_berjangka (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    member_id BIGINT UNSIGNED NOT NULL,
    amount DECIMAL(15,2) NOT NULL,
    nisbah_rate DECIMAL(5,2) NOT NULL,  -- profit sharing rate %
    tenor_months INT NOT NULL,
    start_date DATE NOT NULL,
    maturity_date DATE NOT NULL,
    status ENUM('ACTIVE', 'MATURED', 'EARLY_WITHDRAWN', 'ROLLED_OVER') DEFAULT 'ACTIVE',
    auto_rollover BOOLEAN DEFAULT FALSE,
    notes TEXT NULL,
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    FOREIGN KEY (member_id) REFERENCES members(id)
);
```

Journal posting rules:
- Opening: Dr. Kas → Cr. 2.1.05 Simpanan Berjangka
- Maturity profit: Dr. 5.1.xx Beban Bagi Hasil → Cr. 2.1.05 Simpanan Berjangka
- Withdrawal: Dr. 2.1.05 Simpanan Berjangka → Cr. Kas

#### `rat_book_sections`
```sql
CREATE TABLE rat_book_sections (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    rat_session_id BIGINT UNSIGNED NOT NULL,
    chapter_number TINYINT NOT NULL,  -- 1-8
    title VARCHAR(255) NOT NULL,
    content JSON NOT NULL,  -- flexible content per chapter
    is_auto_generated BOOLEAN DEFAULT FALSE,
    updated_by BIGINT UNSIGNED NULL,
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    FOREIGN KEY (rat_session_id) REFERENCES rat_sessions(id) ON DELETE CASCADE,
    FOREIGN KEY (updated_by) REFERENCES users(id) ON DELETE SET NULL,
    UNIQUE INDEX idx_session_chapter (rat_session_id, chapter_number)
);
```

#### Other tables to keep as-is:
- products, categories, suppliers
- consignment_batches, consignment_items
- supplier_payouts, supplier_payout_allocations
- cashier_shifts, stock_movements
- rat_sessions, member_shu_distributions, rat_manual_entries
- financial_report_snapshots, calk_entries
- users, activity_logs, notifications
- cooperative_settings (add `fin_loan_penalty_rate DECIMAL(5,2) DEFAULT 0; -- % per month`)
- member_settlements

### 4.3 Category-to-Account Mapping Table

NEW table to replace hardcoded string matching:
```sql
CREATE TABLE category_account_mappings (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    category_name VARCHAR(255) NOT NULL,       -- the string used in financial_transactions
    transaction_type ENUM('INCOME', 'EXPENSE') NOT NULL,
    account_id BIGINT UNSIGNED NOT NULL,       -- maps to chart_of_accounts
    created_at TIMESTAMP,
    updated_at TIMESTAMP,
    FOREIGN KEY (account_id) REFERENCES chart_of_accounts(id),
    UNIQUE INDEX idx_category_type (category_name, transaction_type)
);
```

### 4.4 Entity Relationship Diagram
```mermaid
erDiagram
    %% Core Accounting
    CHART_OF_ACCOUNTS ||--o{ JOURNAL_ENTRIES : contains
    JOURNALS ||--o{ JOURNAL_ENTRIES : has
    FISCAL_PERIODS ||--o{ JOURNALS : tracks

    %% Koperasi Domain
    MEMBERS ||--o{ SIMPANAN_TRANSACTIONS : makes
    MEMBERS ||--o{ LOANS : has
    LOANS ||--o{ LOAN_PAYMENTS : receives
    MEMBERS ||--o{ JOURNAL_ENTRIES : subledger
    SIMPANAN_TRANSACTIONS ||--o| JOURNALS : triggers
    LOANS ||--o| JOURNALS : triggers
    LOAN_PAYMENTS ||--o| JOURNALS : triggers

    %% Minimarket & Finance
    TRANSACTIONS ||--o| JOURNALS : triggers
    FINANCIAL_TRANSACTIONS ||--o| JOURNALS : triggers
    CHART_OF_ACCOUNTS ||--o{ CATEGORY_ACCOUNT_MAPPINGS : maps_to
```

## 5. Service Architecture

### 5.1 NEW: JournalService
```php
class JournalService
{
    public function createJournal(array $data): Journal;
    public function postJournal(Journal $journal): void;
    public function reverseJournal(Journal $journal, string $reason): Journal;
    public function validateBalance(Journal $journal): bool; // sum(debit) == sum(credit)
    public function getNextJournalNumber(string $period): string;
}
```

### 5.2 NEW: GeneralLedgerService
```php
class GeneralLedgerService
{
    public function getAccountBalance(int $accountId, ?string $asOfDate = null): float;
    public function getTrialBalance(string $periodCode): array;
    public function getAccountLedger(int $accountId, string $startDate, string $endDate): Collection;
    public function getMemberSubLedger(int $memberId, int $accountId): Collection;
}
```

### 5.3 NEW: FiscalPeriodService
```php
class FiscalPeriodService
{
    public function getCurrentPeriod(): FiscalPeriod;
    public function closePeriod(FiscalPeriod $period): void;
    public function lockPeriod(FiscalPeriod $period): void;  // permanent
    public function reopenPeriod(FiscalPeriod $period): void; // only CLOSED, not LOCKED
    public function ensurePeriodOpen(string $date): void;     // throws if closed/locked
}
```

### 5.4 REFACTORED: FinancialStatementService
Refactor to read ONLY from journals:
```php
class FinancialStatementService
{
    public function getBalanceSheet(int $year): array;
    // Implementation: query journal_entries grouped by account type
    // Aset = SUM(debit - credit) WHERE type = 'ASET'
    // Liabilitas = SUM(credit - debit) WHERE type = 'LIABILITAS'
    // Ekuitas = SUM(credit - debit) WHERE type = 'EKUITAS'
    // ALWAYS balanced by construction

    public function getIncomeStatement(int $year): array;
    // Pendapatan = SUM(credit - debit) WHERE type = 'PENDAPATAN' AND period in year
    // Beban = SUM(debit - credit) WHERE type = 'BEBAN' AND period in year
    // SHU = Pendapatan - Beban

    public function getEquityChanges(int $year): array;
    public function getCashFlowStatement(int $year): array;
    public function getHealthScorecard(int $year): array;
}
```

### 5.5 REFACTORED: ShuCalculationService
Refactor to use fiscal period locked data:
```php
class ShuCalculationService
{
    public function calculateFromLedger(int $fiscalYear): array;
    // Read SHU from GL: Pendapatan - Beban for the locked fiscal year
    // Member proportions calculated from journal_entries sub-ledger
}
```

### 5.6 NEW: PengawasService
```php
class PengawasService
{
    // Aggregates read-only data for the supervisory board
    public function getDashboardSummary(int $year): array;
    // Returns: financial health indicators, member stats, loan portfolio quality
    
    public function getAuditTrail(string $startDate, string $endDate): Collection;
    // Returns: filtered activity logs for audit purposes
    
    public function getReconciliationStatus(): array;
    // Returns: status of simpanan reconciliation, loan audit, bank matching
}
```

### 5.7 NEW: RatBookGeneratorService
```php
class RatBookGeneratorService
{
    public function generateBook(int $fiscalYear): RatBook;
    // Collects data for all 8 chapters:
    // BAB I: Cooperative profile from cooperative_settings
    // BAB II: Board activities summary (manual input + auto metrics)
    // BAB III: Financial statements from GL (Neraca, Laba Rugi, Arus Kas, Perubahan Ekuitas)
    // BAB IV: Member growth (joins, resignations, total, charts)
    // BAB V: SHU calculation and distribution from locked fiscal periods
    // BAB VI: Work plan (manually edited by Pengurus, stored in rat_manual_entries)
    // BAB VII: Budget plan (manually edited, stored in rat_manual_entries)
    // BAB VIII: Closing (template)
    
    public function exportToPdf(RatBook $book): string; // returns PDF path
}
```

### 5.8 NEW: WhatsAppNotificationService (Phase 2-3)
```php
class WhatsAppNotificationService 
{
    public function sendInstallmentReminder(Loan $loan, int $daysBefore = 3): void;
    public function sendOverdueNotice(Loan $loan): void;
    public function sendRatAnnouncement(RatSession $session): void;
    public function sendShuDistributionNotice(MemberShuDistribution $distribution): void;
}
```
*Note: Integration options include Fonnte, Wablas, or official WhatsApp Business API.*

## 6. Journal Posting Specifications

### 6.1 Simpanan Transactions
| Transaction | Debit Account | Credit Account | Amount |
|---|---|---|---|
| Setor Simpanan Pokok | 1.1.01 Kas | 3.1.01 Simpanan Pokok | amount |
| Setor Simpanan Wajib | 1.1.01 Kas | 3.1.02 Simpanan Wajib | amount |
| Setor Simpanan Sukarela | 1.1.01 Kas | 2.1.01 Simpanan Sukarela | amount |
| Tarik Simpanan Sukarela | 2.1.01 Simpanan Sukarela | 1.1.01 Kas | amount |
| Transfer Sukarela | 2.1.01 SimSuk (sender) | 2.1.01 SimSuk (receiver) | amount |

### 6.2 Loan Transactions
| Transaction | Debit Account | Credit Account | Amount |
|---|---|---|---|
| Pencairan Pinjaman Bermadani | 1.2.01 Piutang Bermadani | 1.1.01 Kas | baseAmount |
| Pencairan Pinjaman BMT Itqan | 1.2.02 Piutang BMT Itqan | 1.1.01 Kas | baseAmount |
| Admin Fee Pinjaman | 1.1.01 Kas | 4.1.03 Pend. Administrasi | adminFee |
| Angsuran Pokok | 1.1.01 Kas | 1.2.01 Piutang | principalAmount |
| Angsuran Margin | 1.1.01 Kas | 4.1.01 Pend. Margin | interestAmount |
| Denda Keterlambatan | 1.1.01 Kas | 4.1.04 Pendapatan Denda | penalty_amount |

### 6.3 POS Transactions
| Transaction | Debit Account | Credit Account | Amount |
|---|---|---|---|
| Penjualan Tunai | 1.1.01 Kas | 4.2.01 Pend. Penjualan | totalAmount |
| Penjualan via SimSuk | 2.1.01 SimSuk Anggota | 4.2.01 Pend. Penjualan | totalAmount |
| HPP Barang | 5.2.01 HPP | 1.3.01 Persediaan | costOfGoods |

### 6.4 Consignment
| Transaction | Debit Account | Credit Account | Amount |
|---|---|---|---|
| Payout ke Supplier | 2.1.03 Utang Supplier | 1.1.01 Kas | payableAmount |
| Pendapatan Margin Konsinyasi | [included in POS sale] | 4.2.02 Pend. Konsinyasi | coopMargin |

### 6.5 Fixed Assets
| Transaction | Debit Account | Credit Account | Amount |
|---|---|---|---|
| Pembelian Aset | 1.4.xx Aset Tetap | 1.1.01 Kas | acquisitionCost |
| Penyusutan Bulanan | 5.1.05 Beban Penyusutan | 1.4.99 Akum. Penyusutan | monthlyAmount |

### 6.6 Manual Financial Transactions
| Transaction | Debit Account | Credit Account | Amount |
|---|---|---|---|
| Income | 1.1.01 Kas / 1.1.02 Bank | [mapped via category_account_mappings] | amount |
| Expense | [mapped via category_account_mappings] | 1.1.01 Kas / 1.1.02 Bank | amount |

## 7. Authentication & Authorization

### 7.1 Auth Guards
- `web` (default): Users (Pengurus, Staf, Pengawas, Kasir, Anggota)
- `supplier`: Suppliers (separate authentication)

### 7.2 Middleware Stack
| Middleware | Alias | Purpose |
|---|---|---|
| CheckRole | role | RBAC enforcement (e.g. `role:PENGURUS,STAF`, `role:PENGAWAS`, `role:KASIR`) |
| CheckCashierShift | cashier.shift | Kasir must have open shift |
| CheckSupplierStatus | supplier.status | Supplier approval check |
| CheckMemberType | member.type | Route cooperative vs retail members |
| EnsureActiveMember | member.active | Block resigned members from mutations |
| LogActivity | log.activity | Audit trail for state changes |
| EnsureAppIsInstalled | installed | Redirect to installer if needed |

### 7.3 Role Permissions Matrix
| Role | Area | Core Permissions |
|---|---|---|
| PENGURUS | All System | Full CRUD all modules, manage users, close/lock fiscal periods, RAT management, system settings, generate reports |
| PENGAWAS | Read-Only All | VIEW all financial statements, audit trails, member data, loan portfolios. NO create/update/delete. Dashboard Pengawas access |
| STAF | Operations | Manage members, process simpanan/loans, view reports (cannot close periods, cannot manage users, cannot access settings) |
| KASIR | Point of Sale | Open/close shift, POS transactions, receive goods, view own shift history |
| ANGGOTA | Member Portal | View own simpanan/loans/transactions, transfer sukarela, edit own profile |
| SUPPLIER | Supplier Portal | Manage own products, view sales, request restock (separate auth guard) |

### 7.4 Two-Factor Authentication (2FA)
Wajib untuk role `PENGURUS` dan `PENGAWAS` karena memiliki akses ke data keuangan sensitif.

**Implementasi:**
- Primary: TOTP (Time-based One-Time Password) via Google Authenticator / Authy
- Fallback: OTP via WhatsApp (menggunakan WhatsAppNotificationService)
- 2FA enforced pada setiap login dan saat melakukan aksi kritikal (tutup buku, reverse journal, unlock fiscal period)

```php
class TwoFactorService
{
    public function isRequired(User $user): bool;  // true for PENGURUS, PENGAWAS
    public function generateSecret(User $user): string;
    public function verifyCode(User $user, string $code): bool;
    public function sendWhatsAppOtp(User $user): void;  // fallback
}
```

### 7.5 Data Privacy & Security
- **Enkripsi at-rest:** Kolom sensitif (NIK, nomor HP) dienkripsi di database menggunakan Laravel's `Crypt` facade
- **Data Export:** Endpoint untuk anggota meng-export data pribadi (simpanan, pinjaman, transaksi) dalam format CSV/PDF
- **Data Retention:** Data anggota berstatus `KELUAR`/`MENINGGAL` disimpan minimal 5 tahun, setelahnya bisa diarsipkan
- **Privacy Policy:** Route publik `/privacy-policy` wajib tersedia di landing page

## 8. API & Route Architecture

- Admin: `/admin/*` (role: PENGURUS, STAF) — operations
- Pengawas: `/pengawas/*` (role: PENGAWAS) — read-only audit dashboard
- Kasir: `/kasir/*` (role: KASIR)
- Member: `/member/*` (auth + member.type)
- Membership: `/membership/*` (auth + member.type)
- Supplier: `/supplier/*` (auth:supplier)
- Public: `/`, `/login`, `/daftar-supplier`

## 9. Data Migration Strategy

### 9.1 Phase 1: Create New Tables
- Create chart_of_accounts, journals, journal_entries, fiscal_periods, category_account_mappings
- Seed chart_of_accounts with default accounts
- Create fiscal_periods for all historical months
- Add new tables for future phases (`simpanan_berjangka`, `rat_book_sections`)

### 9.2 Phase 2: Generate Historical Journals
- Script to iterate ALL historical simpanan_transactions → create journals
- Script to iterate ALL historical loan_payments → create journals
- Script to iterate ALL historical POS transactions → create journals
- Script to iterate ALL historical financial_transactions → create journals
- Validate: trial balance must zero out

### 9.3 Phase 3: Reconcile & Lock
- Compare journal-derived balances vs current denormalized balances
- Fix any discrepancies
- Lock all historical fiscal periods
- Mark current period as OPEN

### 9.4 Phase 4: Switch Services
- Replace FinancialStatementService to read from GL
- Replace ShuCalculationService to use GL
- Add event listeners for auto-posting
- Keep denormalized columns as cache with periodic reconciliation

## 10. Multi-Tenant Architecture (Future)

Document the planned approach:
- Option A: Database-per-tenant (strongest isolation)
- Option B: Schema-per-tenant 
- Option C: Shared database with tenant_id column

Recommendation: Start with Option C (tenant_id) for simplicity, migrate to Option A when scaling.

Changes needed:
- Add `tenant_id` to all tables
- Add global scope for tenant filtering
- Tenant resolution via subdomain or path

## 11. Testing Strategy

### 11.1 Unit Tests
- JournalService: balance validation, number generation
- GeneralLedgerService: account balance calculations
- ShuCalculationService: formula accuracy

### 11.2 Integration Tests
- Simpanan → Journal posting → Balance verification
- Loan lifecycle → Journal posting → Report accuracy
- Fiscal period locking → Journal rejection

### 11.3 Data Integrity Tests
- Trial balance always zeros
- Denormalized cache matches journal-derived balances
- No orphaned journal entries

## 12. Performance Considerations
- Index strategy for journal_entries (account_id + date range queries)
- Materialized views or cache for frequently accessed balances
- Queue journal posting for high-volume POS transactions
- Pagination for large ledger queries
