# ACC-SCHEMA-01 — MVP সব Accounting schema (এক bundle)

> এই একটাই task-এ MVP-র **সব `acc_*` table-এর schema** আছে।
> উদ্দেশ্য: schema আগে এক জায়গায় বসিয়ে অন্য দুইজন dev যাতে service/UI নিয়ে বসে না থাকে।
> Source: Task Specification Pack (Phase 1–5 schema)। Feature logic আলাদা card-এই থাকবে — এখানে শুধু **migration + model stub**।
> Estimate: প্রতি batch **৩ ঘণ্টা** (নিচে ৭টা sub-task)। Owner সাধারণত **Dev A (Core)**।
> **গুরুত্বপূর্ণ:** এই bundle-এ **কোনো DB foreign key constraint নেই** — শুধু column + index + logical relation।

Project
ERPFlow
Module
Accounting Module
Type
schema (migration bundle)
Title
MVP সব Accounting schema — এক bundle
Branch Name
`feature/accounting-mvp-schema-bundle`
View Order
0
Owner
Dev A (Core / Ledger)
Estimate Hours
21 (৭ × ৩)
Status
Todo

---

## Summary

MVP-র সব accounting table-এর migration একটা critical path task হিসেবে আগে বানাও। Table খালি/stub model থাকবে; business service, API, UI আগের মতোই নিজ নিজ phase card-এ। এতে P1-03/P1-09/P2-05 ইত্যাদিতে schema অপেক্ষা করে কেউ বসে থাকবে না — শুধু service লিখবে।

## Task serial

`ACC-SCHEMA-01 — MVP সব Accounting schema` · schema

## Depends on

- `ACC-P0-01` (Blueprint সই)
- `ACC-P0-02` (Convention Sheet — PK ধরন)
- `ACC-P0-03` (branch/CI নিয়ম)
- `ACC-P0-05` (DB guard design সিদ্ধান্ত — journal guard-এর জন্য; trigger implementation আলাদা `P1-10`)
- `ACC-P1-01` (module scaffold + flag — migration ফোল্ডার থাকতে হবে)

## Relation: পরের task

এই bundle Done হলে schema-র জন্য অপেক্ষা কমে:
- Phase 1 service/UI card (`P1-03` … `P1-12`) — table আগে থেকেই আছে
- Phase 2 approval/posting (`P2-05`, `P2-11`, …)
- Phase 3 report/period (`P3-02`, `P3-10`, `P3-11`, `P3-13`)
- Phase 4 intake (`P4-02` …)
- Phase 5 enablement/alert (`P5-03`, `P5-06`)

**নোট:** আগের phase card-এ যেখানে "Schema: …" লেখা আছে, সেগুলো **reference** হিসেবেই থাকবে। নতুন migration আর লিখবে না যদি এই bundle-এ ইতিমধ্যে table তৈরি হয়ে থাকে — শুধু model/service বাড়িয়ে নেবে। যদি column বাড়াতে হয়, আলাদা alter migration।

## সাধারণ convention (সব table-এ)

- Database: MySQL, InnoDB, `utf8mb4`
- Prefix: `acc_`
- PK: Convention Sheet অনুযায়ী (`P0-02`)
- প্রতি business table-এ `company_id NOT NULL` + composite index-এ `company_id` আগে
- টাকা: `DECIMAL(20,4)`
- Enum: `VARCHAR` + `CHECK`, MySQL ENUM নয়
- Optimistic lock: যেখানে লাগে `version`
- **Foreign key কঠোরভাবে define করা যাবে না।** মানে migration-এ `FOREIGN KEY` / `CONSTRAINT ... FOREIGN KEY` / `ON DELETE` **বসানো যাবে না।** `company_id`, `account_id`, `journal_id`, `user_id` ইত্যাদি শুধু column + index। সম্পর্ক application/Eloquent-এ যাচাই হবে।
- Column টেবিলে যেখানে `FK` লেখা, সেটার মানে শুধু **logical reference** (কোন id বোঝায়) — DB constraint নয়।
- Unique / index / CHECK যেগুলো data integrity-র জন্য দরকার (যেমন company+code unique, debit/credit check) — সেগুলো রাখা যাবে।
- Append-only table: পরে `P1-10`-এর `Schema::appendOnly()` helper; এই task-এ migration + model, trigger `P1-10`-এও চলতে পারে
- প্রতি table আলাদা migration (বা batch অনুযায়ী গ্রুপ — tech lead ঠিক করবে), `down()` নিরাপদ
- Flag (`accounting.enabled` ইত্যাদি) এখানে নয় — বিদ্যমান `settings` table (`P1-01`)

## বিদ্যমান (শুধু পড়া, নতুন নয়)

- `asset_finance_events` — Asset outbox; Accounting শুধু পড়ে
- `companies`, `users`, `activity_logs`, `settings`

## Table index (এই task-এ যা বানাবে)

| Batch | Tables | ~ঘণ্টা |
| --- | --- | --- |
| 1 | settings, settings_history, setting_change_requests, setup_status | 3 |
| 2 | accounts, periods | 3 |
| 3 | journals, journal_lines, journal_attachments, idempotency_keys | 3 |
| 4 | journal_status_history, approval_*, voucher_counters | 3 |
| 5 | period_balances, report_exports, period_events, reconciliation_runs | 3 |
| 6 | finance_event_*, intake_heartbeats, account_mappings | 3 |
| 7 | alert_state, enablement_log | 3 |

**মোট নতুন table:** 29টা

## Out of scope

- Service / API / UI / business rule implementation (নিজ নিজ `ACC-P*`-এ)
- DB guard trigger পূর্ণ বাস্তবায়ন (`P1-10`) — এখানে শুধু table
- Phase 6+ (journal type, dimension) ও Phase 8 tax schema
- Seed data / opening balance ভর্তি

## Definition of done

- উপরের সব table migration CI MySQL-এ চলে, `down()` নিরাপদ
- Eloquent model stub (fillable/casts/relation খালি বা ন্যূনতম) আছে
- Migration SQL/PHP-এ **কোনো `FOREIGN KEY` নেই** (grep করে প্রমাণ)
- কোনো feature flag Default `true` নয়
- অন্য module-এর table বদলায়নি
- Table list + ER নোট PR-এ আছে (logical relation আঁকা যাবে, DB FK নয়)
- পরের card-গুলোতে duplicate create migration নেই বলে লিখিত নোট

## Architecture constraints

- Posted journal line-ই ledger (OD-14 Option A ধরে) — আলাদা ledger projection table নেই
- `source_type` / `source_id` journal-এ থাকবে Phase 4-এর জন্য (এখন ফাঁকা থাকতে পারে)
- Migration apply হওয়ার পর সম্পাদনা নয় — ভুল হলে নতুন migration
- **কোনো table-এ DB-level foreign key নেই** — PR review-এ `FOREIGN KEY` থাকলে reject

---

## Schema বিস্তারিত (source থেকে)

নিচের column/index Task Spec Pack থেকে নেওয়া (FK অংশ logical করে রাখা)। Batch ক্রমে migration চালাও। **`FOREIGN KEY` লিখবে না।**


### Batch 1 — Config
**Sub-task:** `Batch 1 — Config` · Estimate Hours: **3** · Owner: Dev A

---

**Schema: `acc_settings`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `key` | VARCHAR(100) | না | Registry-র key |
| `value` | JSON | না | Typed মান |
| `version` | INT UNSIGNED | না | Default 1 |
| `created_at`, `updated_at` | TIMESTAMP | না |  |
| `updated_by` | ID users (logical) | হ্যাঁ |  |

Constraint ও index: `uq_acc_settings_company_key (company_id, key)`।

---

**Schema: `acc_settings_history`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `key` | VARCHAR(100) | না |  |
| `old_value` | JSON | হ্যাঁ | প্রথমবার হলে null |
| `new_value` | JSON | না |  |
| `changed_by` | ID users (logical) | না |  |
| `reason` | VARCHAR(500) | হ্যাঁ |  |
| `change_request_id` | ID (logical) | হ্যাঁ | P1-04 থেকে এলে |
| `created_at` | TIMESTAMP | না |  |

Index: `idx_acc_settings_history_company_key_time (company_id, key, created_at)`। এই table শুধু যোগ হয়, কখনো বদলায় না বা মোছা হয় না।

---

**Schema: `acc_setting_change_requests`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `key` | VARCHAR(100) | না |  |
| `old_value` | JSON | হ্যাঁ | অনুরোধের সময়ের মান |
| `proposed_value` | JSON | না |  |
| `base_version` | INT UNSIGNED | না | অনুরোধের সময় `acc_settings.version` |
| `reason` | VARCHAR(500) | না | বাধ্যতামূলক |
| `status` | VARCHAR(16) | না | `pending`, `approved`, `rejected`, `cancelled` |
| `requested_by` | ID users (logical) | না |  |
| `decided_by` | ID users (logical) | হ্যাঁ |  |
| `decided_at` | TIMESTAMP | হ্যাঁ |  |
| `decision_comment` | VARCHAR(500) | হ্যাঁ |  |
| `version` | INT UNSIGNED | না |  |
| `created_at`, `updated_at` | TIMESTAMP | না |  |

Index: `idx_acc_scr_company_status (company_id, status)`। Check: `decided_by` ও `decided_at` শুধু `status` ≠ `pending` হলে থাকবে।

---

**Schema: `acc_setup_status`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `step_code` | VARCHAR(40) | না | `S01_fiscal_year` ইত্যাদি (Section ২.৪) |
| `state` | VARCHAR(16) | না | `pending`, `done`, `not_applicable`, `skipped` |
| `detail` | JSON | হ্যাঁ | কী বাকি তার বিবরণ |
| `evaluated_at` | TIMESTAMP | না |  |
| `updated_by` | ID users (logical) | হ্যাঁ | `skipped` হলে কে |

Constraint: `uq_acc_setup_company_step (company_id, step_code)`।

### Batch 2 — CoA ও Period
**Sub-task:** `Batch 2 — CoA ও Period` · Estimate Hours: **3** · Owner: Dev A

---

**Schema: `acc_accounts`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `code` | VARCHAR(20) | না |  |
| `name` | VARCHAR(150) | না |  |
| `type` | VARCHAR(16) | না | `asset`, `liability`, `equity`, `revenue`, `expense` |
| `parent_id` | ID self (logical) | হ্যাঁ |  |
| `is_group` | BOOLEAN | না | Default false। `true` হলে line বসানো যাবে না |
| `is_active` | BOOLEAN | না | Default true |
| `is_system` | BOOLEAN | না | Default false |
| `system_key` | VARCHAR(40) | হ্যাঁ | `opening_balance_equity`, `retained_earnings` |
| `cashflow_class` | VARCHAR(16) | হ্যাঁ | `operating`, `investing`, `financing` |
| `is_cash_equivalent` | BOOLEAN | না | Default false |
| `description` | VARCHAR(500) | হ্যাঁ |  |
| `version` | INT UNSIGNED | না | Default 1 |
| `created_at`, `updated_at`, `created_by`, `updated_by` |  |  | সাধারণ নিয়ম |

Constraint ও index:

- `uq_acc_accounts_company_code (company_id, code)`
- `uq_acc_accounts_company_id (company_id, id)`। এটা অন্য table-এর composite unique-এর লক্ষ্য (logical join / app check — DB FK নয়)
- `uq_acc_accounts_company_syskey (company_id, system_key)`। `system_key` null হলে বহু row চলে (MySQL-এ null unique-এ গণ্য হয় না)
- logical ref `(company_id, parent_id)` (index only, DB FK নেই)। এতে অন্য company-র account parent হতে পারে না, DB নিজেই আটকায়
- `ck_acc_accounts_type`: type পাঁচটার একটা। `ck_acc_accounts_cf`: `cashflow_class` null বা তিনটার একটা
- `idx_acc_accounts_company_parent (company_id, parent_id)`, `idx_acc_accounts_company_type_active (company_id, type, is_active)`

**Logical relation নিয়ম (DB FK নেই):** যে table `account_id` বা `journal_id` রাখে, সেখানে index রাখো `(company_id, account_id)` / `(company_id, journal_id)` আর application-এ যাচাই করো যে account/journal একই company-র। **`FOREIGN KEY` বসিও না।**

---

**Schema: `acc_periods`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `fiscal_year` | SMALLINT UNSIGNED | না | শুরুর বছর |
| `period_no` | TINYINT UNSIGNED | না | 1 থেকে 12 |
| `period_key` | VARCHAR(10) | না | `YYYY-MM` (সেই মাসের শুরুর তারিখের) |
| `start_date`, `end_date` | DATE | না |  |
| `status` | VARCHAR(12) | না | Default `open`, `soft_closed`, `closed` |
| `closed_at`, `closed_by` |  | হ্যাঁ | Phase 3-এ ব্যবহার |
| `reopened_at`, `reopened_by` |  | হ্যাঁ | Phase 3-এ ব্যবহার |
| `version` | INT UNSIGNED | না |  |
| `created_at`, `updated_at` |  | না |  |

Constraint ও index: `uq_acc_periods_company_key (company_id, period_key)`, `uq_acc_periods_company_year_no (company_id, fiscal_year, period_no)`, `uq_acc_periods_company_id (company_id, id)` (journals-এর logical ref / index (DB FK নেই)-র জন্য), `idx_acc_periods_company_dates (company_id, start_date, end_date)`, `ck_acc_periods_dates`: `end_date >= start_date`, `ck_acc_periods_status`।

### Batch 3 — Journal ভিত্তি
**Sub-task:** `Batch 3 — Journal ভিত্তি` · Estimate Hours: **3** · Owner: Dev A

---

**Schema: `acc_journals`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `journal_type` | VARCHAR(20) | না | `manual`, `opening`, `reversal`, `adjustment`, `event_generated` |
| `status` | VARCHAR(16) | না | Default `draft`। `draft`, `submitted`, `approved`, `rejected`, `posted`, `cancelled`, `reversed` |
| `voucher_no` | VARCHAR(40) | হ্যাঁ | Post-এর সময় বসে (OD-8) |
| `journal_date` | DATE | না |  |
| `period_id` | ID (logical) | হ্যাঁ | Post-এর সময় বসে |
| `narration` | VARCHAR(1000) | হ্যাঁ |  |
| `reference_no` | VARCHAR(100) | হ্যাঁ |  |
| `currency` | CHAR(3) | না | তৈরির সময়ের base currency |
| `total_debit`, `total_credit` | DECIMAL(20,4) | না | Default 0, service রক্ষণাবেক্ষণ করে |
| `source_type`, `source_id`, `source_event` | VARCHAR | হ্যাঁ | Phase 4-এর জন্য, system-তৈরি journal-এ |
| `reverses_journal_id` | ID self (logical) | হ্যাঁ |  |
| `adjusts_journal_id` | ID self (logical) | হ্যাঁ |  |
| `reversal_reason` | VARCHAR(500) | হ্যাঁ |  |
| `submission_no` | INT UNSIGNED | না | Default 0 |
| `submitted_at`, `submitted_by`, `approved_at`, `posted_at`, `posted_by`, `cancelled_at` |  | হ্যাঁ |  |
| `reversed_at`, `reversed_by_journal_id` |  | হ্যাঁ | মূল journal-এ reversal post হলে |
| `opening_active` | TINYINT (generated, stored) | হ্যাঁ | `journal_type = 'opening'` আর status `draft`, `submitted`, `approved`, `posted`-এর একটা হলে 1, নইলে null |
| `version` | INT UNSIGNED | না |  |
| `created_by`, `updated_by`, `created_at`, `updated_at`, `deleted_at` |  |  |  |

Constraint ও index:

- `uq_acc_journals_company_id (company_id, id)`
- `uq_acc_journals_company_voucher (company_id, voucher_no)`, null অনেক চলে
- `uq_acc_journals_source (company_id, source_type, source_id, source_event)`। এটা Phase 4-এর idempotency-র DB স্তরের ভিত্তি (R4.1)। সংশোধন (P4-06 থেকে): বাতিল (\`cancelled\`) journal বাদ দিয়ে unique হতে হবে (R4.10), তাই P1-09-এ constraint সরাসরি তিন column-এ নয়। একটা generated stored column \`source\_active\` (\`CASE WHEN source\_type IS NOT NULL AND status <> 'cancelled' THEN CONCAT(source\_type, ':', source\_id, ':', source\_event) END\`), আর তার ওপর \`uq\_acc\_journals\_source\_active (company\_id, source\_active)\`। এতে Event-এর Draft বাতিল হলে একই event থেকে নতুন Draft বানানো যায়, কিন্তু একসাথে দুটো সক্রিয় নয়
- `uq_acc_journals_opening (company_id, opening_active)`। এটা R1.11-কে DB স্তরে প্রয়োগ করে
- index `(company_id, reverses_journal_id)`, `(company_id, adjusts_journal_id)` — logical self-ref, DB FK নেই
- index `(company_id, period_id)` — logical period ref, DB FK নেই
- `ck_acc_journals_status`, `ck_acc_journals_type`
- `ck_acc_journals_posted`: `status NOT IN ('posted','reversed')` অথবা (`voucher_no IS NOT NULL` এবং `posted_at IS NOT NULL` এবং `period_id IS NOT NULL`)
- `ck_acc_journals_reversal`: `reverses_journal_id IS NULL` অথবা `journal_type = 'reversal'`। অনুরূপ `adjusts`-এর জন্য
- `ck_acc_journals_balanced_when_posted`: `status NOT IN ('posted','reversed')` অথবা `total_debit = total_credit`
- `idx_acc_journals_company_status_date (company_id, status, journal_date)`, `idx_acc_journals_company_ref (company_id, reference_no)`, `idx_acc_journals_company_type_status (company_id, journal_type, status)`

---

**Schema: `acc_journal_lines`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `journal_id` | ID (logical) | না |  |
| `line_no` | SMALLINT UNSIGNED | না |  |
| `account_id` | ID (logical) | না |  |
| `debit`, `credit` | DECIMAL(20,4) | না | Default 0 |
| `line_narration` | VARCHAR(500) | হ্যাঁ |  |
| `cost_center_id` | ID Configuration (logical) | হ্যাঁ |  |
| `created_at`, `updated_at` |  | না |  |

Constraint ও index: `uq_acc_jl_journal_line (journal_id, line_no)`, index `(company_id, journal_id)`, `(company_id, account_id)` — logical ref, DB FK নেই; `ck_acc_jl_amounts`: `debit >= 0 AND credit >= 0 AND ((debit > 0 AND credit = 0) OR (credit > 0 AND debit = 0))`, `idx_acc_jl_company_account (company_id, account_id, journal_id)`, `idx_acc_jl_journal (journal_id)`।

---

**Schema: `acc_journal_attachments`:** `id`, `company_id`, `journal_id`, `storage_path`, `original_name`, `mime_type`, `size_bytes`, `uploaded_by`, `created_at`। Index `(company_id, journal_id)` — logical journal ref, DB FK নেই। Index `(company_id, journal_id)`।

---

**Schema: `acc_idempotency_keys`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `idem_key` | VARCHAR(100) | না | Client-এর পাঠানো key |
| `endpoint` | VARCHAR(120) | না | Method ও route |
| `request_hash` | CHAR(64) | না | Request body-র hash |
| `state` | VARCHAR(12) | না | `processing`, `done` |
| `response_status` | SMALLINT | হ্যাঁ |  |
| `response_body` | JSON | হ্যাঁ |  |
| `created_by` | ID users (logical) | না |  |
| `created_at`, `expires_at` | TIMESTAMP | না |  |

Constraint: `uq_acc_idem (company_id, idem_key, endpoint)`। Index: `idx_acc_idem_expires (expires_at)`।

### Batch 4 — Approval ও voucher
**Sub-task:** `Batch 4 — Approval ও voucher` · Estimate Hours: **3** · Owner: Dev A

---

**Schema: `acc_journal_status_history`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `journal_id` | ID (logical) | না |  |
| `from_status` | VARCHAR(16) | হ্যাঁ | তৈরির সময় null |
| `to_status` | VARCHAR(16) | না |  |
| `action` | VARCHAR(20) | না | `create`, `submit`, `approve`, `reject`, `withdraw`, `return`, `post`, `cancel`, `mark_reversed` |
| `actor_id` | ID users (logical) | হ্যাঁ | system হলে null |
| `submission_no` | INT UNSIGNED | না |  |
| `comment` | VARCHAR(500) | হ্যাঁ |  |
| `meta` | JSON | হ্যাঁ |  |
| `created_at` | TIMESTAMP | না |  |

Constraint ও index: index/logical ref `(company_id, journal_id)` (DB FK নেই), `idx_acc_jsh_company_journal_time (company_id, journal_id, created_at)`। Append-only (P1-10-এর helper)।

---

**Schema: `acc_approval_rules`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `name` | VARCHAR(120) | না |  |
| `journal_type` | VARCHAR(20) | না | `manual`, `opening`, `reversal`, `adjustment`, `event_generated`, বা `any` |
| `min_amount` | DECIMAL(20,4) | না | Default 0, অন্তর্ভুক্ত |
| `max_amount` | DECIMAL(20,4) | হ্যাঁ | অন্তর্ভুক্ত নয় (`amount < max_amount`), null মানে সীমাহীন |
| `mode` | VARCHAR(12) | না | `sequential` বা `parallel` |
| `self_approval` | VARCHAR(8) | না | `inherit` (default), `allow`, `deny` |
| `is_active` | BOOLEAN | না |  |
| `effective_from`, `effective_to` | DATE | হ্যাঁ |  |
| `version` | INT UNSIGNED | না |  |
| সাধারণ column |  |  |  |

Check: `ck_acc_ar_range`: `max_amount IS NULL OR max_amount > min_amount`; `ck_acc_ar_mode`, `ck_acc_ar_type`, `ck_acc_ar_self`। Index: `idx_acc_ar_lookup (company_id, is_active, journal_type, min_amount)`, `uq_acc_ar_company_id (company_id, id)`।

---

**Schema: `acc_approval_rule_levels`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `rule_id` | ID (logical) | না | Index/logical ref `(company_id, rule_id)` (DB FK নেই) |
| `level_no` | SMALLINT UNSIGNED | না | 1 থেকে শুরু |
| `approver_role_id` | ID RBAC role (logical) | হ্যাঁ |  |
| `approver_user_id` | ID users (logical) | হ্যাঁ |  |
| `condition` | JSON | হ্যাঁ | `{ "account_ids": [...], "cost_center_ids": [...] }`, null মানে সবসময় প্রযোজ্য |

Check: `ck_acc_arl_approver`: `approver_role_id` আর `approver_user_id`-র ঠিক একটা null নয়। Unique: `uq_acc_arl_rule_level (rule_id, level_no)`।

---

**Schema: `acc_approval_requests`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `journal_id` | ID (logical) | না | Logical ref / index (DB FK নেই) |
| `submission_no` | INT UNSIGNED | না |  |
| `rule_id` | ID (logical) | না |  |
| `rule_snapshot` | JSON | না | Submit-এর মুহূর্তের rule ও effective levels, যাতে পরে rule বদলালেও চলমান request প্রভাবিত না হয় (Blueprint ৭.৮) |
| `mode` | VARCHAR(12) | না |  |
| `amount` | DECIMAL(20,4) | না | Journal-এর `total_debit` |
| `status` | VARCHAR(12) | না | `pending`, `approved`, `rejected`, `cancelled`, `returned` |
| `current_level` | SMALLINT UNSIGNED | হ্যাঁ | Sequential হলে |
| `total_levels` | SMALLINT UNSIGNED | না |  |
| `requested_by` | ID users (logical) | না |  |
| `decided_at` | TIMESTAMP | হ্যাঁ |  |
| `version` | INT UNSIGNED | না |  |
| `created_at`, `updated_at` |  | না |  |

Unique: `uq_acc_areq_journal_sub (company_id, journal_id, submission_no)`। Index: `idx_acc_areq_company_status (company_id, status)`।

---

**Schema: `acc_approval_actions`** (append-only)

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `request_id` | ID (logical) | না |  |
| `level_no` | SMALLINT UNSIGNED | না |  |
| `actor_id` | ID users (logical) | না |  |
| `acted_for_id` | ID users (logical) | হ্যাঁ | Delegation হলে মূল approver |
| `decision` | VARCHAR(10) | না | `approve`, `reject`, `return`, `override` |
| `comment` | VARCHAR(500) | হ্যাঁ | Reject, return, override-এ বাধ্যতামূলক |
| `created_at` | TIMESTAMP | না |  |

Unique: `uq_acc_aact_request_level (request_id, level_no)`। এটা এক ধাপে দুইবার সিদ্ধান্ত DB স্তরেই আটকায়।

---

**Schema: `acc_approval_delegations`:** `id`, `company_id`, `delegator_id`, `delegate_id`, `valid_from`, `valid_to`, `is_active`, `created_by`, `created_at`। Check: `delegator_id <> delegate_id`, `valid_to >= valid_from`। Index: `idx_acc_adel_lookup (company_id, delegate_id, is_active, valid_from, valid_to)`।

---

**Schema যোগ: `acc_approval_override_reviews`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `action_id` | ID (logical) | না | `acc_approval_actions`-এর override row |
| `reviewed_by` | ID users (logical) | না |  |
| `outcome` | VARCHAR(12) | না | `accepted`, `flagged` |
| `comment` | VARCHAR(500) | না |  |
| `created_at` | TIMESTAMP | না |  |

Unique: `uq_acc_aor_action (action_id)`। Append-only।

---

**Schema: `acc_voucher_counters`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `fiscal_year` | SMALLINT UNSIGNED | না |  |
| `series` | VARCHAR(20) | না | Default `JV`। ভবিষ্যতে ধরন ধরে আলাদা series-এর জন্য |
| `last_no` | INT UNSIGNED | না | Default 0 |
| `updated_at` | TIMESTAMP | না |  |

Unique: `uq_acc_vc (company_id, fiscal_year, series)`।

### Batch 5 — Ledger/Report/Period control
**Sub-task:** `Batch 5 — Ledger/Report/Period control` · Estimate Hours: **3** · Owner: Dev A

---

**Schema: `acc_period_balances`** (period ধরে নড়াচড়া, জমা নয়)

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `period_id` | ID (logical) | না | Logical ref / index (DB FK নেই) |
| `account_id` | ID (logical) | না | Logical ref / index (DB FK নেই) |
| `debit_total`, `credit_total` | DECIMAL(20,4) | না | ওই period-এর Debit ও Credit-এর যোগফল |
| `line_count` | INT UNSIGNED | না |  |
| `period_version` | BIGINT UNSIGNED | না | হিসাবের সময়ের `acc_periods.balance_version` |
| `computed_at` | TIMESTAMP | না |  |

Unique: `uq_acc_pb (company_id, period_id, account_id)`। Index: `idx_acc_pb_period (company_id, period_id)`।

**Schema যোগ (expand migration):** `acc_periods.balance_version BIGINT UNSIGNED NOT NULL DEFAULT 0`।

---

**Schema: `acc_report_exports`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `report` | VARCHAR(40) | না | `general-ledger`, `trial-balance` ইত্যাদি |
| `filters` | JSON | না |  |
| `format` | VARCHAR(8) | না | `xlsx`, `pdf` |
| `request_hash` | CHAR(64) | না | `report` ও `filters` ও `format`-এর hash |
| `status` | VARCHAR(12) | না | `queued`, `processing`, `done`, `failed` |
| `active_key` | CHAR(64) (generated, stored) | হ্যাঁ | `status` `queued` বা `processing` হলে `request_hash`, নইলে null |
| `file_path` | VARCHAR(255) | হ্যাঁ |  |
| `row_count` | INT UNSIGNED | হ্যাঁ |  |
| `requested_by` | ID users (logical) | না |  |
| `requested_at`, `started_at`, `finished_at` | TIMESTAMP |  |  |
| `error` | VARCHAR(500) | হ্যাঁ |  |
| `expires_at` | TIMESTAMP | না |  |

Unique: `uq_acc_exp_active (company_id, requested_by, active_key)`। এতে একই ব্যক্তির একই report-এর দুটো চলমান export DB স্তরে আটকায় (দ্বিতীয় অনুরোধ আগেরটাই ফেরত পায়)। Index: `idx_acc_exp_company_time (company_id, requested_at)`।

---

**Schema: `acc_period_events`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `period_id` | ID (logical) | না | Logical ref / index (DB FK নেই) |
| `kind` | VARCHAR(8) | না | `close`, `reopen`, `soft` |
| `status` | VARCHAR(10) | না | `pending`, `approved`, `rejected`, `cancelled` |
| `reason` | VARCHAR(500) | হ্যাঁ | Reopen-এ বাধ্যতামূলক |
| `checks` | JSON | হ্যাঁ | বন্ধের যাচাইয়ের ফল |
| `requested_by` | ID users (logical) | না |  |
| `decided_by` | ID users (logical) | হ্যাঁ |  |
| `decision_comment` | VARCHAR(500) | হ্যাঁ |  |
| `requested_at`, `decided_at` | TIMESTAMP |  |  |

Index: `idx_acc_pe_company_period (company_id, period_id, requested_at)`। Unique: একটা period-এ একটাই `pending` অনুরোধ, `pending_key` generated column (`status = 'pending'` হলে `period_id`)-এ `uq_acc_pe_pending (company_id, pending_key)`। Trigger: `OLD.status <> 'pending'` হলে row আর বদলানো বা মোছা যায় না (সিদ্ধান্ত হয়ে গেলে অপরিবর্তনীয়)।

---

**Schema: `acc_reconciliation_runs`** (append-only)

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `check_code` | VARCHAR(8) | না | `RC1` থেকে `RC9` |
| `scope` | VARCHAR(10) | না | `as_of`, `period` |
| `as_of_date` | DATE | না |  |
| `period_id` | ID (logical) | হ্যাঁ |  |
| `result` | VARCHAR(8) | না | `pass`, `fail`, `error`, `skipped` |
| `expected`, `actual`, `difference` | DECIMAL(20,4) | হ্যাঁ |  |
| `detail` | JSON | হ্যাঁ | অমিলের account বা journal-এর তালিকা |
| `triggered_by` | VARCHAR(10) | না | `manual`, `close`, `schedule` |
| `run_by` | ID users (logical) | হ্যাঁ | schedule হলে null |
| `started_at`, `finished_at` | TIMESTAMP |  |  |

Index: `idx_acc_rr_company_check_time (company_id, check_code, started_at)`।

| Check | কী মেলায় | কীভাবে (স্বাধীন পথ) |
| --- | --- | --- |
| RC1 | Trial Balance-এ মোট Debit = মোট Credit | TB service-এর ফল |
| RC2 | প্রতিটা account-এর GL-এর closing = TB-র closing | দুই service-এর ফল তুলনা |
| RC3 | Balance Sheet: Asset = Liability + Equity | BS service-এর ফল |
| RC4 | Cash Flow-এর শেষের নগদ = Balance Sheet-এর নগদ-সমতুল্য account-এর যোগফল, আর তিন ভাগের মোট = নিট পরিবর্তন | CF ও BS service |
| RC5 | P&L-এর নিট লাভ = TB-র Revenue ও Expense account-এর পার্থক্য | P&L ও TB service |
| RC6 | Report-এর মোট = posted journal line-এর যোগফল | **সরাসরি raw SQL**, `LedgerQueryService` পার না হয়ে, যাতে service-এর ভুল ধরা পড়ে। দুটো আলাদা query path |
| RC7 | Derived cache = মূল line-এর যোগফল (শুধু বৈধ row) | cache ও raw SQL |
| RC8 | Event থেকে ledger পর্যন্ত মিল (Phase 4) | P4-10 |
| RC9 | `posted_hash` মিল: Posted journal ও line থেকে hash আবার হিসাব করে সংরক্ষিতটার সাথে মেলে | batch করে, সবসময় সব নয় (নিচে) |

### Batch 6 — Finance event intake
**Sub-task:** `Batch 6 — Finance event intake` · Estimate Hours: **3** · Owner: Dev A

---

**Schema: `acc_finance_event_inbox`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `source_type` | VARCHAR(40) | না | যেমন `asset_finance_event` |
| `source_id` | VARCHAR(64) | না | মূল record-এর id, string যাতে যেকোনো PK ধরন চলে |
| `event_type` | VARCHAR(40) | না | যেমন `write_off` |
| `schema_version` | SMALLINT UNSIGNED | না | Default 1 |
| `effective_date` | DATE | হ্যাঁ | অবৈধ payload ধরার জন্য null চলে |
| `amount` | DECIMAL(20,4) | হ্যাঁ |  |
| `currency` | CHAR(3) | হ্যাঁ |  |
| `payload` | JSON | না | মূল তথ্য হুবহু |
| `payload_hash` | CHAR(64) | না | `payload`-এর canonical SHA-256 |
| `conflict_payload` | JSON | হ্যাঁ | একই identity ভিন্ন payload এলে |
| `status` | VARCHAR(16) | না | Default `received` |
| `error_code` | VARCHAR(40) | হ্যাঁ |  |
| `error_message` | VARCHAR(500) | হ্যাঁ |  |
| `attempts` | SMALLINT UNSIGNED | না | Default 0 |
| `next_retry_at` | TIMESTAMP | হ্যাঁ |  |
| `received_at`, `processed_at` | TIMESTAMP |  |  |
| `version` | INT UNSIGNED | না |  |
| `created_at`, `updated_at` |  | না |  |

Constraint ও index:

- `uq_acc_fei_identity (company_id, source_type, source_id, event_type)`। এটাই R4.1, DB স্তরে
- `uq_acc_fei_company_id (company_id, id)`
- `ck_acc_fei_status`: `received`, `draft_created`, `held_mapping`, `held_currency`, `conflict`, `failed`, `ignored`
- `idx_acc_fei_claim (company_id, status, next_retry_at)`, `idx_acc_fei_received (company_id, received_at)`

**অবস্থা বদলের নিয়ম** (একটাই service দিয়ে, `InboxTransitionService`, Journal state machine-এর মতো):

| থেকে | যায় | কে | কখন |
| --- | --- | --- | --- |
| (নতুন) | `received` | গ্রহণ | event প্রথমবার এল |
| `received` | `draft_created` | Consumer | Draft Journal তৈরি হলো |
| `received` | `held_mapping`, `held_currency`, `failed` | Consumer | নিয়ম অনুযায়ী (P4-03) |
| `received`, `draft_created`, `held_*` | `conflict` | গ্রহণ | একই identity ভিন্ন payload |
| `held_mapping`, `held_currency`, `failed`, `ignored` | `received` | মানুষ (release বা retry, P4-09) | সমস্যা ঠিক করার পর |
| `held_mapping`, `held_currency`, `failed` | `ignored` | মানুষ | কারণ সহ |
| `conflict` | `received` | মানুষ | নতুন payload গ্রহণ করে (পুরোটা আবার প্রক্রিয়া) |
| `conflict` | `ignored` | মানুষ | নতুন payload প্রত্যাখ্যান |
| `draft_created` | `received` | System | লিঙ্কযুক্ত Draft journal `cancelled` হলে (R4.10) |

অন্য জোড়া নতুন code `EVT_STATE_INVALID` (409)।

---

**Schema: `acc_intake_heartbeats`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `company_id` | PK, FK | না |  |
| `last_started_at`, `last_finished_at` | TIMESTAMP | হ্যাঁ |  |
| `last_error` | VARCHAR(500) | হ্যাঁ |  |
| `processed_count`, `held_count`, `failed_count` | INT UNSIGNED | না | সর্বশেষ run-এর |

---

**Schema: `acc_account_mappings`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `source_type` | VARCHAR(40) | না |  |
| `event_type` | VARCHAR(40) | না |  |
| `mapping_key` | VARCHAR(40) | না | `debit_account`, `credit_account` |
| `account_id` | ID (logical) | না | Index/logical ref `(company_id, account_id)` (DB FK নেই) |
| `is_active` | BOOLEAN | না |  |
| `version` | INT UNSIGNED | না |  |
| `created_by`, `updated_by`, `created_at`, `updated_at` |  |  |  |

Unique: `uq_acc_am (company_id, source_type, event_type, mapping_key)`। Index: `idx_acc_am_account (company_id, account_id)`।

---

**Schema: `acc_finance_event_journal_links`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `inbox_id` | ID (logical) | না | Index/logical ref `(company_id, inbox_id)` (DB FK নেই) |
| `journal_id` | ID (logical) | না | Index/logical ref `(company_id, journal_id)` (DB FK নেই) |
| `is_live` | BOOLEAN | না | Default true। Draft বাতিল হলে false |
| `live_key` | BIGINT (generated, stored) | হ্যাঁ | `CASE WHEN is_live = 1 THEN inbox_id END` |
| `mapping_snapshot` | JSON | না | যে mapping থেকে বানানো: প্রতিটা key-র `account_id`, mapping row-এর `version` ও `updated_by`, সময় |
| `created_at` | TIMESTAMP | না |  |

Unique: `uq_acc_fejl_live (company_id, live_key)` (একটা event-এর একটাই সক্রিয় journal), `uq_acc_fejl_journal (company_id, journal_id)` (একটা journal একটাই event-এর)।

**Write-off-এর লাইন-নিয়ম** (OD-10 বন্ধ হলে):

| Line | Account | Debit | Credit | Dimension |
| --- | --- | --- | --- | --- |
| 1 | Mapping-এর `debit_account` | `amount` |  | event-এর cost center |
| 2 | Mapping-এর `credit_account` |  | `amount` | event-এর cost center |

---

**Schema: `acc_finance_event_actions`** (append-only)

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `inbox_id` | ID (logical) | না | Logical ref / index (DB FK নেই) |
| `action` | VARCHAR(16) | না | `retry`, `release`, `ignore`, `accept_conflict`, `reject_conflict` |
| `from_status`, `to_status` | VARCHAR(16) | না |  |
| `actor_id` | ID users (logical) | না |  |
| `reason` | VARCHAR(500) | হ্যাঁ | `ignore`, `accept_conflict`, `reject_conflict`-এ বাধ্যতামূলক |
| `created_at` | TIMESTAMP | না |  |

Index: `idx_acc_fea_inbox (company_id, inbox_id, created_at)`।

### Batch 7 — Release/ops
**Sub-task:** `Batch 7 — Release/ops` · Estimate Hours: **3** · Owner: Dev A

---

**Schema: `acc_alert_state`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `alert_key` | VARCHAR(60) | না | নিচের তালিকার কোড |
| `state` | VARCHAR(8) | না | `ok`, `firing` |
| `first_fired_at`, `last_fired_at`, `last_notified_at` | TIMESTAMP | হ্যাঁ |  |
| `detail` | JSON | হ্যাঁ |  |

Unique: `uq_acc_alert (company_id, alert_key)`।

| Alert | কখন | সীমা (setting) | উৎস |
| --- | --- | --- | --- |
| `recon_failed` | RC1 থেকে RC9-এর কোনোটা `fail` | যেকোনো একটা, সঙ্গে সঙ্গে | `acc_reconciliation_runs` |
| `consumer_lag` | Consumer শেষ চলার সময় পুরোনো | `monitor_consumer_lag_minutes` | `acc_intake_heartbeats` |
| `held_events` | আটকে থাকা event-এর সংখ্যা বা বয়স | `monitor_held_event_count`, `monitor_held_event_age_hours` | `acc_finance_event_inbox` |
| `failed_events` | যেকোনো `failed` বা `conflict` | যেকোনো একটা | inbox |
| `ack_failing` | Asset-এ ফেরত জানানো `ack_attempts` শেষেও ব্যর্থ | যেকোনো একটা | inbox |
| `approval_stuck` | `pending` approval দীর্ঘ সময় | `monitor_approval_stuck_days` | `acc_approval_requests` |
| `post_failures` | Post ব্যর্থ হওয়ার audit event | যেকোনো একটা (পরপর তিনটা হলে গুরুতর) | `activity_logs` |
| `locked_post_attempts` | বন্ধ period-এ Post-এর চেষ্টা | `monitor_closed_period_post_attempts` (দিনে) | `activity_logs` |
| `override_unreviewed` | Override হয়েছে কিন্তু পর্যালোচনা নেই | ২৪ ঘণ্টার বেশি | `acc_approval_actions` |
| `export_failures` | Export `failed` | যেকোনো একটা | `acc_report_exports` |

---

**Schema: `acc_enablement_log`** (append-only)

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | ID (logical) | না |  |
| `flag` | VARCHAR(60) | না | `accounting.enabled`, `event_intake.enabled`, `event_intake.sources.asset_write_off`, অথবা গেট: `gate:uat`, `gate:cutover`, `gate:security` |
| `action` | VARCHAR(12) | না | `on`, `off`, `gate_passed`, `gate_revoked` |
| `requested_by` | ID users (logical) | না |  |
| `approved_by` | ID users (logical) | হ্যাঁ |  |
| `reason` | VARCHAR(500) | না |  |
| `gate_snapshot` | JSON | হ্যাঁ | চালুর মুহূর্তের Checklist আর গেটের অবস্থা |
| `created_at` | TIMESTAMP | না |  |

Index: `idx_acc_enl_company_flag_time (company_id, flag, created_at)`।


---

## Migration চালানোর ক্রম (সংক্ষেপ)

1. Batch 1 → config table
2. Batch 2 → CoA + period (`acc_accounts` parent self-FK সাবধানে)
3. Batch 3 → journal (generated column / unique opening constraint Pack অনুযায়ী)
4. Batch 4 → approval + voucher
5. Batch 5 → report/period derived
6. Batch 6 → intake (P4 flag বন্ধ থাকলেও table থাকতে পারে)
7. Batch 7 → alert + enablement log

DB FK নেই বলে migration order শিথিল — তবুও পড়তে সুবিধা হয় বলে একই ক্রম রাখো। Logical ref: `journal_lines.account_id` → accounts; approval → journals; event links → inbox + journals — application-এ যাচাই।

## Board field

| Field | মান |
| --- | --- |
| Wave | `schema-0` (Phase 1 feature-এর আগে বা সাথে সাথে P1-01-এর পর) |
| Lane | Core / Dev A |
| Blocked by | P0-01, P0-02, P0-03, P1-01 (P0-05 journal guard সিদ্ধান্ত) |
| Unblocks | প্রায় সব Phase 1–5 service/UI card-এর schema অংশ |

