# Accounting Module — Task Specification Pack (DB Schema ও DB Flow সহ)

Oct 13, 2026 · @Riajul Hasan

## ১. কীভাবে পড়বেন

এই doc Accounting Module Blueprint থেকে বানানো। প্রতিটা **task card** একটা কাজের সম্পূর্ণ specification: কী বানাতে হবে, কোন table ও column, DB-তে কী পড়া ও লেখা হবে, কোন lock, কোন নিয়ম, কী দিয়ে প্রমাণ হবে। Card গুলো **build sequence** অনুযায়ী সাজানো, যাতে একটা card শেষ করতে যা লাগে তা আগের card-এ হয়ে থাকে। পরে প্রতিটা card এক বা একাধিক WBS task-এ ভাঙা যাবে।

### ১.১ Card-এর ছাঁচ

প্রতিটা card-এ এই field গুলো আছে (যেটা প্রযোজ্য নয় সেটা বাদ):

| Field | মানে |
| --- | --- |
| ID | `P{phase}-{ক্রম}`, যেমন `P2-07`। নম্বর কখনো বদলায় না, card মুছলে নম্বর খালি থাকে |
| ধরন | `schema` (migration), `service`, `api`, `ui`, `job`, `test`, `ops`, `gov` (সিদ্ধান্ত বা অনুমোদন) |
| নির্ভর করে | যে card-গুলো `Done` না হলে এটা শুরু করা যাবে না |
| অবরুদ্ধ করে | এটা শেষ না হলে যে card শুরু হবে না |
| OD | যে Open Decision বন্ধ থাকতে হবে |
| উদ্দেশ্য | এক বা দুই লাইনে |
| Schema | table, column, ধরন, constraint, index |
| DB flow | কোন table থেকে পড়া, কোনটায় লেখা, কোন lock, কোন transaction সীমা, কী ক্রমে |
| নিয়ম | Business rule (Blueprint-এর R-নম্বর সহ) |
| Contract | API endpoint, service method, বা job-এর input ও output |
| Permission | কোন slug দরকার |
| Audit | কোন event log হবে |
| Error | কোন error code |
| Acceptance | কোন AC প্রমাণ করে |
| Test | কোন test case লাগবে |
| Deliverable | কী জমা হবে |
| Sub-tasks | WBS-এ ভাঙার জন্য ইঙ্গিত |

### ১.২ Card শুরু ও শেষের নিয়ম

- **শুরু:** `নির্ভর করে`-এর সব card `Done`, আর `OD`-এর সবকটা লিখিতভাবে বন্ধ।
- **Done:** code review শেষ, Acceptance-এর test সবুজ, golden suite সবুজ (Blueprint ১২.২), বিদ্যমান ERPFlow regression সবুজ, audit event আছে, error code আর বার্তা আছে, flag বন্ধ রেখে merge নিরাপদ।
- **সমান্তরাল:** যে card-গুলোর `নির্ভর করে` একই, সেগুলো একসাথে চলতে পারে। Master sequence-এ (Section ১০) এগুলো চিহ্নিত।
- **অবস্থা:** `Todo`, `Blocked` (OD বা নির্ভরতা বাকি), `In progress`, `In review`, `Done`। এখানে সবগুলো `Todo` বা `Blocked`। কাজের পরিমাণ, owner আর sprint ইচ্ছা করে ফাঁকা।

### ১.৩ Schema-র সাধারণ convention

এগুলো সব table-এ খাটে। প্রতিটা card-এ আর পুনরাবৃত্তি করা হয়নি।

| বিষয় | নিয়ম |
| --- | --- |
| Database | MySQL (ERPFlow-এর CI-তে `backend-mysql-concurrency` আছে), InnoDB, `utf8mb4` |
| Table prefix | `acc_`। Blueprint-এ লেখা `accounts`, `journals` ইত্যাদি generic নাম ERPFlow-এর অন্য table-এর সাথে সংঘাত করতে পারে, তাই এখানে প্রতিটার নাম prefix সহ। মিল Section ২-এ। |
| Primary key | `id`। ধরন ERPFlow-এর বিদ্যমান convention অনুযায়ী (bigint unsigned বা ULID)। এটা **অনুমান**, P0-02-এ repo দেখে নিশ্চিত হবে। নিচের সব `*_id` foreign key একই ধরনের |
| Company | প্রতিটা business table-এ `company_id NOT NULL`, `companies(id)`-এর FK, আর প্রতিটা composite index-এ `company_id` সবার আগে |
| টাকার অঙ্ক | `DECIMAL(20,4)`। অঙ্ক কত দশমিক পর্যন্ত চলবে তা `currency_precision` setting ঠিক করে, application সেটা জোর করে। Debit ও Credit-এ `CHECK (>= 0)` |
| Enum | `VARCHAR(32)` আর `CHECK (col IN (...))`। MySQL enum type ব্যবহার হবে না, যাতে মান যোগ করা migration সহজ থাকে |
| Concurrency | বদলানো যায় এমন row-এ `version INT UNSIGNED NOT NULL DEFAULT 1`, প্রতিটা update-এ `WHERE version = ?` আর `version = version + 1` |
| সময় ও ব্যক্তি | `created_at`, `updated_at`, `created_by`, `updated_by` (nullable, users-এর FK) |
| মোছা | শুধু Draft ধরনের row-এ `deleted_at` (soft delete)। Posted বা লেজারের row কখনো মোছা হয় না |
| নামকরণ | Index `idx_<table>_<columns>`, unique `uq_<table>_<columns>`, FK `fk_<table>_<column>`, check `ck_<table>_<rule>` |
| Migration | প্রতি table-এ আলাদা migration, প্রয়োগ হওয়া migration কখনো সম্পাদনা নয়, সব migration পিছনে সামঞ্জস্যপূর্ণ (আগে যোগ, পরে বদল, শেষে মোছা), `down()` নিরাপদ হতে হবে। কারণ migration ব্যর্থ হলে rollback হয় না |
| Transaction | নিয়ম বদলাতে না চাইলে default isolation। যেখানে row lock লাগে সেখানে `SELECT ... FOR UPDATE` (card-এ লেখা) |
| Foreign key | `ON DELETE RESTRICT` ডিফল্ট। `CASCADE` কখনো লেজারের table-এ নয় |

### ১.৪ Open Decision-এর সাথে সম্পর্ক

যে card কোনো OD-র ওপর নির্ভর করে, সেখানে OD লেখা আছে। OD বন্ধ না হলে card `Blocked`। যেখানে OD-র একাধিক বিকল্প আছে সেখানে card **Blueprint-এর ধরে নেওয়া বিকল্প** অনুযায়ী লেখা, আর OD অন্য বিকল্পে গেলে কোন অংশ বদলাবে তা card-এ বলা আছে। এই Pack-এর OD-নম্বর আগের Blueprint-এর OD register (১১ নম্বর section) থেকে নেওয়া, নম্বর বদলানো হয়নি।

### ১.৫ Settings-এর যে ফাঁক ধরা পড়েছিল

Blueprint-এর Section ৩-এর setting table-এ কয়েকটা configurable জিনিস ওঠেনি। এই Pack-এ সেগুলো **P1-03-এর settings registry**-তে (Section ২.৪) যোগ করা আছে, তাই আলাদা করে আর কিছু করতে হবে না।

## ২. Schema index, ER overview ও registry

### ২.১ Table index

সব table এক জায়গায়, কোন card-এ প্রথম বানানো হয় সহ। Blueprint-এর generic নামের (`accounts`, `journals`) মিল পাশে।

| Table | Phase | কাজ | Blueprint-এর নাম | Card |
| --- | --- | --- | --- | --- |
| `acc_settings` | 1 | Company অনুযায়ী setting-এর মান | `accounting_settings` | P1-03 |
| `acc_settings_history` | 1 | Setting বদলের ইতিহাস | নতুন | P1-03 |
| `acc_setting_change_requests` | 1 | সংবেদনশীল setting-এ দ্বিতীয় অনুমোদন | নতুন | P1-04 |
| `acc_setup_status` | 1 | Setup Checklist-এর অবস্থা | `accounting_setup_status` | P1-05 |
| `acc_accounts` | 1 | Chart of Accounts | `accounts` | P1-06 |
| `acc_periods` | 1 | আর্থিক বছর ও মাসের period | `accounting_periods` | P1-08 |
| `acc_journals` | 1 | Journal header | `journals` | P1-09 |
| `acc_journal_lines` | 1 | Debit ও Credit line | `journal_lines` | P1-09 |
| `acc_journal_attachments` | 1 | Journal-এর কাগজপত্র | `journal_attachments` | P1-09 |
| `acc_idempotency_keys` | 1 | API-র idempotency key ও ফল | নতুন | P1-09 |
| `acc_journal_status_history` | 2 | Journal-এর অবস্থা বদলের ইতিহাস | `journal_status_history` | P2-04 |
| `acc_approval_rules` | 2 | Approval rule | `approval_rules` | P2-05 |
| `acc_approval_rule_levels` | 2 | Rule-এর ধাপ | `approval_rule_levels` | P2-05 |
| `acc_approval_requests` | 2 | Journal-ভিত্তিক approval request | `approval_requests` | P2-05 |
| `acc_approval_actions` | 2 | প্রতিটা সিদ্ধান্ত | `approval_actions` | P2-05 |
| `acc_approval_delegations` | 2 | Delegation | `approval_delegations` | P2-05 |
| `acc_voucher_counters` | 2 | Voucher নম্বরের counter | `voucher_counters` | P2-11 |
| `acc_period_balances` | 3 | Derived balance cache | `accounting_period_balances` | P3-02 |
| `acc_reconciliation_runs` | 3 | Reconciliation-এর ফল | `accounting_reconciliation_runs` | P3-13 |
| `acc_report_exports` | 3 | Export-এর ইতিহাস | `report_exports` | P3-10 |
| `acc_period_events` | 3 | Period বন্ধ ও আবার খোলার অনুরোধ ও ইতিহাস | নতুন | P3-11 |
| `acc_finance_event_inbox` | 4 | Event গ্রহণ ও অবস্থা | `finance_event_inbox` | P4-02 |
| `acc_finance_event_journal_links` | 4 | Event আর journal-এর সংযোগ | `finance_event_journal_links` | P4-06 |
| `acc_account_mappings` | 4 | Event-এর অংশ থেকে account | `account_mappings` | P4-05 |
| `acc_finance_event_actions` | 4 | Retry, ignore, release-এর ইতিহাস | `finance_event_actions` | P4-09 |
| `acc_enablement_log` | 5 | Flag চালু ও বন্ধের ইতিহাস | `accounting_enablement_log` | P5-06 |
| `asset_finance_events` | বিদ্যমান | Asset-এর outbox, Accounting শুধু পড়ে | — | — |

`activity_logs`, `companies`, `users` আর Platform-এর `approval_*` (যদি OD-6-এ ব্যবহার হয়) বিদ্যমান, এখানে তালিকাভুক্ত নয়।

### ২.২ ER overview

সব table-এর সব সম্পর্ক এক ছবিতে নেই, শুধু মূল শৃঙ্খলাটা। `acc_settings`, `acc_periods`, আর সহায়ক table-গুলো নিজেদের card-এ আছে।

&#91;embedded content: ER overview · মূল table-গুলোর সম্পর্ক\]

মাঝের সারিতে journal আর তার line, নিচে account। ডান দিকে approval-এর শৃঙ্খল, বাঁ দিকে Asset থেকে আসা event আর তার mapping। Asset-এর table Accounting শুধু পড়ে, লেখে না।

### ২.৩ Permission slug ও Error code registry

**Permission slug।** `config/actions.php`-এ যোগ হবে (P1-02)। প্রতিটা slug company ধরে দেওয়া হয়। কোন role ডিফল্টে কোনটা পাবে তা Blueprint ৪.৫ ও Phase-ভিত্তিক role table-এ আছে।

| Slug | কী করা যায় | Phase |
| --- | --- | --- |
| `accounting.settings.view` | Setting দেখা | 1 |
| `accounting.settings.manage` | Setting বদলানো | 1 |
| `accounting.settings.approve` | সংবেদনশীল setting-এর দ্বিতীয় অনুমোদন | 1 |
| `accounting.enable.manage` | `accounting.enabled` ও অন্য flag চালু বা বন্ধ | 1 |
| `accounting.coa.view` | CoA দেখা | 1 |
| `accounting.coa.manage` | CoA বানানো ও বদলানো | 1 |
| `accounting.journal.view` | Journal দেখা | 1 |
| `accounting.journal.create` | Draft বানানো, বদলানো, submit, withdraw | 1 |
| `accounting.journal.approve` | Approve বা reject | 2 |
| `accounting.journal.post` | Post করা | 2 |
| `accounting.journal.reverse` | Reversal ও Adjustment শুরু | 2 |
| `accounting.journal.override` | Emergency override | 2 |
| `accounting.approval.manage` | Approval rule, delegation বানানো | 2 |
| `accounting.period.view` | Period দেখা | 1 |
| `accounting.period.manage` | Period বন্ধ বা আবার খোলার অনুরোধ | 3 |
| `accounting.period.approve` | Period বন্ধ বা খোলার অনুমোদন | 3 |
| `accounting.report.view` | Report দেখা | 3 |
| `accounting.report.export` | Report নামানো | 3 |
| `accounting.cashflow.manage` | Cash Flow mapping বদলানো | 3 |
| `accounting.reconcile.run` | Reconciliation চালানো | 3 |
| `accounting.audit.view` | Audit log ও Audit Report দেখা | 1 |
| `accounting.mapping.manage` | Event mapping বদলানো | 4 |
| `accounting.intake.view` | Inbox দেখা | 4 |
| `accounting.intake.manage` | Retry, release, ignore | 4 |

**Error response-এর আকার।** সব API একই আকারে error দেয়:

`{ "code": "JRN_UNBALANCED", "message": "...", "field": "lines", "details": { ... }, "correlation_id": "..." }`

`field` ঐচ্ছিক (কোন input-এ সমস্যা), `details`-এ দরকারি তথ্য (যেমন Debit ও Credit-এর পার্থক্য), `correlation_id` log খোঁজার জন্য।

**Error code ও HTTP status।**

| Code | কখন | HTTP |
| --- | --- | --- |
| `ACC_FORBIDDEN` | Permission নেই | 403 |
| `ACC_COMPANY_FORBIDDEN` | অন্য company-র data চাওয়া হয়েছে | 403 |
| `ACC_MODULE_DISABLED` | `accounting.enabled` বন্ধ | 403 |
| `ACC_IDEMPOTENCY_CONFLICT` | একই key, ভিন্ন request | 409 |
| `SET_INCOMPLETE` | Setup Checklist অসম্পূর্ণ | 422 |
| `SET_LOCKED` | Lock হওয়া setting বদলানোর চেষ্টা | 409 |
| `SET_INVALID` | Setting-এর মান অবৈধ | 422 |
| `COA_CODE_DUPLICATE` | Company-র মধ্যে একই code | 422 |
| `COA_IN_USE` | Line আছে, মোছা যাবে না | 409 |
| `COA_SYSTEM_PROTECTED` | System account বদলানো বা মোছার চেষ্টা | 409 |
| `JRN_UNBALANCED` | Debit আর Credit মেলেনি | 422 |
| `JRN_ACCOUNT_INVALID` | Account ওই company-র নয়, inactive বা group | 422 |
| `JRN_LINE_INVALID` | Line-এ Debit আর Credit একসাথে, ঋণাত্মক বা শূন্য | 422 |
| `JRN_DUPLICATE_REF` | Reference আগে ব্যবহার হয়েছে | 409 বা 422 (setting অনুযায়ী) |
| `JRN_DATE_INVALID` | Cutover-এর আগে বা backdate সীমা পেরিয়ে | 422 |
| `JRN_ATTACHMENT_REQUIRED` | দরকারি attachment নেই | 422 |
| `JRN_NOT_APPROVED` | Approve ছাড়া post | 409 |
| `JRN_SOD_VIOLATION` | নিজের journal নিজে approve বা post | 403 |
| `JRN_PERIOD_LOCKED` | বন্ধ period-এ post | 409 |
| `JRN_IMMUTABLE` | Posted journal বদলানোর চেষ্টা | 409 |
| `JRN_STATE_INVALID` | ভুল অবস্থা থেকে ভুল কাজ | 409 |
| `JRN_CONFLICT` | `version` মেলেনি | 409 |
| `JRN_ALREADY_REVERSED` | আগেই reverse হয়েছে | 409 |
| `APR_NO_RULE` | প্রযোজ্য rule নেই | 422 |
| `APR_NOT_ELIGIBLE` | Approver-এর যোগ্যতা নেই | 403 |
| `APR_ALREADY_DECIDED` | এই ধাপে সিদ্ধান্ত হয়ে গেছে | 409 |
| `APR_RULE_GAP` | Rule-এ amount-এর ফাঁক বা overlap | 422 |
| `PRD_CLOSE_BLOCKED` | Period বন্ধের শর্ত পূরণ হয়নি | 422 |
| `PRD_REOPEN_DENIED` | Period খোলার শর্ত বা অনুমোদন নেই | 403 |
| `RPT_MAPPING_INCOMPLETE` | Cash Flow-এর mapping অসম্পূর্ণ | 422 |
| `RPT_RECON_FAILED` | Reconciliation মেলেনি | তথ্য হিসেবে (error নয়, report-এর সাথে) |
| `EVT_DUPLICATE` | একই event আবার | 200 (আগের ফল) |
| `EVT_CONFLICT` | একই identity, ভিন্ন payload | 409 |
| `EVT_MAPPING_MISSING` | Mapping নেই | তথ্য (inbox অবস্থা `held_mapping`) |
| `EVT_CURRENCY_MISMATCH` | Currency মেলেনি | তথ্য (`held_currency`) |
| `EVT_PAYLOAD_INVALID` | Payload অসম্পূর্ণ | তথ্য (`failed`) |
| `EVT_COMPANY_MISMATCH` | Source আর asset-এর company আলাদা | তথ্য (`failed`) |

`EVT_*` গুলো সাধারণত inbox-এর অবস্থা হিসেবে সংরক্ষিত হয়, কোনো user-এর request-এর error হিসেবে নয়। ops screen থেকে নিজে retry করলে তখন HTTP response আসে।

### ২.৪ সম্পূর্ণ Settings registry

সব setting `acc_settings`-এ `(company_id, key)` ধরে থাকে (P1-03)। **ধরন** কলামে যাচাইয়ের নিয়ম। **Default** কলামে "বাধ্যতামূলক" মানে কোনো default নেই, Admin না বসানো পর্যন্ত Setup Checklist সবুজ হয় না। যে default এখানে দেওয়া আছে সেটা Blueprint থেকে নেওয়া বা নিরাপদ প্রস্তাব, client অন্য মান চাইলে বদলাবে। যেগুলো **নতুন** চিহ্নিত, সেগুলো Blueprint-এর Section ৩-এর table-এ ছিল না, কিন্তু Phase section-এ উল্লেখ ছিল।

| Key | ধরন ও যাচাই | Default | Lock | Phase | OD / নোট |
| --- | --- | --- | --- | --- | --- |
| `accounting.enabled` | bool | false | না | 1 | Flag, Checklist সবুজ লাগে |
| `fiscal_year_start` | মাস ও দিন (MM-DD) | বাধ্যতামূলক | হ্যাঁ | 1 | OD-4 |
| `base_currency` | ISO 4217 code | বাধ্যতামূলক | হ্যাঁ | 1 | OD-3 |
| `currency_precision` | পূর্ণসংখ্যা 0 থেকে 4 | বাধ্যতামূলক | হ্যাঁ | 1 | OD-3, column scale 4-এর বেশি নয় |
| `accounting_basis` | `accrual` বা `cash` | বাধ্যতামূলক | হ্যাঁ | 1 | OD-1, OD-16 |
| `coa_code_min_length`, `coa_code_max_length` | পূর্ণসংখ্যা | 1 ও 20 | না | 1 | OD-7 |
| `coa_type_ranges` | type ধরে code-এর পরিসর (ঐচ্ছিক) | খালি | না | 1 | OD-7 |
| `coa_inactive_nonzero_policy` | `warn` বা `block` | `warn` | না | 1 | **নতুন**, OD-7 |
| `system_accounts` | `opening_balance_equity`, `retained_earnings`-এর account id | বাধ্যতামূলক | হ্যাঁ | 1 | OD-7 |
| `opening_cutover_date` | তারিখ | বাধ্যতামূলক | হ্যাঁ | 1 | OD-9 |
| `sensitive_settings` | কোন key-এ দ্বিতীয় অনুমোদন লাগবে তার তালিকা | `accounting_basis`, `base_currency`, `system_accounts`, `opening_cutover_date`, `cash_equivalent_accounts`, `account_mappings` | না | 1 | **নতুন** |
| `settings_require_second_approval` | bool | true | না | 1 | **নতুন**, ৬.৬-এর কথা |
| `voucher_format` | prefix, company code, বছর, অঙ্ক সংখ্যা, বছরে পুনরায় শুরু | বাধ্যতামূলক | হ্যাঁ | 2 | OD-8 |
| `duplicate_reference_mode` | `warn` বা `block` | `warn` | না | 2 | — |
| `attachment_required` | `never`, `always`, বা amount-এর সীমা | `never` | না | 2 | — |
| `backdate_limit_days` | পূর্ণসংখ্যা বা খালি (সীমাহীন) | খালি | না | 2 | OD-5 |
| `sod_maker_cannot_approve` | bool | true | না | 2 | **নতুন** |
| `sod_maker_cannot_post` | bool | true | না | 2 | **নতুন** |
| `sod_rule_editor_cannot_approve` | bool | true | না | 2 | **নতুন** |
| `sod_mapping_editor_cannot_approve` | bool | true | না | 4 | **নতুন** |
| `sod_poster_cannot_sole_close` | bool | true | না | 3 | **নতুন** |
| `reversal_default_date` | `today`, `original`, `ask` | `today` | না | 2 | **নতুন**, OD-15 |
| `override_reason_required` | bool | true | না | 2 | **নতুন** |
| `override_review_required` | bool | true | না | 2 | **নতুন** |
| `approval_no_rule_behavior` | `block` বা `fallback_approver` | `block` | না | 2 | OD-6 |
| `period_lock_mode` | `off`, `warn`, `strict` | `warn` | না | 3 | OD-5 |
| `period_close_checks` | `no_pending_journals`, `tb_balanced`, `recon_pass`-এর তালিকা | তিনটাই | না | 3 | **নতুন**, OD-5 |
| `cashflow_method` | `direct` বা `indirect` | বাধ্যতামূলক (Cash Flow চালু করতে) | না | 3 | OD-2 |
| `cash_equivalent_accounts` | account id-র তালিকা | খালি | না, audit সহ | 3 | OD-2, `is_cash_equivalent` flag-এর সাথে একই |
| `report_show_zero_balance` | bool | false | না | 3 | **নতুন** |
| `report_comparison_enabled` | bool | true | না | 3 | **নতুন** |
| `event_intake.enabled` | bool | false | না | 4 | — |
| `event_intake.sources.asset_write_off` | bool | false | না | 4 | OD-10, OD-18 |
| `event_currency_policy` | `hold` বা `ignore` | `hold` | না | 4 | OD-3 |
| `intake_batch_size` | পূর্ণসংখ্যা | 50 | না | 4 | **নতুন** |
| `intake_retry_backoff_seconds` | সেকেন্ডের তালিকা | 60, 300, 900, 3600 | না | 4 | **নতুন** |
| `intake_max_attempts` | পূর্ণসংখ্যা | 5 | না | 4 | **নতুন** |
| `monitor_held_event_age_hours` | পূর্ণসংখ্যা | 24 | না | 5 | **নতুন** |
| `monitor_held_event_count` | পূর্ণসংখ্যা | 10 | না | 5 | **নতুন** |
| `monitor_approval_stuck_days` | পূর্ণসংখ্যা | 3 | না | 5 | **নতুন** |
| `monitor_consumer_lag_minutes` | পূর্ণসংখ্যা | 30 | না | 5 | **নতুন** |
| `monitor_closed_period_post_attempts` | পূর্ণসংখ্যা | 5 | না | 5 | **নতুন** |

Approval rule আর mapping আলাদা table-এ থাকে (`acc_approval_rules`, `acc_account_mappings`), কারণ এগুলো তালিকা, একক মান নয়।

**Setup Checklist-এর ধাপ।** `acc_setup_status`-এ প্রতিটা ধাপের অবস্থা (`pending`, `done`, `not_applicable`)।

| ধাপ | কী যাচাই | কখন বাধ্যতামূলক |
| --- | --- | --- |
| `S01_fiscal_year` | `fiscal_year_start` আছে | সবসময় |
| `S02_currency` | `base_currency` ও `currency_precision` আছে | সবসময় |
| `S03_basis` | `accounting_basis` আছে | সবসময় |
| `S04_coa` | কমপক্ষে একটা leaf account, প্রতিটা type-এ অন্তত একটা | সবসময় |
| `S05_system_accounts` | দুটো system account নির্ধারিত | সবসময় |
| `S06_voucher` | `voucher_format` আছে | Phase 2 চালু হলে |
| `S07_approval_rule` | কমপক্ষে একটা সক্রিয় rule | Phase 2 চালু হলে |
| `S08_roles` | কমপক্ষে একজন Maker আর একজন Checker | Phase 2 চালু হলে |
| `S09_cutover` | `opening_cutover_date` আছে | সবসময় |
| `S10_opening` | Opening journal post হয়েছে বা Admin স্পষ্ট করে এড়িয়েছে | চালুর আগে |
| `S11_cashflow_mapping` | নগদ account ও `cashflow_class` ঠিক | Cash Flow চালু হলে। নয়তো `not_applicable`, শুধু Cash Flow বন্ধ থাকে |
| `I01_mappings` | প্রয়োজনীয় সব mapping বৈধ | `event_intake.enabled` চালু করতে |
| `I02_currency_policy` | `event_currency_policy` আছে | `event_intake.enabled` চালু করতে |
| `I03_source_flags` | অন্তত একটা source চালু | Asset intake চালু করতে |

`S01` থেকে `S10` সবুজ না হলে `accounting.enabled` চালু করা যায় না (`SET_INCOMPLETE`)। `I01` থেকে `I03` আলাদা group, শুধু intake চালু করার সময় দেখা হয়।

### ২.৫ Audit event registry

সব event `ActivityLogService` দিয়ে `activity_logs`-এ যায় (ERPFlow-এর বিদ্যমান append-style table)। নাম `accounting.<বিষয়>.<কাজ>`। **প্রতিটা event-এ সাধারণ field:** `company_id`, `actor_id`, `subject_type`, `subject_id`, `before`, `after`, `reason` (যেখানে লাগে), `correlation_id`। ব্যর্থ বা অস্বীকৃত চেষ্টাও লেখা হয়, শুধু সফল কাজ নয়।

| Event | কখন | Phase |
| --- | --- | --- |
| `accounting.flag.enabled`, `accounting.flag.disabled` | কোনো flag চালু বা বন্ধ (কে অনুমোদন দিল সহ) | 1 |
| `accounting.setting.changed` | Setting-এর মান বদল (আগে ও পরে) | 1 |
| `accounting.setting.change_requested`, `.approved`, `.rejected` | সংবেদনশীল setting-এর দ্বিতীয় অনুমোদন | 1 |
| `accounting.setup.step_changed` | Checklist-এর ধাপের অবস্থা বদল | 1 |
| `accounting.coa.created`, `.updated`, `.deactivated`, `.reactivated`, `.deleted` | CoA-র যেকোনো পরিবর্তন | 1 |
| `accounting.period.generated` | আর্থিক বছরের period তৈরি | 1 |
| `accounting.journal.draft_created`, `.draft_updated`, `.draft_deleted`, `.cancelled` | Draft-এর জীবন | 1, 2 |
| `accounting.journal.submitted`, `.withdrawn` | Submit ও প্রত্যাহার | 2 |
| `accounting.journal.approved`, `.rejected` | প্রতিটা ধাপের সিদ্ধান্ত (মন্তব্য সহ) | 2 |
| `accounting.journal.posted` | Post, voucher নম্বর সহ | 2 |
| `accounting.journal.reversal_created`, `.adjustment_created`, `.marked_reversed` | সংশোধনের শৃঙ্খল | 2 |
| `accounting.voucher.assigned` | Voucher নম্বর বরাদ্দ | 2 |
| `accounting.approval.rule_changed` | Rule বা ধাপের বদল | 2 |
| `accounting.approval.delegated`, `.delegation_ended` | Delegation | 2 |
| `accounting.approval.override_used`, `.override_reviewed` | Emergency override ও পর্যালোচনা | 2 |
| `accounting.guard.denied` | Permission, company বা SoD লঙ্ঘনের চেষ্টা (কারণ সহ) | 1, 2, 4 |
| `accounting.report.run`, `accounting.report.exported` | Report চালানো ও নামানো (filter সহ) | 3 |
| `accounting.period.close_requested`, `.closed`, `.reopen_requested`, `.reopened` | Period বন্ধ ও খোলা | 3 |
| `accounting.period.locked_post_attempt` | বন্ধ period-এ post-এর চেষ্টা (সফল বা ব্যর্থ) | 3 |
| `accounting.cashflow.mapping_changed` | Cash Flow-এর mapping বা নগদ চিহ্ন বদল | 3 |
| `accounting.recon.run` | Reconciliation চালানো ও ফল | 3 |
| `accounting.cache.rebuilt` | Derived cache পুনর্নির্মাণ | 3 |
| `accounting.event.received`, `.draft_created`, `.held`, `.failed`, `.conflict` | Inbox-এর অবস্থা বদল | 4 |
| `accounting.event.retried`, `.released`, `.ignored` | মানুষের কাজ (কারণ সহ) | 4 |
| `accounting.mapping.changed` | Mapping তৈরি, বদল, বন্ধ (আগে ও পরে) | 4 |
| `accounting.asset_status.sent`, `.failed` | Asset-এ ফল জানানো | 4 |
| `accounting.enablement.changed` | Release gate-এর সিদ্ধান্ত | 5 |

## ৩. Phase 0 task cards

Phase 0-এর কাজ code নয়, অনুমোদন আর নিশ্চিত করা। তবু প্রতিটা card WBS-এ আলাদা task হবে, কারণ এগুলো পরের সব card-এর শর্ত।

### P0-01: Blueprint ও ADR অনুমোদন

| Field | Specification |
| --- | --- |
| ধরন | `gov` |
| নির্ভর করে | Cursor-এর Phase 0 audit report (শেষ), Blueprint, এই Pack |
| অবরুদ্ধ করে | P0-03, P0-04, পুরো Phase 1 |
| উদ্দেশ্য | Architecture (A1 থেকে A10), Phase ক্রম, Open Decision register আর এই Pack লিখিতভাবে মেনে নেওয়া |
| কাজ | Tech lead, Product owner আর Finance contact মিলে Blueprint-এর ১ থেকে ১২ নম্বর section পর্যালোচনা। Audit-এ ধরা ফাঁকগুলো (approval-এ amount threshold নেই, posted journal DB-level অপরিবর্তনীয় নয়, company-তে currency ও fiscal year field নেই) স্বীকৃত হিসেবে লেখা |
| Deliverable | সই করা অনুমোদন নথি, সংশোধিত Blueprint (যদি দরকার হয়) |
| Acceptance | লিখিত অনুমোদন আছে, প্রতিটা OD-র একজন owner আছে |
| DB | প্রযোজ্য নয় |

### P0-02: Repo-র convention নিশ্চিত করা (spike)

| Field | Specification |
| --- | --- |
| ধরন | `gov` (read-only investigation) |
| নির্ভর করে | P0-01 |
| অবরুদ্ধ করে | P1-01 থেকে সব schema card |
| উদ্দেশ্য | Pack-এ "অনুমান" লেখা জিনিসগুলো কোড দেখে নিশ্চিত করা, যাতে Phase 1-এ কোনো ঘুরে আসা না লাগে |
| যা নিশ্চিত করতে হবে | (1) বিদ্যমান table-এর primary key ধরন (bigint বা ULID), (2) `companies` ও `users` table-এর নাম ও key, (3) `TenantContext` queue ও console job-এ কীভাবে বসে, (4) `Setting` model-এর API (group, key, value, cache), (5) `ActivityLogService`-এর method signature, (6) `ApprovalEngine`-এর contract ও executor registry, (7) permission কীভাবে `config/actions.php`-এ যোগ হয় ও role-এ বসে, (8) module scaffold-এর বিদ্যমান পদ্ধতি (কোন command বা কোন বিদ্যমান module নকল করা হয়), (9) queue driver ও scheduler-এর ব্যবস্থা, (10) file storage ও attachment-এর বিদ্যমান ব্যবস্থা, (11) test framework ও factory-র নিয়ম, (12) DB user-এর অনুমতি (trigger বানাতে পারে কি না) |
| Deliverable | "Convention Sheet", একটা পাতা, প্রতিটা প্রশ্নের উত্তর file ও line সহ |
| Acceptance | ১২টা প্রশ্নের সবকটার উত্তর আছে, যেটা অনিশ্চিত সেটা "অনিশ্চিত" লেখা |
| প্রভাব | PK ধরন বদলালে এই Pack-এর সব `*_id` column-এর ধরন বদলায়। বাকি কিছু বদলায় না |

### P0-03: Feature branch ও release নিয়ম

| Field | Specification |
| --- | --- |
| ধরন | `gov` |
| নির্ভর করে | P0-01 |
| অবরুদ্ধ করে | P1-01 |
| উদ্দেশ্য | `development`-এ push মানে production deploy, এই ঝুঁকি আটকানো |
| নিয়ম | (1) Accounting-এর সব কাজ `feature/accounting-*` branch-এ, (2) `development`-এ merge শুধু flag বন্ধ রেখে, আর PR-এ "flag Default বন্ধ প্রমাণিত" চেকবক্স, (3) প্রতিটা migration পিছনে সামঞ্জস্যপূর্ণ ও PR-এ তার প্রমাণ, (4) Project, Payroll, Asset, Commissioning-এর code Phase 1 থেকে 3-এ বদলানো নিষেধ, (5) CI-তে একটা অতিরিক্ত job যা দেখে Accounting-এর কোনো flag Default `true` নয় |
| Deliverable | Branching ও merge নিয়মের এক পাতা, PR template, CI check-এর খসড়া |
| Acceptance | নিয়ম দলের সবাই মেনেছে, CI check একটা ইচ্ছা করে ভুল PR-এ ব্যর্থ হয় |

### P0-04: Client-এর Setup Checklist ও Decision list পাঠানো

| Field | Specification |
| --- | --- |
| ধরন | `gov` |
| নির্ভর করে | P0-01 |
| অবরুদ্ধ করে | P1-03 (OD-3, OD-4 ছাড়া setting-এর মান বসবে না, কিন্তু কাঠামো আটকাবে না) |
| উদ্দেশ্য | Client-কে কী সিদ্ধান্ত নিতে হবে আর কখনের মধ্যে, তা পরিষ্কার জানানো |
| কাজ | Section ২.৪-এর registry আর Blueprint ১১-এর OD register থেকে client-এর জন্য ছোট নথি: কোন সিদ্ধান্ত, কে নেবে, কোন Phase শুরুর আগে লাগবে। Asset pilot-এর change request (OD-18)-এর খসড়া আলাদা পাতায় |
| Deliverable | Client-কে পাঠানো নথি, OD owner-এর তালিকা |
| Acceptance | Client পেয়েছে, প্রতিটা OD-র নাম ধরে একজন owner নির্ধারিত |

### P0-05: DB guard ও ledger-উৎস design spike

| Field | Specification |
| --- | --- |
| ধরন | `gov` (design spike, prototype merge হবে না) |
| নির্ভর করে | P0-02 |
| অবরুদ্ধ করে | P1-09, P1-10 |
| OD | OD-13, OD-14 |
| উদ্দেশ্য | Posted journal DB স্তরে কীভাবে অপরিবর্তনীয় হবে, আর ledger-এর উৎস কী হবে, তা প্রমাণসহ ঠিক করা |
| OD-13-এর বিকল্প | (ক) `BEFORE UPDATE` ও `BEFORE DELETE` trigger যা `acc_journals.status = 'posted'` থাকলে `SIGNAL` দিয়ে ব্যর্থ করে, `acc_journal_lines`-এ parent-এর status দেখে একই কাজ। (খ) Application-এর DB user-এর অনুমতি কমানো। (গ) দুটোই |
| কাজ | CI-র MySQL-এ trigger-এর prototype বানিয়ে দেখানো: Posted row-এ সরাসরি `UPDATE` ও `DELETE` ব্যর্থ হয়, `INSERT` আর Draft-এ `UPDATE` চলে, Post-এর transaction-এর ভেতরে অবস্থা বদল চলে (trigger-এ "Draft থেকে Posted" পরিবর্তন অনুমোদিত, "Posted থেকে অন্য কিছু" নিষিদ্ধ), `reversed` চিহ্ন বসানো চলে (শুধু একটা নির্দিষ্ট column) |
| OD-14 | Posted `acc_journal_lines` সরাসরি ledger (Option A, ধরে নেওয়া) নাকি আলাদা projection (Option B)। Prototype-এ বড় data-য় GL query-র গতি মেপে দেখা |
| Deliverable | Design note: বেছে নেওয়া উপায়, কেন, prototype migration আর test, trigger-এর `down()` নিরাপদ কি না |
| Acceptance | Prototype CI-র MySQL-এ চলে, `JRN_IMMUTABLE` প্রমাণিত |

## ৪. Phase 1 task cards: Foundation ও Configuration

Phase 1-এর শেষে একটা company-কে accounting-এর জন্য তৈরি করা যায়, কিন্তু কোনো লেনদেন post হয় না। Card-গুলো ক্রমে: module → permission → settings → checklist → CoA → period → journal-এর ভিত্তি → DB guard → opening balance → tenancy ও audit → test।

### P1-01: Module scaffold ও flag

| Field | Specification |
| --- | --- |
| ধরন | `service` |
| নির্ভর করে | P0-02, P0-03 |
| অবরুদ্ধ করে | সব Phase 1 card |
| উদ্দেশ্য | `Accounting` module-এর খালি কাঠামো, যা flag বন্ধ থাকলে কিছুই করে না |
| কাজ | (1) `Modules/Accounting` তৈরি (Convention Sheet-এর পদ্ধতিতে): service provider, routes, config, migrations ফোল্ডার। (2) `modules_statuses.json`-এ `Accounting` যোগ। Module চালু থাকলে migration চলে, তবে সব ব্যবহার flag-এ আটকানো। (3) Flag `accounting.enabled` company ধরে, ERPFlow-এর বিদ্যমান `Setting` model-এ (group `accounting`, key `enabled`), `TimeBillingFeatureService`-এর ধাঁচে একটা `AccountingFeatureService`। Flag তিনটা (`accounting.enabled`, `event_intake.enabled`, `event_intake.sources.*`) `acc_settings` নয়, `Setting`-এ থাকে, যাতে `acc_settings` table তৈরির আগেই flag কাজ করে। (4) Middleware `accounting.enabled`: flag বন্ধ হলে `ACC_MODULE_DISABLED`। (5) Frontend: `registerModules.ts`-এ module নিবন্ধন, navigation-এ আইটেম যা flag বন্ধ থাকলে দেখা যায় না |
| DB | শুধু বিদ্যমান `Setting` table পড়া: `SELECT value FROM settings WHERE company_id = ? AND group = 'accounting' AND key = 'enabled'`। কোনো নতুন table নেই |
| Contract | `AccountingFeatureService::enabled(companyId): bool`, `::intakeEnabled(companyId)`, `::sourceEnabled(companyId, source)`। Default সবসময় false |
| Error | `ACC_MODULE_DISABLED` (403) |
| Audit | `accounting.flag.enabled`, `accounting.flag.disabled` (flag বদলানোর card P5-06 আর P1-05-এ) |
| Acceptance | AC1.10 |
| Test | Flag বন্ধ থাকলে কোনো route সাড়া দেয় না, navigation-এ আইটেম নেই, বিদ্যমান module-এর কোনো test ভাঙে না |
| Deliverable | খালি কিন্তু বুট হওয়া module, flag service, middleware |
| Sub-tasks | module কাঠামো, status, flag service, middleware, frontend নিবন্ধন, test |

### P1-02: Permission slug ও role seed

| Field | Specification |
| --- | --- |
| ধরন | `service` |
| নির্ভর করে | P1-01 |
| অবরুদ্ধ করে | সব API card |
| উদ্দেশ্য | Section ২.৩-এর সব slug নিবন্ধন আর ডিফল্ট role-এ বসানো |
| কাজ | (1) `config/actions.php`-এ `accounting` module-এর অধীনে সব slug। (2) ডিফল্ট role-এ বসানোর seeder বা migration (Convention Sheet অনুযায়ী): Accountant, Finance Manager, Head of Finance, Auditor, Admin। প্রতিটা role-এর ডিফল্ট permission Blueprint ৪.৫ ও Phase-ভিত্তিক role table থেকে। (3) প্রতিটা API route-এ permission middleware |
| DB | RBAC-এর বিদ্যমান table-এ row যোগ (নতুন table নয়)। Seeder idempotent: আবার চালালে duplicate নয় |
| নিয়ম | Permission company ধরে। এক company-তে পাওয়া permission অন্য company-তে কাজ করে না (বিদ্যমান RBAC এটা কীভাবে করে তা Convention Sheet-এ) |
| Error | `ACC_FORBIDDEN`, `ACC_COMPANY_FORBIDDEN` |
| Audit | `accounting.guard.denied` |
| Acceptance | AC1.6 |
| Test | প্রতিটা role-slug জোড়ার matrix test, company পার হওয়া চেষ্টা, seeder দুইবার চালানো |
| Deliverable | Slug তালিকা, seeder, middleware |

### P1-03: Settings store, registry, history ও lock

| Field | Specification |
| --- | --- |
| ধরন | `schema` + `service` |
| নির্ভর করে | P1-01, P0-04 |
| অবরুদ্ধ করে | P1-04, P1-05, P1-06, P1-08, P2-03 |
| OD | OD-3, OD-4 (setting-এর মান নয়, শুধু কাঠামোর জন্য নয় আটকায় না) |
| উদ্দেশ্য | Company-ভিত্তিক typed setting, তার ইতিহাস আর lock-after-post নিয়ম |

**Schema: `acc_settings`**

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

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

**Schema: `acc_settings_history`**

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

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

| Field | Specification |
| --- | --- |
| DB flow: `set()` | (1) transaction শুরু। (2) `SELECT ... FROM acc_settings WHERE company_id = ? AND key = ? FOR UPDATE`। (3) Registry দিয়ে মান যাচাই (ধরন, পরিসর), ব্যর্থ হলে `SET_INVALID`। (4) Key-র `lock = true` হলে `PostedJournalProbe::hasPosted(companyId)` ডাকা। এটা একটা interface। P1-03-তে এর একটা stub implementation থাকে যা সবসময় `false` দেয় (কারণ `acc_journals` table তখনো নেই)। P1-09-এ আসল implementation বসে: `SELECT EXISTS(SELECT 1 FROM acc_journals WHERE company_id = ? AND status = 'posted')`। Posted থাকলে `SET_LOCKED`। (5) Key `sensitive_settings` তালিকায় থাকলে আর `settings_require_second_approval` true হলে শুধু change request তৈরি (P1-04), এখানেই শেষ। (6) নইলে `acc_settings` upsert: `version + 1`, `WHERE version = ?` মিলিয়ে। না মিললে `JRN_CONFLICT`। (7) `acc_settings_history`-এ row। (8) Audit event। (9) commit |
| Registry | `SettingRegistry` class: প্রতিটা key-র সংজ্ঞা (ধরন, যাচাই, default, lock, sensitive, phase, OD)। Section ২.৪ ঠিক যা। কোডে একটাই উৎস, UI আর API এটাই পড়ে |
| Flag ব্যতিক্রম | `accounting.enabled`, `event_intake.enabled`, `event_intake.sources.*` এখানে নয়, P1-01-এ বিদ্যমান `Setting` model-এ |
| Contract | `SettingsService::get(companyId, key)` (না থাকলে registry-র default, "বাধ্যতামূলক" হলে `null`), `::set(...)`, `::all(companyId)`, `::history(companyId, key)` |
| Error | `SET_INVALID`, `SET_LOCKED`, `JRN_CONFLICT` |
| Audit | `accounting.setting.changed` |
| Acceptance | AC1.4 |
| Test | টাইপ ও পরিসরের যাচাই, lock, দুজন একসাথে বদলানো, history যোগ হওয়া, default ফেরত |
| Deliverable | দুটো migration, registry, service, probe interface ও stub, API (`GET`, `PUT`), history API |
| Sub-tasks | migration ×2, registry, service, probe interface ও stub, API, test |

### P1-04: Settings UI ও দ্বিতীয় অনুমোদন

| Field | Specification |
| --- | --- |
| ধরন | `schema` + `service` + `ui` |
| নির্ভর করে | P1-03, P1-02 |
| অবরুদ্ধ করে | P1-05 (checklist-এর UI) |
| উদ্দেশ্য | Admin-এর setting screen আর সংবেদনশীল setting-এ Maker–Checker |

**Schema: `acc_setting_change_requests`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | FK | না |  |
| `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` | FK users | না |  |
| `decided_by` | FK users | হ্যাঁ |  |
| `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` হলে থাকবে।

| Field | Specification |
| --- | --- |
| DB flow: অনুমোদন | (1) transaction। (2) `SELECT ... FROM acc_setting_change_requests WHERE id = ? FOR UPDATE`। (3) `status = 'pending'` না হলে `JRN_STATE_INVALID`। (4) `decided_by <> requested_by` না হলে `JRN_SOD_VIOLATION`। (5) `accounting.settings.approve` permission। (6) `acc_settings` row `FOR UPDATE`, এর `version` = `base_version` না হলে অনুরোধ বাতিল (`status = 'cancelled'`, নতুন code `SET_STALE_REQUEST` 409, নতুন করে অনুরোধ করতে হবে)। (7) `SettingsService`-এর ভেতরের লেখার অংশ (দ্বিতীয় অনুমোদন এড়িয়ে) দিয়ে মান লেখা, history-তে `change_request_id` সহ। (8) request `approved`। (9) commit |
| নিয়ম | অনুরোধকারী নিজে অনুমোদন করতে পারে না (সবসময়, setting বদলানো যায় না)। একটা key-র জন্য একসাথে একটাই `pending` অনুরোধ। Pending থাকা অবস্থায় সরাসরি `set()` সেই key-তে আটকে। `settings_require_second_approval` false হলে অনুরোধ তৈরিই হয় না, সরাসরি লেখা |
| UI | (1) Settings পাতা: group ধরে (Basic, Journal, Approval-সংক্রান্ত, Period, Report, Intake, Monitoring), প্রতিটায় মান, default, lock চিহ্ন (কেন lock, কোন journal পোস্ট হয়েছে), সংবেদনশীল চিহ্ন। (2) বদলানোর সময় কারণ লেখার ঘর। (3) History ড্রয়ার। (4) "অপেক্ষমাণ অনুরোধ" tab অনুমোদনকারীর জন্য, পুরোনো ও নতুন মান পাশাপাশি আর প্রভাবের বিবরণ (যেমন cash equivalent বদলালে Cash Flow বদলাবে) |
| Contract | `POST /accounting/settings/{key}/change-requests`, `POST .../{id}/approve`, `.../reject`, `.../cancel`, `GET` তালিকা |
| Permission | `accounting.settings.manage` (অনুরোধ), `accounting.settings.approve` (সিদ্ধান্ত) |
| Error | `JRN_STATE_INVALID`, `JRN_SOD_VIOLATION`, `SET_STALE_REQUEST` (নতুন) |
| Audit | `accounting.setting.change_requested`, `.approved`, `.rejected` |
| Acceptance | AC1.4, AC1.6 |
| Test | নিজে approve, stale অনুরোধ, একই key-তে দুটো অনুরোধ, rejected-এর পর আবার |
| Deliverable | Migration, service, API, UI |
| Sub-tasks | migration, request service, approve/reject service, API, settings পাতা, history ড্রয়ার, অপেক্ষমাণ tab, test |

### P1-05: Setup Checklist

| Field | Specification |
| --- | --- |
| ধরন | `schema` + `service` + `ui` |
| নির্ভর করে | P1-03, P1-04 |
| অবরুদ্ধ করে | `accounting.enabled` চালু করার কাজ (P5-06) |
| উদ্দেশ্য | Company চালুর আগে সব দরকারি জিনিস ঠিক আছে কি না যাচাই |

**Schema: `acc_setup_status`**

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

Constraint: `uq_acc_setup_company_step (company_id, step_code)`।

| Field | Specification |
| --- | --- |
| DB flow: মূল্যায়ন | `ChecklistService::evaluate(companyId)`: প্রতিটা ধাপ আলাদা checker class, যে নিজে DB পড়ে ফল বের করে (ধাপ সংরক্ষিত অবস্থার ওপর নির্ভর করে না, প্রতিবার মূল তথ্য থেকে)। S01-S03, S09 `acc_settings` পড়ে। S04, S05 `acc_accounts` পড়ে (P1-06-এর পর)। S06 থেকে S08 Phase 2 চালু না হওয়া পর্যন্ত `not_applicable`। S10: opening journal post হয়েছে কি না (Phase 2-এর আগে শুধু `skipped` হতে পারে, Admin কারণ সহ)। S11 `not_applicable` যতক্ষণ Cash Flow চালু নয়। ফল `acc_setup_status`-এ upsert (একটা transaction-এ সব ধাপ) |
| কখন চলে | যেকোনো setting, CoA বা permission বদলের পর (event listener), checklist পাতা খোলার সময়, আর `accounting.enabled` চালুর অনুরোধের সময় (সবসময় নতুন করে) |
| `canEnable()` | `S01` থেকে `S10`-এর সবকটা `done`, `not_applicable` বা `skipped` হলে true। নইলে `SET_INCOMPLETE` আর কোন ধাপ বাকি তার তালিকা |
| Contract | `GET /accounting/setup-checklist`, `POST /accounting/setup-checklist/steps/{code}/skip` (শুধু S10, কারণ সহ), `POST /accounting/enable` (P5-06-এ পূর্ণ) |
| UI | ধাপের তালিকা: সবুজ, লাল, ধূসর (প্রযোজ্য নয়), প্রতিটা লাল ধাপে "ঠিক করুন" লিংক সংশ্লিষ্ট পাতায় |
| Error | `SET_INCOMPLETE` |
| Audit | `accounting.setup.step_changed` |
| Acceptance | AC1.5 |
| Test | প্রতিটা ধাপের লাল ও সবুজ, ধাপ বদলের পর নিজে নিজে মূল্যায়ন, `canEnable` |
| Deliverable | Migration, checker class সব ধাপের, service, API, UI |
| Sub-tasks | migration, checker ×14, service, listener, API, UI, test |

### P1-06: Chart of Accounts: schema ও service

| Field | Specification |
| --- | --- |
| ধরন | `schema` + `service` |
| নির্ভর করে | P1-03 |
| অবরুদ্ধ করে | P1-07, P1-09, P1-11 |
| OD | OD-7 (code-এর দৈর্ঘ্য ও পরিসর setting-এ, কাঠামো আটকায় না) |
| উদ্দেশ্য | প্রতি company-র আলাদা account তালিকা, DB স্তরে company-র সীমা সহ |

**Schema: `acc_accounts`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | FK | না |  |
| `code` | VARCHAR(20) | না |  |
| `name` | VARCHAR(150) | না |  |
| `type` | VARCHAR(16) | না | `asset`, `liability`, `equity`, `revenue`, `expense` |
| `parent_id` | FK (নিজের) | হ্যাঁ |  |
| `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 foreign key-র লক্ষ্য
- `uq_acc_accounts_company_syskey (company_id, system_key)`। `system_key` null হলে বহু row চলে (MySQL-এ null unique-এ গণ্য হয় না)
- `fk_acc_accounts_parent (company_id, parent_id)` → `acc_accounts (company_id, id)`। এতে অন্য 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)`

**Composite foreign key নিয়ম (এই Pack-জুড়ে):** যে table `account_id` বা `journal_id` রাখে, সেখানে একা `account_id`-র FK নয়, `(company_id, account_id)` composite FK। এতে অন্য company-র account বা journal জোড়া লাগানো DB স্তরেই অসম্ভব হয়, শুধু application-এর যাচাইয়ের ওপর ভরসা থাকে না।

| Field | Specification |
| --- | --- |
| DB flow: তৈরি | (1) transaction। (2) `parent_id` থাকলে parent row `SELECT ... FOR UPDATE`। (3) যাচাই: parent `is_group = true`, parent-এর type = নতুন account-এর type (R1.3), code-এর দৈর্ঘ্য ও পরিসর settings অনুযায়ী। ভাঙলে নতুন code `COA_CODE_INVALID` (422)। (4) `INSERT`। Unique ভাঙলে `COA_CODE_DUPLICATE`। (5) Audit। (6) commit |
| DB flow: type বদল | `PostedLineProbe::hasLines(accountId)` (P1-09-এর আগে stub `false`, তারপর `SELECT EXISTS(SELECT 1 FROM acc_journal_lines WHERE company_id = ? AND account_id = ?)`)। Line থাকলে আটকানো (R1.2), `COA_IN_USE` |
| DB flow: group বানানো | Account-এ line থাকলে `is_group = true` করা যায় না (R1.3) |
| DB flow: parent বদল | Chain ধরে উপরে উঠে চক্র দেখা (সর্বোচ্চ গভীরতা 10)। নতুন parent-এর type মেলা |
| DB flow: inactive | `coa_inactive_nonzero_policy` অনুযায়ী: closing balance শূন্য না হলে `warn` (নিশ্চিত করলে চলে) বা `block`। Balance হিসাব P1-09-এর পরে (stub শূন্য ফেরত) |
| DB flow: মোছা | Line নেই আর অন্য account-এর parent নয়, তবেই। নইলে `COA_IN_USE` |
| নিয়ম | R1.1 থেকে R1.6 (Blueprint ৬.৫)। System account মোছা, inactive করা, type বদল সব আটকে (`COA_SYSTEM_PROTECTED`) |
| Contract | `AccountService::create/update/deactivate/reactivate/delete`, প্রতিটায় `version` মেলানো |
| Error | `COA_CODE_DUPLICATE`, `COA_CODE_INVALID` (নতুন), `COA_IN_USE`, `COA_SYSTEM_PROTECTED`, `JRN_CONFLICT` |
| Audit | `accounting.coa.*` |
| Acceptance | AC1.2 |
| Test | একই code দুই company-তে, অন্য company-র parent, type বদল line সহ ও ছাড়া, চক্র, system account, দুজন একসাথে বদলানো |
| Deliverable | Migration, model, service |
| Sub-tasks | migration, model, create, update, deactivate, delete, চক্র যাচাই, probe stub, test |

### P1-07: CoA API ও UI

| Field | Specification |
| --- | --- |
| ধরন | `api` + `ui` |
| নির্ভর করে | P1-06, P1-02 |
| অবরুদ্ধ করে | P1-11, Phase 2-এর journal screen |
| উদ্দেশ্য | Accountant ও Finance Manager-এর CoA পরিচালনা |
| Contract | `GET /accounting/accounts` (গাছ বা flat, filter: type, active, search, `is_group`), `POST`, `PUT /{id}` (`version` সহ), `POST /{id}/deactivate`, `POST /{id}/reactivate`, `DELETE /{id}`। `POST /accounting/accounts/system` (Admin একটা ক্লিকে দুটো system account তৈরি করে, নাম ও code বসিয়ে, না হলে বিদ্যমান account চিহ্নিত করে) |
| UI | (1) গাছের view, expand ও collapse, প্রতিটা node-এ code, নাম, type, অবস্থা। (2) তৈরি ও সম্পাদনার ফর্ম: setting-এর code নিয়মের ইঙ্গিত দেখায়। (3) Type বদলানোর ঘর line থাকলে নিষ্ক্রিয়, কারণ লেখা। (4) Inactive দেখার toggle। (5) System account চিহ্ন ও সুরক্ষা। (6) Bulk import নেই, বাটন নেই (OD-19) |
| Permission | দেখা: `accounting.coa.view`। বদলানো: `accounting.coa.manage` |
| Acceptance | AC1.2, AC1.6 |
| Test | API-র permission matrix, UI-তে গাছ ও ফর্ম, optimistic `version` ভাঙা |
| Deliverable | API, গাছের UI, ফর্ম |
| Sub-tasks | list API, write API, system account API, গাছ UI, ফর্ম UI, error বার্তা, test |

### P1-08: Accounting period কাঠামো

| Field | Specification |
| --- | --- |
| ধরন | `schema` + `service` + `job` |
| নির্ভর করে | P1-03 |
| অবরুদ্ধ করে | P2-12 (PeriodGuard), P3-11 |
| OD | OD-4 (আর্থিক বছরের নামকরণ: বছর কি শুরুর বছর ধরে নাকি শেষের বছর ধরে, এই blueprint শুরুর বছর ধরেছে) |
| উদ্দেশ্য | Fiscal year আর মাসের ভিত্তিতে period তৈরি, যেগুলো পরে বন্ধ করা যায় |

**Schema: `acc_periods`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | FK | না |  |
| `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-এর composite FK-র জন্য), `idx_acc_periods_company_dates (company_id, start_date, end_date)`, `ck_acc_periods_dates`: `end_date >= start_date`, `ck_acc_periods_status`।

| Field | Specification |
| --- | --- |
| DB flow: তৈরি | `PeriodGenerator::ensureYear(companyId, fiscalYear)`: (1) `fiscal_year_start` পড়া (না থাকলে `SET_INCOMPLETE`)। (2) 12টা মাসের সীমা হিসাব। FY শুরু মাসের ১ তারিখ না হলে (যেমন ১৫ জুলাই) period-ও সেই তারিখে শুরু ও তার আগের দিন শেষ। (3) transaction-এ `INSERT ... ON DUPLICATE KEY` (বা `INSERT IGNORE`) দিয়ে সব period, আবার চালালে একই ফল |
| DB flow: খোঁজা | `PeriodResolver::forDate(companyId, date)`: `SELECT ... WHERE company_id = ? AND start_date <= ? AND end_date >= ?`। না পেলে `JRN_DATE_INVALID` |
| Job | মাসে একবার চলে, পরের আর্থিক বছরের period আগে থেকে তৈরি করে রাখে (idempotent, company context সহ) |
| নিয়ম | Period-এর তারিখ ফাঁকহীন ও overlap-হীন। একবার post হওয়া লেনদেন আছে এমন বছরের period-এর তারিখ বদলানো যাবে না (`SET_LOCKED`-এর মতো, আর্থিক বছর শুরু lock) |
| Edge | Leap year, ২৯ ফেব্রুয়ারিতে শুরু হওয়া FY (setting-এ যাচাই করে `SET_INVALID`), বছরের মাঝে setting বদল (lock আটকায়) |
| Error | `SET_INCOMPLETE`, `JRN_DATE_INVALID` |
| Audit | `accounting.period.generated` |
| Acceptance | নতুন AC1.12: period ফাঁকহীন ও overlap-হীন |
| Test | বিভিন্ন FY শুরু (জানুয়ারি, জুলাই, ১৫ জুলাই), আবার চালানো, leap year, তারিখ খোঁজা |
| Deliverable | Migration, generator, resolver, job |
| Sub-tasks | migration, generator, resolver, scheduler job, test |

### P1-09: Journal-এর ভিত্তি table ও idempotency

| Field | Specification |
| --- | --- |
| ধরন | `schema` + `service` |
| নির্ভর করে | P1-06, P1-08, P0-05 |
| অবরুদ্ধ করে | P1-10, P1-11, পুরো Phase 2 |
| উদ্দেশ্য | সব journal-এর জন্য header, line, attachment table আর API-র idempotency। এই Phase-এ শুধু opening-এর জন্য ব্যবহার হবে কিন্তু পুরো Phase 2 ও 4-এর জন্য নকশা করা |

**Schema: `acc_journals`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | FK | না |  |
| `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` | FK | হ্যাঁ | 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` | FK (নিজের) | হ্যাঁ |  |
| `adjusts_journal_id` | FK (নিজের) | হ্যাঁ |  |
| `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 স্তরে প্রয়োগ করে
- `fk_acc_journals_reverses (company_id, reverses_journal_id)` ও `fk_acc_journals_adjusts` composite, নিজের table-এ
- `fk_acc_journals_period (company_id, period_id)` → `acc_periods (company_id, id)`
- `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` | FK | না |  |
| `journal_id` | FK | না |  |
| `line_no` | SMALLINT UNSIGNED | না |  |
| `account_id` | FK | না |  |
| `debit`, `credit` | DECIMAL(20,4) | না | Default 0 |
| `line_narration` | VARCHAR(500) | হ্যাঁ |  |
| `cost_center_id` | FK (Configuration) | হ্যাঁ |  |
| `created_at`, `updated_at` |  | না |  |

Constraint ও index: `uq_acc_jl_journal_line (journal_id, line_no)`, `fk_acc_jl_journal (company_id, journal_id)` → `acc_journals (company_id, id)` `ON DELETE RESTRICT`, `fk_acc_jl_account (company_id, account_id)` → `acc_accounts (company_id, id)`, `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`। FK composite journal-এ। Index `(company_id, journal_id)`।

**Schema: `acc_idempotency_keys`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | FK | না |  |
| `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` | FK users | না |  |
| `created_at`, `expires_at` | TIMESTAMP | না |  |

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

| Field | Specification |
| --- | --- |
| DB flow: idempotency middleware | (1) `Idempotency-Key` header পড়া (write endpoint-এ বাধ্যতামূলক বলে চিহ্নিতগুলোতে না থাকলে 400)। (2) `INSERT` row `state = 'processing'`। Unique ভাঙলে: আগের row পড়া। `request_hash` মিললে আর `done` হলে সংরক্ষিত response ফেরত। `processing` হলে 409 "চলছে"। `request_hash` না মিললে `ACC_IDEMPOTENCY_CONFLICT`। (3) আসল কাজ চলে। (4) Response সংরক্ষণ ও `state = 'done'`। (5) Controller ব্যর্থ হলে (5xx) row মোছা হয়, আবার চেষ্টা করা যায় |
| Job | দৈনিক, `expires_at` পেরোনো row মোছা। Retention: 7 দিন |
| Probe | `PostedJournalProbe` (P1-03) আর `PostedLineProbe` (P1-06)-এর আসল implementation এখানে বসে |
| নিয়ম | Posted হওয়া journal বা line বদলানো এই card-এ শুধু schema-এ ঠিক করা, প্রয়োগ P1-10-এ |
| Acceptance | AC1.1 (composite FK), AC1.9-এর ভিত্তি |
| Test | Constraint গুলোর প্রতিটা সরাসরি SQL দিয়ে ভাঙার চেষ্টা (অন্য company-র account, দুটো opening, দুটো একই source, Debit আর Credit দুটোই, ঋণাত্মক), idempotency-র চারটা পথ |
| Deliverable | চারটা migration, model, idempotency middleware ও cleanup job, দুটো probe-এর আসল implementation |
| Sub-tasks | migration ×4, model, middleware, cleanup job, probe implementation, constraint test, idempotency test |

### P1-10: DB স্তরের অপরিবর্তনীয়তা (guard)

| Field | Specification |
| --- | --- |
| ধরন | `schema` |
| নির্ভর করে | P1-09, P0-05 |
| অবরুদ্ধ করে | P2-12 (Post) |
| OD | OD-13 (এই card P0-05-এর ফল অনুযায়ী লেখা, trigger-ভিত্তিক বিকল্প (ক) ধরে) |
| উদ্দেশ্য | Posted journal আর তার line application বাদ দিয়ে সরাসরি SQL দিয়েও বদলানো বা মোছা যাবে না |

**নিয়ম যা trigger প্রয়োগ করবে:**

| Table | কাজ | কখন অনুমোদিত |
| --- | --- | --- |
| `acc_journals` | UPDATE | `OLD.status` যদি `posted` বা `reversed` না হয়, সবকিছু। `OLD.status = 'posted'` আর `NEW.status = 'reversed'` হলে শুধু `status`, `reversed_at`, `reversed_by_journal_id`, `updated_at`, `updated_by`, `version` বদলাতে পারবে, আর কোনো column নয় |
| `acc_journals` | DELETE | `OLD.status` `posted` বা `reversed` না হলে |
| `acc_journal_lines` | INSERT, UPDATE, DELETE | Parent journal-এর status `draft` বা `rejected` হলে |
| `acc_journal_attachments` | INSERT, UPDATE, DELETE | Parent journal-এর status `draft` বা `rejected` হলে |
| শুধু-যোগ-হয় এমন table (নিচে) | UPDATE, DELETE | কখনো নয় |

**শুধু-যোগ-হয় (append-only) table:** `acc_settings_history`, এবং পরের Phase-এ `acc_journal_status_history`, `acc_approval_actions`, `acc_finance_event_actions`, `acc_period_events`, `acc_reconciliation_runs`, `acc_enablement_log`। প্রতিটার জন্য একটা সাধারণ migration helper `Schema::appendOnly('table')`, যাতে প্রতিবার হাতে trigger লিখতে না হয়।

Trigger-এর কাঠামো (সংক্ষিপ্ত, পুরো column তালিকা implementation-এ):

```sql
CREATE TRIGGER trg_acc_journals_bu BEFORE UPDATE ON acc_journals
FOR EACH ROW
BEGIN
  IF OLD.status IN ('posted','reversed') THEN
    IF NOT (
      OLD.status = 'posted' AND NEW.status = 'reversed'
      AND NEW.reversed_at IS NOT NULL AND NEW.reversed_by_journal_id IS NOT NULL
      AND NEW.company_id <=> OLD.company_id
      AND NEW.voucher_no <=> OLD.voucher_no
      AND NEW.journal_date <=> OLD.journal_date
      AND NEW.total_debit <=> OLD.total_debit
      AND NEW.total_credit <=> OLD.total_credit
      /* বাকি সব column একইভাবে */
    ) THEN
      SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'JRN_IMMUTABLE';
    END IF;
  END IF;
END
```

| Field | Specification |
| --- | --- |
| অতিরিক্ত column | `acc_journals.posted_hash CHAR(64) NULL` (expand migration)। P2-12 post-এর সময় journal আর তার line-এর canonical SHA-256 এখানে বসায় |
| ব্যর্থ হলে application | `SQLSTATE 45000` আর বার্তা `JRN_IMMUTABLE` ধরে `JRN_IMMUTABLE` (409), `APPEND_ONLY` ধরে নতুন code `ACC_APPEND_ONLY` (409) |
| সতর্কতা | MySQL-এ binary log চালু থাকলে trigger বানাতে `log_bin_trust_function_creators` বা সংশ্লিষ্ট অনুমতি লাগে। P0-02-এর ১২ নম্বর প্রশ্নের উত্তর অনুযায়ী ঠিক হয় deployment-এ কী করতে হবে। Trigger বানানো না গেলে বিকল্প: application-এ guard + `posted_hash` দিয়ে রাতের integrity check (P3-13)। এটা দুর্বল, তাই OD-13-এ client ও tech lead-কে স্পষ্ট জানাতে হবে |
| Rollback | `down()` trigger মুছে। এটা বিপজ্জনক (guard সরে যায়), তাই migration-এর মন্তব্যে স্পষ্ট লেখা আর production-এ `down()` চালানো নিষেধ (runbook) |
| Acceptance | AC1.9, পরে AC2.5 |
| Test | সরাসরি SQL দিয়ে: Posted journal-এ `UPDATE` ও `DELETE` ব্যর্থ, `posted` থেকে `reversed` শুধু অনুমোদিত column সহ চলে, বাকি column বদলালে ব্যর্থ, Draft journal ও line স্বাভাবিক, `submitted` অবস্থায় line বদলানো ব্যর্থ, append-only table-এ `UPDATE`/`DELETE` ব্যর্থ, transaction-এর ভেতরে `approved` থেকে `posted` চলে |
| Deliverable | Migration ও helper, trigger, error mapping, test |
| Sub-tasks | journals trigger, lines trigger, attachments trigger, append-only helper, `posted_hash` column, error mapping, test, fallback ও সতর্কতার নথি |

### P1-11: Opening balance Draft

| Field | Specification |
| --- | --- |
| ধরন | `service` + `api` + `ui` |
| নির্ভর করে | P1-07, P1-09, P1-10, P1-12 |
| অবরুদ্ধ করে | P2-14 (opening post), P1-05-এর S10 |
| OD | OD-9 (cutover তারিখ setting-এ থাকে, বসানো Admin-এর কাজ) |
| উদ্দেশ্য | শুরুর balance হাতে ঢোকানোর জায়গা, যা না মিললে সংরক্ষণই হয় না |
| Schema | কোনো নতুন table নেই। `acc_journals` (`journal_type = 'opening'`) আর `acc_journal_lines` ব্যবহার। একটা company-র একটাই সক্রিয় opening journal, `uq_acc_journals_opening` (P1-09) DB স্তরে নিশ্চিত করে |

| Field | Specification |
| --- | --- |
| DB flow: `PUT` | (1) Idempotency middleware। (2) transaction। (3) `opening_cutover_date` setting পড়া (না থাকলে `SET_INCOMPLETE`)। (4) এই company-র সক্রিয় opening journal `SELECT ... WHERE company_id = ? AND opening_active = 1 FOR UPDATE`। না থাকলে নতুন `draft` journal `INSERT` (`journal_date` = cutover, `currency` = `base_currency`)। থাকলে আর `status` `draft`-এ না হলে `JRN_STATE_INVALID` (posted হলে সংশোধন Phase 2-এর Adjustment-এ)। (5) `version` মেলানো, না মিললে `JRN_CONFLICT`। (6) সব পুরোনো line `DELETE` (Draft, তাই guard অনুমোদন করে)। (7) নতুন line `INSERT`: শূন্য amount-এর line বাদ। (8) যাচাই: প্রতিটা account `leaf` ও `active` ও এই company-র (`JRN_ACCOUNT_INVALID`), amount precision-এর মধ্যে (`JRN_LINE_INVALID`), Debit বা Credit একটাই। (9) `total_debit`, `total_credit` হিসাব ও `UPDATE`। (10) `total_debit <> total_credit` হলে পুরো transaction rollback আর `JRN_UNBALANCED` (`details.difference` সহ)। (11) `version + 1`। (12) Audit। (13) commit |
| DB flow: বাতিল | `DELETE /accounting/opening-balance`: Draft হলে `status = 'cancelled'`, `cancelled_at`। `opening_active` তখন null হয়, নতুন Draft বানানো যায় |
| নিয়ম | R1.7 থেকে R1.11 (Blueprint ৬.৫)। কোনো আপনা-আপনি মেলানোর line (যেমন "Opening Balance Equity" দিয়ে পার্থক্য ঢাকা) তৈরি হয় না, scope doc বলে না মেলা balance চুপচাপ গ্রহণ নয় |
| Contract | `GET /accounting/opening-balance` (journal, line, মোট, পার্থক্য), `PUT` (পুরো line তালিকা, `version`, idempotency key), `DELETE` |
| UI | (1) Account-এর গ্রিড: leaf ও active account, প্রতিটায় Debit ও Credit ঘর, অনুসন্ধান, type অনুযায়ী ভাগ। (2) চলতি মোট Debit, মোট Credit আর পার্থক্য, না মিললে লাল ব্যানার, সংরক্ষণ বাটন নিষ্ক্রিয়। (3) Cutover তারিখ দেখানো (সম্পাদনা এখানে নয়, setting-এ)। (4) অবস্থা দেখানো (Draft, আর Phase 2-এর পর Submitted ইত্যাদি) |
| Permission | লেখা: `accounting.journal.create`। দেখা: `accounting.journal.view` |
| Error | `SET_INCOMPLETE`, `JRN_ACCOUNT_INVALID`, `JRN_LINE_INVALID`, `JRN_UNBALANCED`, `JRN_STATE_INVALID`, `JRN_CONFLICT` |
| Audit | `accounting.journal.draft_created`, `.draft_updated`, `.cancelled` |
| Acceptance | AC1.3 |
| Test | মিললে সংরক্ষণ, না মিললে rollback ও পার্থক্য, inactive ও group account, অন্য company-র account, দ্বিতীয় opening Draft বানানো (DB আটকায়), বাতিলের পর নতুন, একই idempotency key দুইবার, দুজন একসাথে |
| Deliverable | Service, API, গ্রিড UI |
| Sub-tasks | service, `PUT`, `GET`, `DELETE`, গ্রিড UI, মোট ও পার্থক্যের অংশ, test |

### P1-12: Tenancy, audit আর error-এর সাধারণ ব্যবস্থা

| Field | Specification |
| --- | --- |
| ধরন | `service` |
| নির্ভর করে | P1-01, P0-02 |
| অবরুদ্ধ করে | P1-03 থেকে সব card যেখানে error বা audit বা job আছে (এটা আগে থেকে প্রস্তুত থাকা ভালো, তাই Master sequence-এ P1-01-এর পরেই) |
| উদ্দেশ্য | সব card যে তিনটা জিনিস ব্যবহার করবে তা একবারই বানানো: company context, audit লেখা, error response |
| কাজ: company context | (1) `RunsInCompanyContext` trait বা base class `AccountingJob`: constructor-এ `companyId` বাধ্যতামূলক, `handle()`-এর শুরুতে `TenantContext` বসানো আর `finally`-তে পরিষ্কার। (2) `companyId` ছাড়া job ডাকলে exception (job আরম্ভই হয় না)। (3) `CompanyScope` console-এ enforce না হলে সেটা test দিয়ে ধরা |
| কাজ: audit | `AccountingAudit::log(event, subject, before, after, reason = null)`: Section ২.৫-এর সাধারণ field বসিয়ে `ActivityLogService` ডাকে। `correlation_id` request-এর header থেকে, না থাকলে নতুন। Permission বা company লঙ্ঘনের middleware-এ `accounting.guard.denied` লেখা |
| কাজ: error | `AccountingException` ও তার সন্তান class, প্রতিটা Section ২.৩-এর code ও HTTP status-এর সাথে বাঁধা (একটা `AccountingError` enum, কোডে একটাই উৎস)। Exception handler সব Accounting API-তে `{code, message, field, details, correlation_id}` আকারে উত্তর দেয়। অপ্রত্যাশিত exception 500 আর `correlation_id`, ভেতরের তথ্য বাইরে নয় |
| কাজ: correlation | Middleware `X-Correlation-Id` পড়ে বা বানায়, response-এও ফেরত দেয়, log-এ সব জায়গায় |
| DB | কোনো নতুন table নেই। বিদ্যমান `activity_logs`-এ লেখা |
| Error | `ACC_FORBIDDEN`, `ACC_COMPANY_FORBIDDEN`, `ACC_MODULE_DISABLED` |
| Acceptance | AC1.8 |
| Test | Job context ছাড়া ব্যর্থ, context পরিষ্কার হয় (পরের job আগের company দেখে না), সব error code-এর HTTP status আর আকার (একটা contract test যা `AccountingError`-এর প্রতিটা case ঘোরে), audit-এ সব সাধারণ field |
| Deliverable | Base job, audit service, error enum ও handler, correlation middleware |
| Sub-tasks | base job, audit service, error enum, handler, correlation, test |

### P1-13: Phase 1-এর test suite ও regression

| Field | Specification |
| --- | --- |
| ধরন | `test` |
| নির্ভর করে | P1-01 থেকে P1-12 |
| অবরুদ্ধ করে | P1-14, Phase 2 শুরু |
| উদ্দেশ্য | Phase 1-এর সব Acceptance Criteria অটোমেটেড test দিয়ে প্রমাণ, আর বিদ্যমান ERPFlow অক্ষত |

| AC | কী প্রমাণ | Test class (প্রস্তাব) |
| --- | --- | --- |
| AC1.1 | অন্য company-র data দেখা বা বদলানো যায় না (API, DB composite FK) | `CompanyIsolationTest` |
| AC1.2 | প্রতি company-র আলাদা CoA | `CoaRulesTest` |
| AC1.3 | Opening না মিললে সংরক্ষণ হয় না | `OpeningBalanceTest` |
| AC1.4 | Lock নিয়ম | `SettingsLockTest` |
| AC1.5 | Checklist অসম্পূর্ণ হলে চালু হয় না | `SetupChecklistTest` |
| AC1.6 | Permission matrix | `PermissionMatrixTest` |
| AC1.7 | সব বদল audit-এ | `AuditCoverageTest` |
| AC1.8 | Job-এ company context | `CompanyContextJobTest` |
| AC1.9 | DB guard | `GuardTriggerTest` |
| AC1.10 | Flag বন্ধ থাকলে কিছু হয় না | `FeatureFlagTest` |
| AC1.11 | বিদ্যমান ERPFlow অক্ষত | সম্পূর্ণ বিদ্যমান suite |
| AC1.12 | Period ফাঁকহীন | `PeriodGeneratorTest` |

আরও: `ConstraintTest` (P1-09-এর প্রতিটা constraint ভাঙার চেষ্টা), `IdempotencyTest`, `ErrorContractTest`। **Test data:** Factory-তে দুই company, প্রতিটায় পাঁচ role-এর user, একটায় একই code-এর account। CI-তে MySQL-ভিত্তিক job চালাতে হবে (SQLite-এ trigger, generated column, composite FK আলাদা আচরণ করে, তাই এই test গুলো MySQL ছাড়া মূল্যহীন)।

| Field | Specification |
| --- | --- |
| Deliverable | সব test class, factory, CI configuration, test report |
| Done | AC1.1 থেকে AC1.12 সবুজ, Golden suite (Blueprint ১২.২)-এর প্রথম ৪টা সবুজ, বিদ্যমান suite সবুজ |
| Sub-tasks | প্রতিটা test class আলাদা, factory, CI job, report |

### P1-14: Runbook ও merge gate

| Field | Specification |
| --- | --- |
| ধরন | `ops` |
| নির্ভর করে | P1-13 |
| অবরুদ্ধ করে | Phase 2 শুরু |
| উদ্দেশ্য | Phase 1 নিরাপদে `development`-এ merge করা (flag বন্ধ), আর Phase 2-এর আগে accountant-এর মতামত |
| কাজ | (1) Runbook-এর প্রথম খসড়া: module কী, flag, Setup Checklist, trigger-এর সতর্কতা, `down()` চালানো নিষেধ, সাধারণ সমস্যা। (2) Merge gate checklist: সব flag Default বন্ধ প্রমাণ, প্রতিটা migration পিছনে সামঞ্জস্যপূর্ণ প্রমাণ, বিদ্যমান suite সবুজ, Project/Payroll/Asset/Commissioning-এর code অপরিবর্তিত (diff দিয়ে প্রমাণ)। (3) Accountant-কে নমুনা company-তে CoA, settings আর opening Draft দেখানো, তাঁর মতামত লিখে রাখা |
| Deliverable | Runbook খসড়া, merge gate-এর সই করা checklist, accountant-এর মতামত নথি |
| Acceptance | Phase 1 exit criteria (Blueprint ৬.১০) |
| Sub-tasks | runbook, merge checklist, accountant demo, মতামত নথি |

## ৫. Phase 2 task cards: Journal Engine ও Approvals

Phase 2-এর শেষে হাতে লেখা journal-এর পুরো জীবনচক্র চলে: Draft, Submit, Approve, Post, Reversal। Card-এর ক্রম: state machine → validator → Draft → history → approval (schema, rule, resolver, request, delegation, SoD) → voucher → posting → reversal → opening post → queue → test।

### P2-01: Journal state machine

| Field | Specification |
| --- | --- |
| ধরন | `service` |
| নির্ভর করে | P1-13 (Phase 1 exit), P1-10 |
| অবরুদ্ধ করে | P2-02 থেকে সব Phase 2 card |
| উদ্দেশ্য | Journal-এর সব অবস্থা বদল একটাই জায়গা দিয়ে, যাতে নিয়ম ভাঙার কোনো পথ না থাকে |

**বৈধ পরিবর্তন:**

| থেকে | কাজ | যায় | কে | বিশেষ শর্ত | DB-তে কী বদলায় |
| --- | --- | --- | --- | --- | --- |
| `draft` | `submit` | `submitted` | Maker (নিজের journal) | Submit validator পাস (P2-03), rule পাওয়া গেছে | `submission_no + 1`, `submitted_at`, `submitted_by`, approval request তৈরি |
| `draft` | `cancel` | `cancelled` | Maker | — | `cancelled_at` |
| `submitted` | `withdraw` | `draft` | Maker | কেউ কোনো সিদ্ধান্ত দেয়নি | approval request `cancelled` |
| `submitted` | `approve` | `submitted` বা `approved` | Approver | যোগ্য approver, SoD | শেষ ধাপ হলে `approved`, `approved_at` |
| `submitted` | `reject` | `rejected` | Approver | মন্তব্য বাধ্যতামূলক | approval request `rejected` |
| `rejected` | `submit` | `submitted` | Maker | আবার সব validator, নতুন `submission_no` | নতুন approval request |
| `rejected` | `cancel` | `cancelled` | Maker | — | `cancelled_at` |
| `approved` | `post` | `posted` | Post-অনুমতিওয়ালা | P2-12-এর সব শর্ত | `voucher_no`, `period_id`, `posted_at`, `posted_by`, `posted_hash` |
| `approved` | `return` | `rejected` | Approver বা Post-অনুমতিওয়ালা | মন্তব্য বাধ্যতামূলক। Post-এর আগে ধরা পড়া সমস্যার জন্য (যেমন account inactive হয়ে গেছে) | approval request `returned` |
| `posted` | `mark_reversed` | `reversed` | শুধু system (reversal post-এর transaction-এর ভেতরে) | মূল journal-এ reversal জোড়া | `reversed_at`, `reversed_by_journal_id` |

এর বাইরের যেকোনো জোড়া `JRN_STATE_INVALID`। `return` কাজটা Blueprint-এ স্পষ্ট ছিল না (সেখানে লেখা ছিল journal `approved`-এই থেকে যায়), কিন্তু তাহলে সমস্যাযুক্ত journal-এর কোনো সংশোধনের পথ থাকে না, তাই এটা যোগ করা হলো। **client-কে জানানো দরকার**, নিয়ম বদলাতে চাইলে এই table-ই একমাত্র জায়গা।

| Field | Specification |
| --- | --- |
| DB flow: একটা পরিবর্তন | `JournalTransitionService::apply(journalId, action, actor, ctx)`: (1) transaction। (2) `SELECT ... FROM acc_journals WHERE id = ? AND company_id = ? FOR UPDATE`। (3) বর্তমান `status`, `version` পড়া। (4) বৈধ জোড়া কি না ওপরের table-এ। (5) কর্তা, SoD ও permission যাচাই। (6) কাজ-নির্দিষ্ট শর্ত (validator, approval)। (7) `UPDATE acc_journals SET status = ?, ... , version = version + 1 WHERE id = ? AND status = ? AND version = ?`। Affected rows 0 হলে কেউ মাঝে বদলে দিয়েছে: আবার পড়ে `JRN_CONFLICT` বা `JRN_STATE_INVALID`। (8) `acc_journal_status_history`-এ row (P2-04)। (9) Audit। (10) commit |
| নিয়ম | সব অবস্থা বদল শুধু এই service দিয়ে। অন্য কোনো code সরাসরি `status` update করবে না (code review ও একটা static test: `acc_journals`-এ `status` set করা শুধু এই class-এ) |
| Contract | `JournalTransitionService::apply`, `::can(journal, action, actor)` (UI-র বাটন দেখানোর জন্য) |
| Error | `JRN_STATE_INVALID`, `JRN_CONFLICT`, `JRN_SOD_VIOLATION`, `ACC_FORBIDDEN` |
| Audit | অবস্থা বদলের প্রতিটার নিজের event (Section ২.৫) |
| Acceptance | AC2.4, AC2.10 |
| Test | প্রতিটা বৈধ জোড়া, প্রতিটা অবৈধ জোড়া (matrix test, সব অবস্থা × সব কাজ), দুজন একসাথে একই journal-এ, `return` |
| Deliverable | State machine class, transition service, matrix test |
| Sub-tasks | transition table, service, `can()`, static test, matrix test, concurrency test |

### P2-02: Draft তৈরি ও সম্পাদনা (API ও UI)

| Field | Specification |
| --- | --- |
| ধরন | `service` + `api` + `ui` |
| নির্ভর করে | P2-01, P2-03, P1-07 |
| অবরুদ্ধ করে | P2-08, P2-15 |
| উদ্দেশ্য | Maker-এর journal লেখার জায়গা |
| Schema | নতুন table নেই। `acc_journals`, `acc_journal_lines`, `acc_journal_attachments` (P1-09) |

| Field | Specification |
| --- | --- |
| Contract | `POST /accounting/journals` (header + line), `GET /accounting/journals` (তালিকা, filter), `GET /accounting/journals/{id}`, `PUT /accounting/journals/{id}` (পুরো line তালিকা বদলানো, `version`), `DELETE /accounting/journals/{id}` (Draft মুছে ফেলা, soft), `POST /{id}/copy`, `POST /{id}/attachments`, `DELETE /{id}/attachments/{aid}`, `POST /{id}/cancel`। `POST`, `PUT`-এ idempotency key |
| DB flow: তৈরি | (1) transaction। (2) `journal_type = 'manual'`, `status = 'draft'`, `currency` = base currency setting। (3) `INSERT` header। (4) Save validator (P2-03)। (5) `INSERT` lines (`line_no` ক্রমিক)। (6) `total_debit`, `total_credit` হিসাব ও `UPDATE`। (7) `status_history` (`from` null, `to` `draft`)। (8) Audit। (9) commit |
| DB flow: সম্পাদনা | (1) header `FOR UPDATE`। (2) `status` শুধু `draft` বা `rejected`, নইলে `JRN_STATE_INVALID` (DB guard-ও আটকায়)। (3) শুধু `created_by`, নইলে `ACC_FORBIDDEN` (Maker-ই সম্পাদনা করবে। ব্যতিক্রম: \`journal\_type = 'event\_generated'\` Draft, যার \`created\_by\` null কারণ system তৈরি করেছে, তা যেকোনো \`journal.create\`-ওয়ালা সম্পাদনা করতে পারে, P4-06। এমন journal-এ SoD-র "Maker" হলো যে Submit করেছে, \`submitted\_by\`)। (4) `version` মেলানো। (5) পুরোনো line `DELETE`, নতুন `INSERT`। (6) totals। (7) `version + 1`। (8) Audit (before ও after-এ line-এর সারাংশ)। (9) commit |
| DB flow: মোছা | Draft হলে `deleted_at` বসে, line আর attachment ফিজিক্যালি থাকে। `rejected` মোছা যায় না, `cancel` করতে হয় |
| DB flow: copy | নতুন Draft: একই line, তারিখ আজ, `reference_no` ও attachment কপি হয় না, `source_*` null |
| নিয়ম | Line-এর সর্বোচ্চ সংখ্যা 500। Attachment-এর আকার ও ধরনের সীমা ERPFlow-এর বিদ্যমান file নিয়ম অনুযায়ী (P0-02)। শুধু নিজের Draft সম্পাদনা। কোনো company-র `accounting.enabled` বন্ধ হলে সব বন্ধ |
| UI | (1) Journal editor: header (তারিখ, narration, reference), line গ্রিড: account picker (leaf ও active, code বা নাম দিয়ে অনুসন্ধান), Debit ও Credit, line-এর বিবরণ, cost center। (2) নিচে চলতি মোট ও পার্থক্য। (3) Attachment আপলোড। (4) বাটন: সংরক্ষণ (Draft), Submit, কপি, বাতিল। বাটন `can()` দেখে দেখায়। (5) Reject হওয়া journal-এ সংশোধন ও আবার submit, আর reject-এর মন্তব্য উপরে |
| Permission | `accounting.journal.create` (লেখা), `accounting.journal.view` (দেখা) |
| Error | `JRN_LINE_INVALID`, `JRN_ACCOUNT_INVALID`, `JRN_STATE_INVALID`, `JRN_CONFLICT`, `ACC_FORBIDDEN` |
| Acceptance | AC2.1, AC2.7 |
| Test | Draft তৈরি, সম্পাদনা, অন্যের Draft সম্পাদনা (নিষেধ), submitted-এ সম্পাদনা (নিষেধ), মোছা, copy, idempotency, `version` ভাঙা |
| Deliverable | API ও service, editor UI |
| Sub-tasks | create, update, delete, copy, attachment, তালিকা API, editor UI, গ্রিড, মোট ও পার্থক্য, test |

### P2-03: Journal validator

| Field | Specification |
| --- | --- |
| ধরন | `service` |
| নির্ভর করে | P1-09, P1-08, P1-03 |
| অবরুদ্ধ করে | P2-02, P2-12, P2-13, P4-06 |
| উদ্দেশ্য | সব journal যেখানেই তৈরি হোক (হাতে, reversal, পরে event থেকে), একই নিয়মে যাচাই |
| কাজ | দুটো class, একটাই কোডে রাখা নিয়ম: `SaveValidator` (হালকা) আর `SubmitValidator` (কঠোর, `SaveValidator`-এর সবকিছু সহ)। দুটোই সব ভুল একসাথে ফেরত দেয় (`details.errors` তালিকা, প্রথমটায় থেমে নয়) |

| নিয়ম | Save | Submit | Post (আবার) | Code |
| --- | --- | --- | --- | --- |
| Account এই company-র ও আছে | হ্যাঁ | হ্যাঁ | হ্যাঁ | `JRN_ACCOUNT_INVALID` |
| Account active ও leaf | হ্যাঁ | হ্যাঁ | হ্যাঁ | `JRN_ACCOUNT_INVALID` |
| Debit বা Credit একটাই, ঋণাত্মক নয়, precision-এর মধ্যে | হ্যাঁ | হ্যাঁ | হ্যাঁ | `JRN_LINE_INVALID` |
| তারিখ cutover-এর আগে নয় (R2.5) | না | হ্যাঁ | হ্যাঁ | `JRN_DATE_INVALID` |
| তারিখ `backdate_limit_days`-এর মধ্যে | না | হ্যাঁ | না | `JRN_DATE_INVALID` |
| তারিখ কোনো period-এ পড়ে | না | হ্যাঁ | হ্যাঁ | `JRN_DATE_INVALID` |
| কমপক্ষে দুই line | না | হ্যাঁ | হ্যাঁ | `JRN_LINE_INVALID` |
| মোট Debit = মোট Credit | না | হ্যাঁ | হ্যাঁ | `JRN_UNBALANCED` |
| Attachment দরকার হলে আছে | না | হ্যাঁ | না | `JRN_ATTACHMENT_REQUIRED` |
| Reference আগে ব্যবহার হয়নি | না | হ্যাঁ | না | `JRN_DUPLICATE_REF` |
| Currency = base currency | হ্যাঁ | হ্যাঁ | হ্যাঁ | `SET_INVALID` নয়, `JRN_LINE_INVALID` |

| Field | Specification |
| --- | --- |
| DB flow | (1) সব line-এর `account_id` একটাই query-তে (`WHERE company_id = ? AND id IN (...)`)। (2) Settings একবার পড়া (cache-সহ, প্রতিবার নয়)। (3) `PeriodResolver::forDate`। (4) Duplicate: `SELECT id FROM acc_journals WHERE company_id = ? AND reference_no = ? AND id <> ? AND status <> 'cancelled' AND deleted_at IS NULL LIMIT 1`। (5) কোনো লেখা নেই, শুধু পড়া |
| নিয়ম | Duplicate reference `duplicate_reference_mode`: `warn` হলে error নয়, `details.warnings`-এ, `block` হলে error |
| Error | ওপরের সব code। একাধিক ভুল হলে প্রধান code প্রথমটা, বাকিগুলো `details.errors`-এ |
| Acceptance | AC2.1 |
| Test | প্রতিটা নিয়মের ইতিবাচক ও নেতিবাচক, একাধিক ভুল একসাথে, `warn` বনাম `block` |
| Deliverable | দুটো validator class, rule-ভিত্তিক test |
| Sub-tasks | প্রতিটা নিয়ম আলাদা class বা method, aggregation, test |

### P2-04: Journal status history

| Field | Specification |
| --- | --- |
| ধরন | `schema` + `service` + `api` |
| নির্ভর করে | P2-01, P1-10 |
| অবরুদ্ধ করে | P2-15 (journal-এর timeline দেখানো) |
| উদ্দেশ্য | প্রতিটা journal-এর পুরো যাত্রা, কে কখন কোন অবস্থা থেকে কোনটায় |

**Schema: `acc_journal_status_history`**

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

Constraint ও index: composite FK `(company_id, journal_id)`, `idx_acc_jsh_company_journal_time (company_id, journal_id, created_at)`। Append-only (P1-10-এর helper)।

| Field | Specification |
| --- | --- |
| DB flow | শুধু `JournalTransitionService` (P2-01) লেখে, একই transaction-এ। অন্য কেউ নয় |
| Contract | `GET /accounting/journals/{id}/history` |
| Acceptance | AC2.10 |
| Test | প্রতিটা অবস্থা বদলে ঠিক একটা row, ব্যর্থ transaction-এ row নেই (rollback), `UPDATE` ও `DELETE` ব্যর্থ |
| Deliverable | Migration, writer, API |
| Sub-tasks | migration, writer, API, test |

### P2-05: Approval-এর table (schema)

| Field | Specification |
| --- | --- |
| ধরন | `schema` |
| নির্ভর করে | P2-01, P1-09 |
| অবরুদ্ধ করে | P2-06 থেকে P2-10 |
| OD | **OD-6**। এই card লেখা হয়েছে বিকল্প (গ) ধরে: Accounting-এর নিজস্ব approval table, Platform `ApprovalEngine`-এর ওপর নির্ভরতা ছাড়া। কারণ audit-এ ধরা পড়েছিল `ApprovalEngine`-এ amount threshold নেই, আর সেটা বাড়ালে অন্য module প্রভাবিত হতে পারে। OD-6 বিকল্প (ক) বা (খ)-তে গেলে কোন অংশ বাদ যাবে তা নিচে আছে |
| উদ্দেশ্য | Rule, ধাপ, request, সিদ্ধান্ত আর delegation রাখার জায়গা |
| বিকল্প (ক) বা (খ) হলে | `acc_approval_requests` আর `acc_approval_actions`-এর বদলে Platform-এর approval table ব্যবহার (journal-এর সাথে morph সম্পর্কে)। `acc_approval_rules` ও `acc_approval_rule_levels` তবু থাকবে, কারণ amount ও condition-ভিত্তিক rule Platform-এ নেই। P2-08-এর service বদলে Platform engine ডাকবে |

**Schema: `acc_approval_rules`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | FK | না |  |
| `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` | FK | না |  |
| `rule_id` | FK | না | Composite FK `(company_id, rule_id)` |
| `level_no` | SMALLINT UNSIGNED | না | 1 থেকে শুরু |
| `approver_role_id` | FK (RBAC role) | হ্যাঁ |  |
| `approver_user_id` | FK users | হ্যাঁ |  |
| `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` | FK | না |  |
| `journal_id` | FK | না | Composite FK |
| `submission_no` | INT UNSIGNED | না |  |
| `rule_id` | FK | না |  |
| `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` | FK users | না |  |
| `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` | FK | না |  |
| `request_id` | FK | না |  |
| `level_no` | SMALLINT UNSIGNED | না |  |
| `actor_id` | FK users | না |  |
| `acted_for_id` | FK users | হ্যাঁ | 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)`।

| Field | Specification |
| --- | --- |
| Deliverable | পাঁচটা migration (rules, levels, requests, actions, delegations), model ও relation, append-only helper `acc_approval_actions`-এ |
| Acceptance | AC2.10, AC2.13-এর ভিত্তি |
| Test | প্রতিটা check ও unique ভাঙার চেষ্টা সরাসরি SQL, composite FK |
| Sub-tasks | migration ×5, model, constraint test |

### P2-06: Approval rule-এর service ও configuration UI

| Field | Specification |
| --- | --- |
| ধরন | `service` + `api` + `ui` |
| নির্ভর করে | P2-05 |
| অবরুদ্ধ করে | P2-07 |
| উদ্দেশ্য | Admin নিজে rule বানাবে, কোডে কিছু বাঁধা নয় |
| Contract | `GET/POST/PUT /accounting/approval-rules`, `POST /{id}/deactivate`, `POST /{id}/levels` (ধাপ বদলানো পুরো তালিকা হিসেবে) |

| Field | Specification |
| --- | --- |
| Save-এর যাচাই | (1) `min_amount < max_amount`। (2) প্রতিটা rule-এ কমপক্ষে একটা শর্তহীন (`condition` null) ধাপ, যাতে সবসময় অন্তত একজন approver থাকে। (3) ধাপ নম্বর 1 থেকে ফাঁকহীন। (4) একটা ধাপে role বা user ঠিক একটা। (5) Approver-এর `accounting.journal.approve` permission আছে ওই company-তে (নইলে ধাপ কখনো পূরণ হবে না)। (6) **Overlap:** একই `(company, journal_type)`-এ দুটো সক্রিয় rule-এর amount-পরিসর (আর effective তারিখ) মিলে গেলে `APR_RULE_GAP` (422)। `any` ধরনের rule শুধু অন্য `any`-এর সাথে মেলা দেখে, নির্দিষ্ট ধরনের rule আগে প্রাধান্য পায় (P2-07)। (7) **ফাঁক:** পরিসর ঢাকা নেই এমন অংশ error নয়, সতর্কতা (`details.gaps`), কারণ ফাঁক ইচ্ছাকৃত হতে পারে (যেমন খুব ছোট amount)। Blueprint-এ "ফাঁক ও overlap ধরা পড়ে" লেখা ছিল, এখানে overlap error আর ফাঁক সতর্কতা। ফাঁকে পড়া journal-এর আচরণ `approval_no_rule_behavior` setting ঠিক করে |
| DB flow | transaction, rule row `FOR UPDATE`, `version` মিলিয়ে, overlap query: `SELECT id FROM acc_approval_rules WHERE company_id = ? AND is_active = 1 AND journal_type = ? AND id <> ? AND min_amount < ? AND (max_amount IS NULL OR max_amount > ?) AND (effective_to IS NULL OR effective_to >= ?) AND (effective_from IS NULL OR effective_from <= ?)`। ধাপ পুরো তালিকা হিসেবে বদলানো (পুরোনো মুছে নতুন বসানো, rule-এর `version + 1`) |
| নিয়ম | Rule বদলালে চলমান request বদলায় না, কারণ `rule_snapshot` (P2-08)। শেষ সক্রিয় rule নিষ্ক্রিয় করা যায়, তবে `S07` Setup ধাপ লাল হয় |
| UI | Rule-এর তালিকা, rule সম্পাদনার ফর্ম: নাম, ধরন, amount-পরিসর, mode, ধাপের তালিকা (role বা user বেছে, শর্ত যোগ), self-approval। সংরক্ষণের সময় overlap error আর ফাঁকের সতর্কতা। একটা "rule পরীক্ষা" বাটন: amount আর ধরন দিলে কোন rule ও কোন ধাপ খাটবে তা দেখায় (P2-07-এর resolver ব্যবহার করে) |
| Permission | `accounting.approval.manage` |
| Error | `APR_RULE_GAP`, `JRN_CONFLICT`, `SET_INVALID` |
| Audit | `accounting.approval.rule_changed` (আগে ও পরে সহ) |
| Acceptance | AC2.13 |
| Test | Overlap ধরা, ফাঁকের সতর্কতা, শর্তহীন ধাপ ছাড়া rule, approver-এর permission নেই, rule বদল চলমান request-এ প্রভাব ফেলে না |
| Deliverable | Service, API, UI |
| Sub-tasks | save validator, overlap query, ধাপ বদল, API, rule UI, "rule পরীক্ষা" বাটন, test |

### P2-07: Approval resolver

| Field | Specification |
| --- | --- |
| ধরন | `service` |
| নির্ভর করে | P2-06 |
| অবরুদ্ধ করে | P2-08 |
| উদ্দেশ্য | একটা journal Submit হলে কোন rule ও কোন ধাপ খাটবে তা একটাই জায়গা থেকে ঠিক হওয়া |
| DB flow | `ApprovalResolver::resolve(journal)`: (1) Amount = `total_debit`। (2) একটা query-তে প্রার্থী rule: `WHERE company_id = ? AND is_active = 1 AND (journal_type = ? OR journal_type = 'any') AND min_amount <= ? AND (max_amount IS NULL OR ? < max_amount) AND (effective_from IS NULL OR effective_from <= CURDATE()) AND (effective_to IS NULL OR effective_to >= CURDATE()) ORDER BY (journal_type = ?) DESC, min_amount DESC LIMIT 1`। নির্দিষ্ট ধরন আগে, তারপর বড় `min_amount`। (3) Rule না পেলে `approval_no_rule_behavior`: `block` হলে `APR_NO_RULE`, `fallback_approver` হলে settings-এ নির্ধারিত approver দিয়ে এক ধাপের নকল rule। (4) ধাপ পড়া: `acc_approval_rule_levels`। (5) প্রতিটা ধাপের `condition` journal-এর line-এর সাথে মিলিয়ে বাদ বা রাখা: `account_ids` বা `cost_center_ids`-এর কোনো একটা line-এ থাকলে ধাপ প্রযোজ্য। (6) ফল: rule, mode, প্রযোজ্য ধাপের তালিকা (ক্রম সহ)। কোনো লেখা নেই |
| নিয়ম | ফল নির্ণায়ক (একই input-এ সবসময় একই ফল)। প্রযোজ্য ধাপ ফাঁকা হতে পারে না (কারণ P2-06-এ শর্তহীন ধাপ বাধ্যতামূলক) |
| Contract | `ApprovalResolver::resolve(Journal): ResolvedApproval` (rule id, version, mode, levels\[\]) |
| Error | `APR_NO_RULE` |
| Acceptance | AC2.2 |
| Test | সীমানার মান (ঠিক `max_amount`-এ কোন rule), নির্দিষ্ট বনাম `any`, শর্তযুক্ত ধাপ থাকা ও না থাকা, `block` বনাম `fallback_approver`, তারিখের পরিসর |
| Deliverable | Resolver class, test |
| Sub-tasks | rule query, ধাপ ছাঁকা, fallback, test |

### P2-08: Approval request ও সিদ্ধান্ত

| Field | Specification |
| --- | --- |
| ধরন | `service` + `api` + `ui` |
| নির্ভর করে | P2-07, P2-10, P2-04 |
| অবরুদ্ধ করে | P2-12 |
| উদ্দেশ্য | Submit থেকে সব ধাপের অনুমোদন পর্যন্ত |
| Contract | `POST /accounting/journals/{id}/submit`, `.../withdraw`, `.../approve`, `.../reject`, `.../return`। `GET /accounting/approvals/pending` (আমার অপেক্ষায়) |

| Field | Specification |
| --- | --- |
| **Lock-এর ক্রম (সব Phase-এর জন্য নিয়ম)** | সবসময় `acc_journals` → `acc_approval_requests` → `acc_voucher_counters`। এই ক্রম ভাঙলে deadlock হয় |
| DB flow: submit | (1) idempotency। (2) transaction। (3) journal `FOR UPDATE`। (4) Submit validator (P2-03)। (5) `ApprovalResolver::resolve` (P2-07)। (6) `INSERT acc_approval_requests` (`rule_snapshot` সহ, `status = 'pending'`, `current_level` = প্রথম ধাপ বা null)। (7) `JournalTransitionService::apply(submit)`-এর ভেতরের অংশ (P2-01, একই transaction)। (8) commit |
| DB flow: approve | (1) idempotency। (2) transaction। (3) journal `FOR UPDATE`, তারপর request `FOR UPDATE`। (4) request `pending` কি না, নইলে `JRN_STATE_INVALID`। (5) কর্তা কোন ধাপের জন্য যোগ্য (P2-09-এর `ApproverEligibility`)। Sequential: শুধু `current_level`। Parallel: যেকোনো অপূর্ণ ধাপ। (6) SoD (P2-10)। (7) `INSERT acc_approval_actions` (`decision = 'approve'`)। Unique ভাঙলে `APR_ALREADY_DECIDED`। (8) সব প্রযোজ্য ধাপ পূর্ণ হলে request `approved` আর `JournalTransitionService::apply(approve)` → journal `approved`। নইলে sequential হলে `current_level` পরের ধাপে। (9) commit |
| DB flow: reject | একই প্রথম চার ধাপ। `INSERT acc_approval_actions` (`reject`, মন্তব্য বাধ্যতামূলক)। Request `rejected`। Journal `rejected` (P2-01)। আর কোনো ধাপ চলে না |
| DB flow: withdraw | শুধু Maker, আর request-এ কোনো action নেই। Request `cancelled`, journal `draft` |
| DB flow: return | শুধু `approved` journal-এ। Request `returned`, journal `rejected`, মন্তব্য বাধ্যতামূলক |
| Re-submit | `rejected` journal আবার Submit হলে নতুন `submission_no`, নতুন request, নতুন `rule_snapshot` (তখনকার rule), আগের request-এর ইতিহাস থাকে |
| নিয়ম | Approve বা reject করার জন্য `accounting.journal.approve` permission আর ধাপের যোগ্যতা দুটোই লাগে। Reject-এ মন্তব্য বাধ্যতামূলক (R2.11) |
| UI | (1) "অপেক্ষমাণ" তালিকা: journal, amount, Maker, কোন ধাপে, কত দিন ধরে। (2) Journal দেখার পাতায় approval-এর অবস্থা: ধাপ ধরে কে করেছে, কে বাকি, মন্তব্য। (3) Approve ও Reject বাটন (`can()` দেখে), Reject-এ মন্তব্যের ঘর। (4) Maker-এর জন্য Withdraw |
| Error | `APR_NO_RULE`, `APR_NOT_ELIGIBLE`, `APR_ALREADY_DECIDED`, `JRN_SOD_VIOLATION`, `JRN_STATE_INVALID`, `JRN_CONFLICT` |
| Audit | `accounting.journal.submitted`, `.approved`, `.rejected`, `.withdrawn` |
| Acceptance | AC2.2, AC2.3, AC2.7, AC2.10 |
| Test | সাধারণ এক ধাপ, দুই ধাপ sequential, parallel, দুজন একসাথে একই ধাপে, reject ও পুনরায় পাঠানো, withdraw, return, যোগ্য নয় এমন user, rule বদলের পরেও চলমান request আগের rule-এ |
| Deliverable | Service, API, অপেক্ষমাণ তালিকা ও journal-এর approval UI |
| Sub-tasks | submit, approve, reject, withdraw, return, re-submit, pending API, UI, test |

### P2-09: Delegation, emergency override ও পর্যালোচনা

| Field | Specification |
| --- | --- |
| ধরন | `schema` + `service` + `ui` |
| নির্ভর করে | P2-08 |
| অবরুদ্ধ করে | P2-16 (test) |
| উদ্দেশ্য | ছুটিতে থাকা approver-এর কাজ অন্যকে দেওয়া, আর জরুরি অবস্থায় approval এড়ানো (কঠোর নিয়ন্ত্রণে) |

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

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

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

| Field | Specification |
| --- | --- |
| `ApproverEligibility::canAct(user, request, level)` | (1) ধাপের `approver_user_id` = user, অথবা user-এর ওই company-তে `approver_role_id` role আছে (RBAC lookup, P0-02-এর পদ্ধতি)। (2) নয়তো একটা সক্রিয় delegation আছে যেখানে `delegate_id` = user, আজকের তারিখ `valid_from` থেকে `valid_to`-র মধ্যে, আর `delegator_id` ধাপের জন্য যোগ্য। (3) Delegation-এ কাজ করলে `acted_for_id` = delegator। Delegate-এর জন্যও SoD খাটে (সে Maker হলে নয়) |
| Delegation তৈরি | `POST /accounting/approval-delegations`। যাচাই: delegate-এর `accounting.journal.approve` permission আছে, তারিখ বৈধ। Delegator নিজে বা Admin তৈরি করতে পারে |
| Override | `POST /accounting/journals/{id}/override`। শুধু `accounting.journal.override` permission। কারণ বাধ্যতামূলক (`override_reason_required`)। Request-এর বাকি সব ধাপ একটা `override` action দিয়ে পূর্ণ ধরা হয়, request `approved`, journal `approved`। `is_override` বোঝাতে `decision = 'override'`। SoD এখানে আরও কঠোর: Maker কখনো নিজের journal override করতে পারে না, `sod_maker_cannot_approve` বন্ধ থাকলেও |
| পর্যালোচনা | `override_review_required` চালু থাকলে প্রতিটা override-এর পর আলাদা একজন (override-কারী নয়, Maker-ও নয়) পর্যালোচনা করে `accepted` বা `flagged` দেয়। অপর্যালোচিত override-এর তালিকা Approval Register-এ আর alert-এ |
| DB flow: override | journal ও request `FOR UPDATE`, `INSERT acc_approval_actions` (override), request ও journal অবস্থা বদল (P2-01), সব এক transaction-এ |
| Error | `APR_NOT_ELIGIBLE`, `JRN_SOD_VIOLATION`, `ACC_FORBIDDEN` |
| Audit | `accounting.approval.delegated`, `.delegation_ended`, `.override_used`, `.override_reviewed` |
| Acceptance | AC2.3, AC2.10 |
| Test | যোগ্য delegate, মেয়াদ পেরোনো delegation, Maker-ই delegate, override কারণ ছাড়া, Maker-এর override, পর্যালোচনা নিজের override-এ (নিষেধ) |
| Deliverable | Migration, eligibility service, delegation API ও UI, override flow ও পর্যালোচনা UI |
| Sub-tasks | migration, eligibility, delegation CRUD, override, review, UI ×3, test |

### P2-10: Segregation of Duties (SoD) service

| Field | Specification |
| --- | --- |
| ধরন | `service` |
| নির্ভর করে | P1-03 (settings) |
| অবরুদ্ধ করে | P2-08, P2-12, P1-04-এর ব্যবহার (settings approve), P3-11, P4-08 |
| উদ্দেশ্য | "কে কোন কাজ করতে পারবে না" নিয়ম একটাই জায়গায়, যেখান থেকে সব card ডাকে |
| কাজ | `SodGuard::assert(action, actor, subject)`: একটা বদ্ধ তালিকা, প্রতিটা নিয়ম একটা setting-এ চালু বা বন্ধ। লঙ্ঘনে `JRN_SOD_VIOLATION` (403) আর audit `accounting.guard.denied` |

| নিয়ম | Setting | Default | যেখানে ডাকে |
| --- | --- | --- | --- |
| Maker নিজের journal approve করতে পারে না | `sod_maker_cannot_approve` | true | P2-08 |
| Maker নিজের journal post করতে পারে না | `sod_maker_cannot_post` | true | P2-12 |
| যে approval rule বদলেছে সে ওই rule-এ যাওয়া journal approve করতে পারে না | `sod_rule_editor_cannot_approve` | true | P2-08 |
| যে mapping বদলেছে সে ওই mapping থেকে বানানো journal approve করতে পারে না | `sod_mapping_editor_cannot_approve` | true | P4-08 |
| যে journal post করেছে সে ওই period বন্ধের একমাত্র অনুমোদনকারী নয় | `sod_poster_cannot_sole_close` | true | P3-11 |
| Setting-এর অনুরোধকারী নিজে অনুমোদন করতে পারে না | সবসময় (বন্ধ করা যায় না) | — | P1-04 |
| Maker নিজের journal override করতে পারে না | সবসময় (বন্ধ করা যায় না) | — | P2-09 |
| Override-কারী নিজের override পর্যালোচনা করতে পারে না | সবসময় | — | P2-09 |

| Field | Specification |
| --- | --- |
| DB flow | কোনো লেখা নেই। প্রয়োজনীয় তথ্য পড়া: `acc_journals.created_by`, `acc_journals.submitted_by`, `acc_approval_rules.updated_by` (rule-এর শেষ বদলকারী), `acc_account_mappings.updated_by`, `acc_settings_history.changed_by` ইত্যাদি। "যে rule বদলেছে" নির্ণয়: journal-এর `acc_approval_requests.rule_id`-র rule-এর `updated_by` (rule snapshot-এ `rule_version` ও `updated_by` রাখা হয় যাতে পরে rule আরও বদলালেও আগের তথ্য ঠিক থাকে, P2-08-এর snapshot-এ এই দুটো field থাকবে) |
| Contract | `SodGuard::assert(string $action, User $actor, mixed $subject): void` |
| Error | `JRN_SOD_VIOLATION` |
| Audit | `accounting.guard.denied` (কোন নিয়ম ভাঙল সহ) |
| Acceptance | AC2.3 |
| Test | প্রতিটা নিয়ম চালু অবস্থায় আটকায়, বন্ধ করলে চলে (শুধু যেগুলো বন্ধ করা যায়), বন্ধ করা যায় না এমনগুলো setting-এ false করলেও আটকায় |
| Deliverable | `SodGuard`, নিয়মের table-driven test |
| Sub-tasks | guard class, প্রতিটা নিয়ম, test |

### P2-11: Voucher নম্বর

| Field | Specification |
| --- | --- |
| ধরন | `schema` + `service` |
| নির্ভর করে | P1-08, P1-09, P1-03 |
| অবরুদ্ধ করে | P2-12 |
| OD | **OD-8**। এই card ধরেছে: নম্বর Post-এর সময় বসে, company ও আর্থিক বছর ধরে ধারাবাহিক। Draft-এর সময় বসালে বাতিল হওয়া Draft-এ ফাঁক পড়ত |
| উদ্দেশ্য | Voucher নম্বরে কোনো গর্ত বা পুনরাবৃত্তি থাকবে না |

**Schema: `acc_voucher_counters`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | FK | না |  |
| `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)`।

| Field | Specification |
| --- | --- |
| DB flow | `VoucherService::next(companyId, fiscalYear)`, **শুধু Post-এর transaction-এর ভেতরে** ডাকা যায়: (1) `INSERT INTO acc_voucher_counters (company_id, fiscal_year, series, last_no) VALUES (?, ?, 'JV', 1) ON DUPLICATE KEY UPDATE last_no = LAST_INSERT_ID(last_no + 1)`। (2) `SELECT LAST_INSERT_ID()` থেকে নম্বর। (3) `voucher_format` setting অনুযায়ী সাজানো: prefix, company code, বছর, নম্বর শূন্য দিয়ে ভরা (যেমন `JV-ACME-2026-000123`)। Counter row-এর lock Post-এর commit পর্যন্ত থাকে, তাই Post ব্যর্থ হলে increment-ও rollback হয় আর নম্বর খরচ হয় না |
| নিয়ম | এটা lock-ক্রমের শেষ ধাপ (journal → request → counter)। Counter আলাদা transaction-এ বাড়ানো নিষেধ। একই company-র একই বছরের Post গুলো এই row-তে ক্রমে চলে, একটার পর একটা (এটা ইচ্ছাকৃত, নম্বর ফাঁকহীন রাখার দাম) |
| বাড়তি নিরাপত্তা | `uq_acc_journals_company_voucher` (P1-09) DB স্তরে একই নম্বর দুইবার ঠেকায় |
| Contract | `VoucherService::next`, `::format(setting, company, year, no)`, `::preview(company)` (Admin-এর জন্য ছাঁচের উদাহরণ) |
| Error | `SET_INCOMPLETE` (`voucher_format` নেই) |
| Audit | `accounting.voucher.assigned` |
| Acceptance | AC2.9 |
| Test | ৫০টা সমান্তরাল Post-এ গর্ত বা পুনরাবৃত্তি নেই (একাধিক DB connection সহ), Post ব্যর্থ হলে counter আগের মানে, বছর বদলে নতুন counter |
| Deliverable | Migration, service, ছাঁচ preview |
| Sub-tasks | migration, `next`, `format`, `preview`, সমান্তরাল test |

### P2-12: Posting service

| Field | Specification |
| --- | --- |
| ধরন | `service` + `api` |
| নির্ভর করে | P2-08, P2-10, P2-11, P1-10, P2-03 |
| অবরুদ্ধ করে | P2-13, P2-14, Phase 3 |
| OD | OD-5 (period lock-এর কঠোর রূপ Phase 3-এ, এখানে শুধু hook আর সতর্কতা), OD-14 (ledger-এর উৎস: ধরা হয়েছে posted line নিজেই) |
| উদ্দেশ্য | ledger-এ বদল আনার একমাত্র পথ। সবকিছু একটাই transaction-এ, ব্যর্থ হলে কিছুই বদলায় না |
| Contract | `POST /accounting/journals/{id}/post` (idempotency key বাধ্যতামূলক) |

| ধাপ | কী | ব্যর্থ হলে |
| --- | --- | --- |
| 1 | Idempotency (P1-09) | আগের ফল ফেরত |
| 2 | transaction শুরু |  |
| 3 | `SELECT ... FROM acc_journals WHERE id = ? AND company_id = ? FOR UPDATE` |  |
| 4 | `status = 'posted'` আগেই হলে সেই ফলই ফেরত (R2.13)। অন্য কোনো কারণে নয় |  |
| 5 | `status = 'approved'` না হলে | `JRN_NOT_APPROVED` |
| 6 | `accounting.journal.post` permission আর `SodGuard::assert('post', ...)` | `ACC_FORBIDDEN`, `JRN_SOD_VIOLATION` |
| 7 | Approval request `FOR UPDATE`, `status = 'approved'` মিলিয়ে | `JRN_NOT_APPROVED` |
| 8 | `SubmitValidator` Post-মোডে আবার (P2-03-এর Post কলাম): account active কি না, balance, period আছে কি না, তারিখ | `JRN_ACCOUNT_INVALID`, `JRN_UNBALANCED`, `JRN_DATE_INVALID` |
| 9 | `PeriodGuard::check(company, journal_date)` (নিচে) | `JRN_PERIOD_LOCKED` |
| 10 | Line থেকে সরাসরি `SELECT SUM(debit), SUM(credit)` আর header-এর `total_*` মিলিয়ে দেখা (সুরক্ষা, header ভুল হলে ধরা পড়ে) | `JRN_UNBALANCED` |
| 11 | `VoucherService::next` (P2-11) |  |
| 12 | `posted_hash` = journal-এর নির্দিষ্ট column আর line (`line_no` ক্রমে) নিয়ে canonical JSON-এর SHA-256 |  |
| 13 | `UPDATE acc_journals SET status = 'posted', voucher_no = ?, period_id = ?, posted_at = NOW(), posted_by = ?, posted_hash = ?, version = version + 1 WHERE id = ? AND status = 'approved' AND version = ?`। Affected rows 0 হলে `JRN_CONFLICT` |  |
| 14 | `journal_type = 'reversal'` হলে একই transaction-এ মূল journal `posted` থেকে `reversed` (P2-13, DB guard-এর অনুমোদিত পথ) |  |
| 15 | `acc_journal_status_history`, audit `accounting.journal.posted` |  |
| 16 | commit |  |
| 17 | commit-এর পরে (`afterCommit`): `JournalPosted` event, যা পরে (P3-02) derived cache `stale` করে। Phase 2-এ কেউ শোনে না |  |

| Field | Specification |
| --- | --- |
| `PeriodGuard::check` | `PeriodResolver::forDate` দিয়ে period। Phase 2-এ period সবসময় `open` (বন্ধ করা Phase 3-এ), তাই এখানে hook আছে কিন্তু শুধু `period_lock_mode`-এর `warn`-এর পথ চলে। Period `closed` আর mode `strict` হলে `JRN_PERIOD_LOCKED`। `warn` হলে সতর্কতা ফেরত আর `accounting.period.locked_post_attempt` audit। Phase 3-এ (P3-12) পূর্ণ রূপ |
| নিয়ম | R2.12, R2.13, R2.14, R2.17, R2.18, R2.20। কোনো ব্যর্থ ধাপে আংশিক কিছু থাকে না। Voucher নম্বর শুধু সফল Post-এ খরচ হয় |
| Error | `JRN_NOT_APPROVED`, `JRN_SOD_VIOLATION`, `ACC_FORBIDDEN`, `JRN_ACCOUNT_INVALID`, `JRN_UNBALANCED`, `JRN_DATE_INVALID`, `JRN_PERIOD_LOCKED`, `JRN_CONFLICT` |
| Audit | `accounting.journal.posted`, `accounting.voucher.assigned`, ব্যর্থ চেষ্টাও |
| Acceptance | AC2.4, AC2.5, AC2.6, AC2.7, AC2.9, AC2.11 |
| Test | সফল Post, দুইবার Post চাপা (আগের ফল), দুজন একসাথে, ধাপ 11-র পরে জোর করে exception (journal `approved`-এ, counter আগের মানে, কোনো line বদলায়নি), ধাপ 13 ব্যর্থ, account inactive হয়ে যাওয়া journal, SoD |
| Deliverable | `PostingService`, API, `PeriodGuard`-এর hook, canonical hash |
| Sub-tasks | service, ধাপ ১ থেকে ১৭, hash, `PeriodGuard` hook, API, failure-injection test |

### P2-13: Reversal ও Adjustment

| Field | Specification |
| --- | --- |
| ধরন | `schema` + `service` + `api` + `ui` |
| নির্ভর করে | P2-12 |
| অবরুদ্ধ করে | P2-16 |
| OD | OD-15 (বন্ধ period-এ reversal-এর তারিখ। Phase 2-এ period সব `open`, তাই `reversal_default_date` setting-ই চলে) |
| উদ্দেশ্য | Post হওয়া ভুল ঠিক করা, মূল journal না ছুঁয়ে |

**Schema যোগ (expand migration):** `acc_journals.reversal_active` একটা generated stored column: `CASE WHEN reverses_journal_id IS NOT NULL AND status <> 'cancelled' THEN reverses_journal_id END`। Unique: `uq_acc_journals_reversal (company_id, reversal_active)`। এতে একটা journal-এর একটাই সক্রিয় reversal DB স্তরে নিশ্চিত (R2.15)। Reversal বাতিল হলে (`cancelled`) আবার নতুন reversal বানানো যায়।

| Field | Specification |
| --- | --- |
| Contract | `POST /accounting/journals/{id}/reverse` (`reason` বাধ্যতামূলক, `date` ঐচ্ছিক), `POST /accounting/journals/{id}/adjust` (`reason`, `lines[]`) |
| DB flow: reversal | (1) idempotency। (2) transaction। (3) মূল journal `FOR UPDATE`। (4) `status = 'posted'` না হলে `JRN_STATE_INVALID`। `journal_type = 'opening'` হলে reversal নিষেধ (opening-এর সংশোধন শুধু Adjustment, P2-14)। (5) আগে থেকে সক্রিয় reversal থাকলে `JRN_ALREADY_REVERSED` (DB-র unique-ও আটকায়)। (6) তারিখ: `date` দিলে সেটা, নইলে `reversal_default_date` (`today`, `original`, `ask` হলে `date` বাধ্যতামূলক)। (7) নতুন journal `INSERT`: `journal_type = 'reversal'`, `status = 'draft'`, `reverses_journal_id`, `reversal_reason`, `reference_no = 'REV-' + মূলের voucher নম্বর`, narration-এ মূলের নম্বর। (8) মূলের প্রতিটা line একই account-এ কিন্তু Debit আর Credit অদলবদল করে `INSERT`। (9) totals। (10) `acc_journal_status_history`। (11) commit। এখানে Post হয় না |
| DB flow: adjustment | একই কাঠামো। `journal_type = 'adjustment'`, `adjusts_journal_id`, line ব্যবহারকারীর দেওয়া (শুধু পার্থক্য)। মূল journal-এ একাধিক adjustment চলে। একটা posted journal-এ আগে adjustment থাকলে reversal-এর সময় সতর্কতা (reversal পুরো মূল amount-ই উল্টায়, adjustment-এর প্রভাব নয়) |
| পরের জীবন | দুটোই সাধারণ Draft, তাই Submit, approve, post (P2-08, P2-12)। Reversal Post হলে মূল journal `reversed` চিহ্ন পায় (P2-12 ধাপ 14)। Reversal reject বা বাতিল হলে মূল `posted`-ই থাকে |
| নিয়ম | R2.15, R2.16। Reversal ও Adjustment-এর company মূলের company-ই (composite FK ও validator)। Reversal-এর account-গুলো আজ inactive হলেও Submit-এর validator আটকাবে (`JRN_ACCOUNT_INVALID`)। তখন Admin account সাময়িক চালু করে বা Adjustment দিয়ে অন্য পথে যেতে হবে। এটা ইচ্ছাকৃত, চুপচাপ অবৈধ account-এ post নয় |
| UI | Posted journal-এর পাতায় "Reverse" ও "Adjust" বাটন (`journal.reverse` permission আর `can()` দেখে)। Reverse-এ dialog: কারণ (বাধ্যতামূলক), তারিখ, উল্টানো line-এর পূর্বরূপ। তৈরির পর নতুন Draft-এ নিয়ে যায়। মূল আর reversal পরস্পরের সাথে লিংক করা, `reversed` ব্যাজ |
| Permission | `accounting.journal.reverse` |
| Error | `JRN_ALREADY_REVERSED`, `JRN_STATE_INVALID`, `ACC_FORBIDDEN`, `JRN_ACCOUNT_INVALID` |
| Audit | `accounting.journal.reversal_created`, `.adjustment_created`, `.marked_reversed` |
| Acceptance | AC2.8 |
| Test | সফল reversal ও তার post-এর পর মূল `reversed` ও মূল line অক্ষত, দুবার reverse (DB আটকায়), reversal বাতিলের পর আবার, opening reverse (নিষেধ), adjustment একাধিক, reversal-এর পরে ভুল account inactive |
| Deliverable | Migration, service, API, UI |
| Sub-tasks | migration, reversal, adjustment, API, UI dialog, link ও ব্যাজ, test |

### P2-14: Opening journal-এর Post

| Field | Specification |
| --- | --- |
| ধরন | `service` |
| নির্ভর করে | P1-11, P2-12 |
| অবরুদ্ধ করে | P2-16, P5-02 |
| উদ্দেশ্য | Phase 1-এর opening Draft সাধারণ পথে Post হওয়া, আর lock চালু হওয়া |
| কাজ | (1) Opening journal সাধারণ Submit, approve, post পথ ধরে চলে (`journal_type = 'opening'` আর rule `opening` বা `any` ধরনের)। (2) **Submit ও Post দুই জায়গাতেই** `journal_date` আবার `opening_cutover_date`-এর সাথে মেলানো। Draft আর Post-এর মাঝে setting বদলালে `JRN_DATE_INVALID` (ইচ্ছাকৃত, Phase 1-এ cutover তারিখ lock হয় না, শুধু posted থাকলে lock) (3) Post হলে `PostedJournalProbe::hasPosted` সত্য, তাই lock হওয়া সব setting এখন থেকে `SET_LOCKED` (৩.৩)। (4) `ChecklistService::evaluate` চালায়, `S10` সবুজ |
| নিয়ম | Opening Reversal নিষেধ (P2-13), শুধু Adjustment। Opening journal-এর `period_id` cutover তারিখের period। Opening journal-এর জন্য SoD আর approval অন্য journal-এর মতোই |
| DB flow | নতুন কিছু নেই, P2-08 ও P2-12 ব্যবহার। শুধু (2)-এর অতিরিক্ত যাচাই আর (4)-এর trigger |
| Acceptance | AC2.12 |
| Test | Opening Post-এর পর `SET_LOCKED`, Post-এর আগে cutover বদলানো আর Post চেষ্টা (`JRN_DATE_INVALID`), Opening reverse (নিষেধ), Adjustment চলে, `S10` সবুজ |
| Deliverable | Date-যাচাই, post-এর পর checklist trigger, test |
| Sub-tasks | date check Submit ও Post-এ, checklist trigger, test |

### P2-15: Queue ও Register

| Field | Specification |
| --- | --- |
| ধরন | `api` + `ui` |
| নির্ভর করে | P2-02, P2-08, P2-12 |
| অবরুদ্ধ করে | P2-16 |
| উদ্দেশ্য | কার কী করার আছে, আর সব journal ও approval-এর তালিকা |

**Schema যোগ (index migration):** `idx_acc_journals_company_creator_status (company_id, created_by, status)`, `idx_acc_journals_company_date (company_id, journal_date)`।

| তালিকা | কে দেখে | কী দেখায় |
| --- | --- | --- |
| আমার Draft | Maker | নিজের `draft` ও `rejected` |
| আমার অনুমোদনের অপেক্ষায় | Approver | যে `pending` request-এ আমি (বা আমার delegation-এ) এখন সিদ্ধান্ত দিতে পারি |
| আমার reject করা, ফেরত দেওয়া | Approver | ইতিহাস |
| অনুমোদিত, Post হয়নি | Post-অনুমতিওয়ালা | `approved` |
| Journal Register | সবাই (`journal.view`) | সব journal, filter |
| Approval Register | সবাই (`journal.view`) | Pending, Approved, Rejected, Posted-এর ইতিহাস |
| আটকে থাকা approval | Admin, Head of Finance | `pending` যা `monitor_approval_stuck_days`-এর বেশি |
| অপর্যালোচিত override | Head of Finance | P2-09 |

| Field | Specification |
| --- | --- |
| DB flow: অপেক্ষমাণ | `acc_approval_requests` `status = 'pending'` ছেঁকে, `rule_snapshot` থেকে `JSON_TABLE(rule_snapshot, '$.levels[*]' ...)` দিয়ে ধাপ বের করে, user-এর role ও সক্রিয় delegation-এর সাথে join। Sequential হলে শুধু `current_level`-এর ধাপ। `pending` সংখ্যা ছোট বলে এটা দ্রুত। বড় হলে অপটিমাইজেশন: ধাপগুলো আলাদা table-এ ছড়িয়ে দেওয়া (এখন নয়) |
| Register-এর filter | অবস্থা, ধরন, তারিখ পরিসর, voucher নম্বর, reference, account, amount পরিসর, Maker, Approver। Pagination `id` ধরে cursor (offset নয়) |
| নিয়ম | Register সবসময় company-scoped। Reference বা voucher নম্বর দিয়ে খোঁজা `company_id` সহ index-এ |
| Contract | `GET /accounting/journals?...`, `GET /accounting/approvals/register?...`, `GET /accounting/approvals/pending`, `GET /accounting/approvals/stuck`, `GET /accounting/approvals/overrides/unreviewed` |
| Acceptance | AC2.10 |
| Test | প্রতিটা তালিকা ঠিক লোকেদের ঠিক journal দেখায়, delegation-এর সাথে, অন্য company-র কিছু আসে না, cursor pagination |
| Deliverable | সব API, তালিকা ও register UI |
| Sub-tasks | index migration, প্রতিটা তালিকা API ও UI, filter, test |

### P2-16: Phase 2-এর test suite

| Field | Specification |
| --- | --- |
| ধরন | `test` |
| নির্ভর করে | P2-01 থেকে P2-15 |
| অবরুদ্ধ করে | P2-17, Phase 3 শুরু |

| AC | Test class (প্রস্তাব) | মূল দৃশ্য |
| --- | --- | --- |
| AC2.1 | `JournalValidationTest` | Unbalanced journal Submit ও Post দুই জায়গাতেই আটকায় |
| AC2.2 | `ApprovalRoutingTest` | Amount ও condition অনুযায়ী ভিন্ন rule ও ধাপ |
| AC2.3 | `ApprovalSodTest`, `OverrideTest` | অযোগ্য user বা Maker approve বা post বা override করতে পারে না, সরাসরি API-তেও |
| AC2.4 | `PostPreconditionTest` | `approved` ছাড়া Post নয় |
| AC2.5 | `PostedImmutabilityTest` | Application ও সরাসরি SQL, দুটোই |
| AC2.6 | `PostAtomicityTest` | মাঝপথে জোর করে ব্যর্থতা, কিছুই বদলায়নি |
| AC2.7 | `ConcurrencyTest` | একসাথে approve, post, edit |
| AC2.8 | `ReversalTest` | মূল অক্ষত, চিহ্ন বসা, জোড়া |
| AC2.9 | `VoucherSequenceTest` | সমান্তরাল Post-এ ফাঁকহীন |
| AC2.10 | `AuditHistoryTest` | প্রতিটা সিদ্ধান্তে user ও সময় |
| AC2.11 | `IdempotencyTest` | একই key-তে দ্বিতীয় call আগের ফল |
| AC2.12 | `OpeningPostTest` | Post-এর পর lock |
| AC2.13 | `ApprovalRuleValidationTest` | Overlap ধরা, ফাঁকের সতর্কতা |
| AC2.14 | সম্পূর্ণ বিদ্যমান suite | Project, Payroll, Asset অক্ষত |

আরও: `StateMachineMatrixTest` (সব অবস্থা × সব কাজ), `ApprovalFlowTest` (এক ধাপ, দুই ধাপ sequential, parallel, reject ও পুনরায়, withdraw, return)। **Golden suite** (Blueprint ১২.২)-এর প্রথম ৬টা এই Phase-এ সবুজ হতে হবে। সব concurrency আর atomicity test MySQL-এ, একাধিক connection দিয়ে।

| Field | Specification |
| --- | --- |
| Deliverable | সব test class, test report |
| Sub-tasks | প্রতিটা class আলাদা, failure-injection helper, সমান্তরাল runner |

### P2-17: Runbook, demo ও সিদ্ধান্ত বন্ধ

| Field | Specification |
| --- | --- |
| ধরন | `ops` + `gov` |
| নির্ভর করে | P2-16 |
| অবরুদ্ধ করে | Phase 3 শুরু |
| কাজ | (1) Runbook হালনাগাদ: Post ব্যর্থ হলে কী করা, voucher নম্বরে গর্ত সন্দেহ হলে কীভাবে যাচাই, আটকে থাকা approval, delegation, override-এর পর্যালোচনা। (2) Accountant demo: সাধারণ journal, reject ও পুনরায় পাঠানো, reversal। (3) OD-6 আর OD-8 লিখিতভাবে বন্ধ আছে কি না যাচাই। (4) Merge gate (P1-14-এর মতো) |
| Deliverable | Runbook, demo-র মতামত নথি, সই করা merge checklist |
| Acceptance | Phase 2 exit criteria (Blueprint ৭.১০) |

## ৬. Phase 3 task cards: General Ledger, Period ও Reports

Phase 3-এ posted data থেকে সব report বানানো হয়, আর হিসাব বন্ধের ব্যবস্থা আসে। **একটা জরুরি স্পষ্টীকরণ:** Blueprint-এ লেখা "শুধু posted journal report-এ আসে"-র মানে হলো ledger-এ আসে `status IN ('posted', 'reversed')` journal। `reversed` অবস্থার journal আসলে posted-ই, শুধু তার reversal-ও posted হয়েছে। দুটো একসাথে ধরলেই প্রভাব শূন্য হয় (R3.2)। `reversed` বাদ দিলে ভুল হিসাব হবে। এই Pack-এর সব query এই নিয়ম মানে।

Card-এর ক্রম: ledger query → cache → GL → Trial Balance → P&L → Balance Sheet → Cash Flow mapping → Cash Flow → অন্য report → export → period → reconciliation → test → সই।

### P3-01: Ledger query layer

| Field | Specification |
| --- | --- |
| ধরন | `service` |
| নির্ভর করে | P2-16 (Phase 2 exit), P0-05 (OD-14) |
| অবরুদ্ধ করে | P3-02 থেকে সব report |
| OD | OD-14 (ধরা হয়েছে: posted line নিজেই ledger, আলাদা projection নয়) |
| উদ্দেশ্য | সব report একই query layer থেকে হিসাব পায়, তাই সংখ্যা সবখানে এক |
| Schema | নতুন table নেই |

**মূল query (নমুনা, সব report-এর ভিত্তি):**

`SELECT l.account_id, SUM(l.debit) AS d, SUM(l.credit) AS c FROM acc_journal_lines l JOIN acc_journals j ON j.company_id = l.company_id AND j.id = l.journal_id WHERE l.company_id = :c AND j.status IN ('posted','reversed') AND j.journal_date <= :asof GROUP BY l.account_id`

| Field | Specification |
| --- | --- |
| Contract | `LedgerQueryService::balancesAsOf(companyId, date, filters)`, `::movements(companyId, from, to, filters)`, `::accountRows(companyId, accountId, from, to, cursor, limit)`, `::netProfit(companyId, from, to)`। সব টাকার অঙ্ক **string** হিসেবে ফেরত, PHP-র float কোথাও নয় (BCMath বা decimal library) |
| চিহ্ন নিয়ম | Debit-প্রধান type (asset, expense): balance = Debit − Credit। Credit-প্রধান (liability, equity, revenue): Credit − Debit (R3.4) |
| তারিখ | `journal_date`-এর ওপর, শুরু ও শেষ দিন অন্তর্ভুক্ত (R3.3) |
| Snapshot | যে report একাধিক query চালায় সেটা একটা `START TRANSACTION WITH CONSISTENT SNAPSHOT` (REPEATABLE READ, read-only) transaction-এ, যাতে চলার মাঝে নতুন Post এলেও সংখ্যা নিজের সাথে মেলে |
| Filter | `account_ids`, `account_group_id` (সব descendant সহ), `cost_center_id`, `type` |
| নিয়ম | R3.1 থেকে R3.5, R3.11। সব query `company_id` দিয়ে শুরু |
| Performance | Index: `idx_acc_journals_company_status_date`, `idx_acc_jl_company_account` (P1-09)। **Contingency:** P3-14-এর পরিমাপে join ধীর হলে দুটো বিকল্প: (১) `acc_journal_lines`-এ `entry_date` কপি রাখা (draft-এ journal-এর তারিখের সাথে সিঙ্ক, post-এর পর অপরিবর্তনীয়) আর `(company_id, account_id, entry_date)` index, (২) OD-14-এর Option B। দুটোর কোনোটাই এখন করা হচ্ছে না, মেপে ঠিক হবে |
| Error | কোনো business error নেই (শুধু পড়া) |
| Acceptance | AC3.1, AC3.5, AC3.10 (পরে), AC3.12 |
| Test | নির্দিষ্ট fixture data দিয়ে প্রতিটা method-এর ফল হাতে হিসাব করা সংখ্যার সাথে মেলে। `reversed` journal ধরা না হলে test ব্যর্থ। Snapshot test: query-র মাঝে অন্য connection থেকে Post করে দেখা সংখ্যা বদলায়নি। Draft, submitted, approved, rejected, cancelled কখনো আসে না |
| Deliverable | `LedgerQueryService`, fixture, test |
| Sub-tasks | চারটা method, চিহ্ন নিয়ম, snapshot wrapper, decimal helper, test |

### P3-02: Derived balance cache

| Field | Specification |
| --- | --- |
| ধরন | `schema` + `service` + `job` |
| নির্ভর করে | P3-01, P1-08 |
| অবরুদ্ধ করে | P3-04 (গতির জন্য), P3-13 (RC7) |
| উদ্দেশ্য | পুরোনো period-এর মোট বারবার হিসাব না করা। এটা কখনো উৎস নয়, শুধু দ্রুত করার জন্য, আর ভুল হলে ফেলে দিয়ে আবার বানানো যায় |

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

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | FK | না |  |
| `period_id` | FK | না | Composite FK |
| `account_id` | FK | না | Composite 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`।

| Field | Specification |
| --- | --- |
| বাতিল (invalidate) | `JournalPosted` event (P2-12, `afterCommit`)-এর listener: `UPDATE acc_periods SET balance_version = balance_version + 1 WHERE company_id = ? AND id = ?` (ওই journal-এর period)। এটা Post-এর transaction-এ নয়, commit-এর পরে, তাই Post-এ hot row তৈরি হয় না |
| বৈধ কি না | একটা cache row বৈধ হলে `row.period_version = acc_periods.balance_version`। না মিললে `stale` |
| হিসাব | `PeriodBalanceCache::rebuild(companyId, periodId)`: (1) `balance_version` পড়া (`v1`)। (2) `LedgerQueryService::movements(period-র পরিসর)`। (3) transaction-এ ওই period-র পুরোনো row মুছে নতুন row বসানো, `period_version = v1`। কোনো মাঝের Post এলে `balance_version` `v2` হয়ে যাবে আর এই row আপনা-আপনি `stale`, ক্ষতি নেই |
| ব্যবহার | `LedgerQueryService::balancesAsOf` রিপোর্টের শুরুর তারিখের আগের সম্পূর্ণ period-গুলো cache থেকে, বাকিটুকু সরাসরি line থেকে। `stale` period পেলে সেটা সরাসরি line থেকে হিসাব, আর পেছনে rebuild job queue-তে |
| Job | `RebuildPeriodBalancesJob(companyId, periodId)`: idempotent, company context (P1-12) সহ। রাতে সব `stale` period-এর জন্য একটা sweep |
| নিয়ম | R3.12। Cache কখনো ভুল সংখ্যা দেবে না, `stale` হলে ব্যবহার হয় না। RC7 প্রতিটা বৈধ row মূল line-এর যোগফলের সাথে মিলায় (P3-13) |
| Audit | `accounting.cache.rebuilt` |
| Acceptance | AC3.10 |
| Test | Post-এর পর cache `stale`, rebuild-এর পর বৈধ, rebuild-এর মাঝে Post (row `stale` হয়ে যায়), cache ও সরাসরি query-র ফল সবসময় এক |
| Deliverable | Migration ×2, cache service, listener, job |
| Sub-tasks | migration, `rebuild`, listener, ব্যবহার, job, রাতের sweep, test |

### P3-03: General Ledger report

| Field | Specification |
| --- | --- |
| ধরন | `api` + `ui` |
| নির্ভর করে | P3-01, P1-02 |
| অবরুদ্ধ করে | P3-13 |
| উদ্দেশ্য | একটা account-এর সব লেনদেন, চলতি balance সহ |
| Contract | `GET /accounting/reports/general-ledger?account_id=&from=&to=&cursor=&limit=&cost_center_id=` |

| Field | Specification |
| --- | --- |
| Response | `opening` (`from`-এর আগের দিন পর্যন্ত balance), `rows[]` (তারিখ, voucher নম্বর, journal id, line নম্বর, narration, reference, Debit, Credit, চলতি balance), `period_debit`, `period_credit`, `closing`, `next_cursor`, `reconciliation` (RC2 ফল) |
| DB flow | (1) Snapshot transaction (P3-01)। (2) `opening = balancesAsOf(from - 1 day)` ওই account-এর জন্য। (3) পৃষ্ঠার row: `ORDER BY j.journal_date, j.id, l.line_no`, keyset cursor `(journal_date, journal_id, line_no)`। (4) প্রতিটা পৃষ্ঠার শুরুর চলতি balance = `opening` + আগের সব row-এর (cursor পর্যন্ত) নিট যোগফল, একটা অতিরিক্ত aggregate query দিয়ে। (5) পৃষ্ঠার ভেতরে চলতি balance window function: `SUM(delta) OVER (ORDER BY ... ROWS UNBOUNDED PRECEDING) + :start`। (6) মোট `period_debit`, `period_credit`, `closing` আলাদা একটা aggregate দিয়ে (পৃষ্ঠা-নিরপেক্ষ) |
| Group account | Group account বেছে নিলে সব descendant leaf-এর row একসাথে, একটা বাড়তি "account" কলাম সহ। Opening আর closing group-এর যোগফল |
| Drill-down | প্রতিটা row-এর voucher নম্বরে click → journal-এর পাতা (P2-02) ও তার approval ইতিহাস (P2-08) ও status history (P2-04) |
| নিয়ম | R3.1 থেকে R3.4। `closing = opening + period_debit − period_credit` (debit-প্রধান হলে, নইলে উল্টো)। RC2: ওই account-এর closing Trial Balance-এর সাথে মেলে |
| UI | Account বাছাই (অনুসন্ধান সহ, group সহ), তারিখ পরিসর, cost center filter, তালিকা, চলতি balance কলাম, শেষে মোট ও closing, লাল সতর্কতা যদি RC2 না মেলে, Excel ও PDF বাটন (P3-10) |
| Permission | `accounting.report.view` |
| Audit | `accounting.report.run` |
| Acceptance | AC3.1, AC3.5, AC3.6 |
| Test | নমুনা account-এ হাতে হিসাব করা মান, group account, পৃষ্ঠা পরিবর্তনে চলতি balance ঠিক, `reversed` journal ধরা, drill-down লিংক, অন্য company-র account (`ACC_COMPANY_FORBIDDEN`) |
| Deliverable | API, UI |
| Sub-tasks | opening, পৃষ্ঠার query, চলতি balance, মোট, group, drill-down, API, UI, test |

### P3-04: Trial Balance

| Field | Specification |
| --- | --- |
| ধরন | `api` + `ui` |
| নির্ভর করে | P3-01, P3-02 |
| অবরুদ্ধ করে | P3-13 |
| উদ্দেশ্য | সব account-এর শুরুর balance, নড়াচড়া আর শেষের balance, আর মোট Debit = মোট Credit পরীক্ষা |
| Contract | `GET /accounting/reports/trial-balance?from=&to=` বা `?as_of=` |

| Field | Specification |
| --- | --- |
| Response | প্রতিটা account-এ: `opening_debit`, `opening_credit`, `period_debit`, `period_credit`, `closing_debit`, `closing_credit`। Group account-এ descendant-এর যোগফল। শেষে মোট ও `balanced` flag (RC1) |
| DB flow | (1) Snapshot। (2) `balancesAsOf(from - 1)` (cache সহ)। (3) `movements(from, to)`। (4) Leaf account ধরে গুনে CoA-র গাছ বেয়ে group-এ যোগ (PHP-তে, গাছ একবার পড়া)। (5) Closing = opening + movement। (6) Net balance চিহ্ন অনুযায়ী Debit বা Credit কলামে (একটাতেই) |
| Presentation | Closing balance Debit-কলামে (নিট Debit হলে) বা Credit-কলামে (নিট Credit হলে), দুটোতে নয় |
| নিয়ম | R3.1 থেকে R3.7। `report_show_zero_balance` false হলে শূন্য balance আর শূন্য নড়াচড়ার account বাদ। Inactive account-এর balance থাকলে দেখায় (R3.7)। `as_of` মোডে opening শূন্য, closing সেই তারিখ পর্যন্ত জমা |
| RC1 | মোট closing Debit = মোট closing Credit। না মিললে লাল ব্যানার, পার্থক্য সহ, report তবু দেখায় |
| UI | তারিখ বা পরিসর বাছাই, গাছের মতো সাজানো তালিকা (expand ও collapse), শূন্য দেখানোর toggle, মোট সারি, `balanced` চিহ্ন, প্রতিটা account-এ click → GL (P3-03) |
| Permission | `accounting.report.view` |
| Audit | `accounting.report.run` |
| Acceptance | AC3.1, AC3.5, AC3.6 |
| Test | নমুনা data-য় হাতে হিসাব, group roll-up, উল্টো balance-এর account (যেমন Asset-এ Credit balance), শূন্য toggle, `as_of`, RC1 ব্যর্থ করানো (ইচ্ছা করে ভাঙা data) |
| Deliverable | API, UI |
| Sub-tasks | movement ও opening, গাছের roll-up, Debit বা Credit কলাম, RC1, API, UI, test |

### P3-05: Profit & Loss

| Field | Specification |
| --- | --- |
| ধরন | `api` + `ui` |
| নির্ভর করে | P3-01, OD-1 |
| অবরুদ্ধ করে | P3-13 (RC5) |
| OD | **OD-1**, OD-16। Accrual ধরে লেখা |
| উদ্দেশ্য | নির্বাচিত সময়ের আয়, খরচ আর নিট লাভ বা ক্ষতি |
| Contract | `GET /accounting/reports/profit-loss?from=&to=&compare=` |

| Field | Specification |
| --- | --- |
| Response | Revenue ও Expense আলাদা অংশে, প্রতিটায় group অনুযায়ী উপমোট, মোট আয়, মোট খরচ, `net_profit` (= মোট আয় − মোট খরচ), `compare` চাইলে আগের সময়ের পাশাপাশি আর পরিবর্তন |
| DB flow | `movements(from, to)` শুধু `type IN ('revenue','expense')` account-এর। Revenue = Credit − Debit, Expense = Debit − Credit। গাছ ধরে group উপমোট |
| Comparison | `report_comparison_enabled` চালু থাকলে `compare=previous_period` (একই দৈর্ঘ্যের আগের সময়) বা `previous_year` |
| Basis | `accounting_basis = 'accrual'` হলে `journal_date`-এর ওপর (এই card)। `cash` হলে এখন `RPT_BASIS_UNSUPPORTED` (422, নতুন code), কারণ cash basis আলাদা logic, OD-16-এর (খ) অনুযায়ী পরের ধাপ। OD-16 (ক) হলে নতুন card `P3-05b` লাগবে |
| নিয়ম | R3.1, R3.2, R3.4। Opening journal-এ Revenue বা Expense account থাকলে তা সাধারণভাবে ধরা হয়, বিশেষ আচরণ নেই। এটা একটা সম্ভাব্য সমস্যা (নিচে OD-23) |
| UI | সময় বাছাই, Revenue ও Expense অংশ, উপমোট, নিট লাভ হাইলাইট (ক্ষতি হলে ঋণাত্মক ও লাল), তুলনা toggle, প্রতিটা account-এ click → GL |
| Permission | `accounting.report.view` |
| Audit | `accounting.report.run` |
| Acceptance | AC3.2, AC3.5, AC3.6 |
| Test | নমুনা data-য় হাতে হিসাব, reversal-সহ যোগফল শূন্য প্রভাব, তুলনা, `cash` basis-এ `RPT_BASIS_UNSUPPORTED`, ক্ষতি |
| Deliverable | API, UI |
| Sub-tasks | movement, group উপমোট, compare, basis check, API, UI, test |

### P3-06: Balance Sheet

| Field | Specification |
| --- | --- |
| ধরন | `api` + `ui` |
| নির্ভর করে | P3-01, P3-05, OD-1, OD-20 |
| অবরুদ্ধ করে | P3-13 (RC3, RC4) |
| OD | **OD-20**। ধরা হয়েছে (ক): গতিশীল হিসাব, বছর-শেষে আলাদা closing journal ছাড়া |
| উদ্দেশ্য | নির্দিষ্ট তারিখে Asset, Liability আর Equity, আর সমীকরণ মেলা |
| Contract | `GET /accounting/reports/balance-sheet?as_of=&compare=` |

| Field | Specification |
| --- | --- |
| Response | Asset, Liability, Equity অংশ, প্রতিটায় group উপমোট, মোট Asset, মোট Liability, মোট Equity, `balanced` flag (RC3) |
| Equity-র গঠন | (১) Equity type-এর account-এর balance, (২) **আগের বছরগুলোর জমানো নিট লাভ**: এই আর্থিক বছর শুরুর আগের দিন পর্যন্ত সব Revenue ও Expense-এর পার্থক্য (`netProfit(inception, FY শুরু − 1)`), (৩) **চলতি বছরের নিট লাভ**: `netProfit(FY শুরু, as_of)`। আর্থিক বছরের শুরু `as_of` যে period-এ পড়ে সেই period-এর `fiscal_year`-এর প্রথম period-এর `start_date` |
| DB flow | (1) Snapshot। (2) `balancesAsOf(as_of)` শুধু `asset`, `liability`, `equity` type। (3) `netProfit` দুটো। (4) Equity = (১) + (২) + (৩)। (5) RC3: মোট Asset = মোট Liability + মোট Equity |
| নিয়ম | R3.6। বছর-শেষের closing journal না থাকায় আগের বছরের লাভ প্রতিবার হিসাব হয়, তাই Retained Earnings account-এ হাতে করা journal থাকলে তা (১)-এ আসে আর (২)-এর সাথে যোগ হয়, double count নয় কি না তা accountant দেখবে (OD-20)। Asset-এ Credit balance বা Liability-তে Debit balance ঋণাত্মক অঙ্কে দেখানো হয়, অন্য অংশে সরানো হয় না |
| RC3 | না মিললে লাল ব্যানার, পার্থক্য সহ |
| UI | তারিখ বাছাই, তিন অংশ, Equity-তে তিন উপাদান আলাদা সারিতে, `balanced` চিহ্ন, তুলনা (আগের বছরের শেষ), প্রতিটা account-এ click → GL |
| Permission | `accounting.report.view` |
| Audit | `accounting.report.run` |
| Acceptance | AC3.2, AC3.3, AC3.5, AC3.6 |
| Test | নমুনা data-য় হাতে হিসাব, একাধিক আর্থিক বছর (আগের বছরের লাভ Equity-তে), আর্থিক বছর জুলাই-শুরু, RC3 সবসময় মেলে সুষম data-য়, ইচ্ছা করে ভাঙা data-য় লাল |
| Deliverable | API, UI |
| Sub-tasks | Asset-Liability-Equity হিসাব, আগের বছরের লাভ, চলতি লাভ, RC3, API, UI, test |

### P3-07: Cash Flow mapping UI ও যাচাই

| Field | Specification |
| --- | --- |
| ধরন | `service` + `api` + `ui` |
| নির্ভর করে | P1-07, P1-04, OD-2 |
| অবরুদ্ধ করে | P3-08 |
| OD | **OD-2** (নগদ-সমতুল্য কোন account, আর পদ্ধতি) |
| উদ্দেশ্য | কোন account নগদ, আর বাকি account Operating, Investing না Financing, সেটা Admin-এর নিজে ঠিক করা |
| Schema | নতুন table নেই। `acc_accounts.cashflow_class` আর `is_cash_equivalent` (P1-06)। **একটাই উৎস:** `cash_equivalent_accounts` setting (সংবেদনশীল, দ্বিতীয় অনুমোদন লাগে)। `is_cash_equivalent` শুধু তার প্রতিফলন, যা `SettingsService` সেই একই transaction-এ আপডেট করে। কেউ সরাসরি এই flag বদলাতে পারে না |

| Field | Specification |
| --- | --- |
| যাচাই (`CashflowMappingService::missing`) | (১) নগদ-সমতুল্য account-গুলো `asset` type আর leaf হতে হবে। (২) নগদ নয় এমন প্রতিটা `asset`, `liability`, `equity` account (leaf, active অথবা balance আছে) এর `cashflow_class` থাকতে হবে। (৩) `revenue` ও `expense` account-এর `cashflow_class` ফাঁকা থাকলে ধরা হয় `operating`, আলাদা মান দিলে (যেমন সম্পদ বিক্রির লাভ `investing`) সেটাই। অসম্পূর্ণ হলে ফেরত দেয় কোন কোন account বাকি |
| DB flow: `cashflow_class` বদল | transaction, account `FOR UPDATE`, `version` মিলিয়ে, `UPDATE`, audit। কোনো lock নেই, Posted data থাকলেও বদলানো যায় (এতে report-এর ফল বদলায়, তাই UI আগে সতর্ক করে আর audit-এ আগে ও পরে) |
| UI | সব account-এর তালিকা, দুটো কলাম: "নগদ-সমতুল্য" (টিক, বদলাতে গেলে setting-এর দ্বিতীয় অনুমোদনের পথে যায়) আর "Cash Flow ভাগ" (ড্রপডাউন)। Group account-এ ভাগ বেছে নিলে সব descendant-এ প্রযোজ্য। উপরে "বাকি আছে: ১২টা account" গণনা আর ফিল্টার |
| Permission | `accounting.cashflow.manage` |
| Error | `RPT_MAPPING_INCOMPLETE`, `SET_INVALID` |
| Audit | `accounting.cashflow.mapping_changed` |
| Acceptance | AC3.11 |
| Test | অসম্পূর্ণ তালিকা ঠিক ধরা, নগদ-সমতুল্য non-asset account-এ আপত্তি, দ্বিতীয় অনুমোদন ছাড়া নগদ চিহ্ন বদলানো যায় না, group-এ প্রয়োগ |
| Deliverable | Service, API, UI |
| Sub-tasks | `missing`, সুরক্ষা (flag-এর একমাত্র লেখক `SettingsService`), `cashflow_class` update, UI, test |

### P3-08: Cash Flow Statement

| Field | Specification |
| --- | --- |
| ধরন | `service` + `api` + `ui` |
| নির্ভর করে | P3-01, P3-07, P3-06, OD-1, OD-2 |
| অবরুদ্ধ করে | P3-13 (RC4) |
| OD | **OD-1, OD-2**। দুটো বন্ধ না হলে এই card শুরু হয় না (acceptance-এ আটকাবে, stub চলবে না) |
| উদ্দেশ্য | নির্বাচিত সময়ে নগদ কীভাবে এল ও গেল, আর শুরু ও শেষের নগদের সাথে মেলে |
| Contract | `GET /accounting/reports/cash-flow?from=&to=` (পদ্ধতি `cashflow_method` setting থেকে) |
| Schema | নতুন table নেই |

**সংজ্ঞা:** নগদ = নগদ-সমতুল্য account-গুলোর balance-এর যোগফল। `নিট পরিবর্তন` = শেষের নগদ − শুরুর নগদ (`balancesAsOf(to)` ও `balancesAsOf(from − 1)` নগদ account-এ)।

**Direct পদ্ধতি (`cashflow_method = 'direct'`):**

| ধাপ | কাজ |
| --- | --- |
| 1 | সময়ের মধ্যে `status IN ('posted','reversed')` সব journal-এর মধ্যে সেগুলো, যেগুলোর অন্তত একটা line নগদ account-এ |
| 2 | প্রতিটা এমন journal-এ নগদ line-এর নিট (`Debit − Credit`) শূন্য হলে বাদ (যেমন ব্যাংক থেকে হাতে নগদে স্থানান্তর, দুটোই নগদ-সমতুল্য) |
| 3 | নইলে journal সুষম, তাই `Σ নগদ(Debit − Credit) = Σ অ-নগদ(Credit − Debit)`। প্রতিটা অ-নগদ line-এর `(Credit − Debit)` হলো ওই line-এর নগদে অবদান |
| 4 | সেই অবদান ওই line-এর account-এর `cashflow_class`-এ (revenue বা expense হলে ফাঁকা মানে Operating) যোগ |
| 5 | Presentation: প্রতিটা ভাগের ভেতরে account (বা group) ধরে লাইন, আগমন (ধনাত্মক) ও নির্গমন (ঋণাত্মক) আলাদা |

**Indirect পদ্ধতি (`cashflow_method = 'indirect'`):**

| অংশ | হিসাব |
| --- | --- |
| Operating | নিট লাভ (P3-05-এর সময়ের) + নগদ নয় এমন Operating-ভাগের `asset`-এর `−Δbalance` + Operating-ভাগের `liability` ও `equity`-র `+Δbalance` |
| Investing | Investing-ভাগের `asset`-এর `−Δbalance` + Investing-ভাগের `liability`, `equity`-র `+Δbalance` |
| Financing | Financing-ভাগের `liability`, `equity`-র `+Δbalance` + Financing-ভাগের `asset`-এর `−Δbalance` |

`Δbalance` = `balancesAsOf(to) − balancesAsOf(from − 1)` ওই account-এর। Depreciation-এর মতো নগদহীন খরচ আলাদা flag ছাড়াই ধরা পড়ে: জমা অবচয় (accumulated depreciation) account Operating ভাগে থাকলে তার balance কমা (Credit বাড়া) নিজেই নিট লাভে যোগ হয়ে ফেরে। তাই প্রতিটা অ-নগদ balance-sheet account-এর ভাগ বাধ্যতামূলক (P3-07)। দুই পদ্ধতির মোট একই হবে।

| Field | Specification |
| --- | --- |
| DB flow | (১) Snapshot। (২) `CashflowMappingService::missing` খালি না হলে `RPT_MAPPING_INCOMPLETE` (তালিকা সহ), report চলে না। (৩) পদ্ধতি অনুযায়ী উপরের ধাপ। (৪) শুরুর ও শেষের নগদ। (৫) তিন ভাগের মোট = নিট পরিবর্তন কি না (RC4-এর অংশ), না মিললে লাল সতর্কতা আর পার্থক্য |
| Direct-এ query | একটা query দিয়ে সংশ্লিষ্ট journal id: `SELECT DISTINCT l.journal_id FROM acc_journal_lines l JOIN acc_journals j ... WHERE l.account_id IN (নগদ account) AND j.status IN ('posted','reversed') AND j.journal_date BETWEEN ? AND ?`। তারপর ওই journal-এর অ-নগদ line পড়ে হিসাব |
| নিয়ম | R3.8, R3.9। কোনো account-এর ভাগ অনুমান করা হয় না |
| UI | সময় বাছাই, পদ্ধতির নাম দেখানো (setting থেকে, এখানে বদলানো যায় না), তিন ভাগ, শুরু ও শেষের নগদ, `নিট পরিবর্তন`, মিল সবুজ বা লাল, প্রতিটা লাইনে click → Direct-এ journal-এর তালিকা, Indirect-এ সংশ্লিষ্ট account-এর GL |
| Permission | `accounting.report.view` |
| Error | `RPT_MAPPING_INCOMPLETE` |
| Audit | `accounting.report.run` |
| Acceptance | AC3.2, AC3.4, AC3.11 |
| Test | নমুনা company-র হাতে করা Cash Flow-এর সাথে মেলানো, দুই পদ্ধতির মোট একই, নগদ থেকে নগদে স্থানান্তর বাদ, মিশ্র journal (এক journal-এ একাধিক ভাগ), ভাগহীন account (`RPT_MAPPING_INCOMPLETE`), reversal-সহ |
| Deliverable | দুই পদ্ধতির service (OD-2 যেটা চায়, (গ) হলে দুটোই), API, UI |
| Sub-tasks | নগদ হিসাব, direct, indirect, মিল পরীক্ষা, API, UI, drill-down, test |

### P3-09: অন্য report

| Field | Specification |
| --- | --- |
| ধরন | `api` + `ui` |
| নির্ভর করে | P3-01, P2-15 |
| অবরুদ্ধ করে | P3-13 |
| উদ্দেশ্য | Account Summary, Opening Balance Report, Audit Report, আর Register-দুটোর পূর্ণ রূপ |

| Report | Contract | কী দেখায় | Data কোথা থেকে |
| --- | --- | --- | --- |
| Account Summary | `GET /accounting/reports/account-summary?from=&to=&group=` | Account ধরে মোট Debit, Credit, শেষের balance | `movements` ও `balancesAsOf` (P3-01) |
| Opening Balance Report | `GET /accounting/reports/opening-balance` | Cutover journal-এর সব line, অবস্থা (Draft, Submitted, Posted), মোট ও পার্থক্য | `acc_journals` (`journal_type = 'opening'`) ও `acc_journal_lines`, Draft-ও দেখায় |
| Audit Report | `GET /accounting/reports/audit?actor=&event=&from=&to=&journal=` | Reversal, adjustment, approval, post, override, setting বদল, period বন্ধ, export | `activity_logs` (`event LIKE 'accounting.%'`, `company_id`)। বড় table-এ `activity_logs`-এর index যথেষ্ট কি না P0-02-এ দেখা, নইলে index যোগ |
| Journal Register (পূর্ণ) | P2-15-এর filter-সহ, এখন export-সহ | সব journal | `acc_journals` |
| Approval Register (পূর্ণ) | P2-15 + export | Pending, Approved, Rejected, Posted | `acc_approval_requests`, `acc_approval_actions` |

| Field | Specification |
| --- | --- |
| নিয়ম | Opening Balance Report শুধু opening ধরনের journal। Audit Report শুধু `accounting.audit.view` permission-ওয়ালাকে। সব report company-scoped |
| Permission | `accounting.report.view`, Audit Report-এ `accounting.audit.view` |
| Audit | `accounting.report.run` |
| Acceptance | AC3.6, AC3.9 |
| Test | প্রতিটা report নমুনা data-য় ঠিক, Draft opening-ও দেখায়, Audit Report-এ অন্য company-র event আসে না |
| Deliverable | চারটা API, UI |
| Sub-tasks | প্রতিটা report আলাদা API ও UI, Audit Report-এর index যাচাই, test |

### P3-10: Export (Excel ও PDF)

| Field | Specification |
| --- | --- |
| ধরন | `schema` + `job` + `api` |
| নির্ভর করে | P3-03 থেকে P3-09 |
| অবরুদ্ধ করে | P3-13 |
| উদ্দেশ্য | Screen-এর সংখ্যা হুবহু ফাইলে, queue দিয়ে, দুইবার তৈরি না করে |

**Schema: `acc_report_exports`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | FK | না |  |
| `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` | FK users | না |  |
| `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)`।

| Field | Specification |
| --- | --- |
| Contract | `POST /accounting/reports/{report}/export` (`filters`, `format`), `GET /accounting/exports/{id}` (অবস্থা), `GET /accounting/exports/{id}/download` (company ও permission যাচাই, মেয়াদী লিংক) |
| DB flow: অনুরোধ | `INSERT` row `queued`। Unique ভাঙলে চলমান row-ই ফেরত। Job dispatch |
| DB flow: job | (১) company context। (২) row `FOR UPDATE`, `queued` হলে `processing`। (৩) Screen যে service ও query ব্যবহার করে **হুবহু সেটাই** (R3.10)। (৪) Excel বা PDF তৈরি, টাকার অঙ্ক নির্ধারিত দশমিক সংখ্যায় সংখ্যা হিসেবে (float-এর rounding নয়)। (৫) ফাইল নির্দিষ্ট পথে (`exports/{company}/{id}.{ext}`) লেখা, আবার চললে একই পথ ওভাররাইট, দুটো ফাইল হয় না। (৬) Reconciliation ব্যর্থ থাকলে ফাইলে "মেলেনি" লেখা (R3.9)। (৭) `done`, `row_count`, `finished_at` |
| ব্যর্থতা | তিনবার আবার চেষ্টা (বাড়তে থাকা বিরতি), তারপর `failed`। সর্বোচ্চ row সীমা (config, প্রস্তাব ১ লক্ষ), ছাড়ালে `failed` ও বার্তা, ছোট পরিসরে ভাগ করতে বলা |
| ফাইল রাখার সময় | `expires_at` পর্যন্ত (30 দিন), তারপর রাতের job ফাইল ও row-এর `file_path` মোছে (row থাকে) |
| নিয়ম | `accounting.report.export` আলাদা permission। প্রতিটা export audit-এ |
| Audit | `accounting.report.exported` |
| Acceptance | AC3.9 |
| Test | Screen ও ফাইলের সংখ্যা এক (কয়েকটা report), একই অনুরোধ দুইবার, job মাঝপথে ভাঙা ও আবার চালানো (একটাই ফাইল), অন্য company-র export ডাউনলোড (নিষেধ), মেয়াদ পেরোনো, সীমার বেশি |
| Deliverable | Migration, job, renderer (Excel ও PDF), API, ডাউনলোড UI |
| Sub-tasks | migration, API, job, Excel renderer, PDF renderer, মেয়াদ job, test |

### P3-11: Period বন্ধ ও আবার খোলা

| Field | Specification |
| --- | --- |
| ধরন | `schema` + `service` + `api` + `ui` |
| নির্ভর করে | P1-08, P3-13 (reconciliation), P2-10 (SoD), P3-01 |
| অবরুদ্ধ করে | P3-12, P5-02 |
| OD | **OD-5**, OD-15 |
| উদ্দেশ্য | হিসাবের সময় বন্ধ করা আর কারণ সহ আবার খোলা, দুজনের সম্মতিতে |

**Schema: `acc_period_events`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | FK | না |  |
| `period_id` | FK | না | Composite FK |
| `kind` | VARCHAR(8) | না | `close`, `reopen`, `soft` |
| `status` | VARCHAR(10) | না | `pending`, `approved`, `rejected`, `cancelled` |
| `reason` | VARCHAR(500) | হ্যাঁ | Reopen-এ বাধ্যতামূলক |
| `checks` | JSON | হ্যাঁ | বন্ধের যাচাইয়ের ফল |
| `requested_by` | FK users | না |  |
| `decided_by` | FK users | হ্যাঁ |  |
| `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 আর বদলানো বা মোছা যায় না (সিদ্ধান্ত হয়ে গেলে অপরিবর্তনীয়)।

| Field | Specification |
| --- | --- |
| Soft close | `POST /accounting/periods/{id}/soft-close` (`accounting.period.manage`), অনুমোদন ছাড়া। Period `open` থেকে `soft_closed`। `kind = 'soft'`, সঙ্গে সঙ্গে `approved` |
| DB flow: বন্ধের অনুরোধ | (১) transaction। (২) period `FOR UPDATE`। (৩) অবস্থা `open` বা `soft_closed`, নইলে `JRN_STATE_INVALID`। (৪) `period_close_checks` setting-এর যাচাই চালানো: `no_pending_journals` (`SELECT ... FROM acc_journals WHERE company_id = ? AND journal_date BETWEEN start AND end AND status IN ('submitted','approved')` খালি, আর `draft`, `rejected` থাকলে সতর্কতা শুধু), `tb_balanced` (RC1, period-শেষ তারিখে), `recon_pass` (RC1 থেকে RC7, আর RC8 যদি intake চালু থাকে, RC9)। (৫) যেকোনো একটা ব্যর্থ হলে `PRD_CLOSE_BLOCKED` আর `checks` সহ (কোন journal, কোন পার্থক্য), অনুরোধ তৈরিই হয় না। (৬) সবুজ হলে `acc_period_events` `INSERT` (`pending`, `checks` সংরক্ষিত)। (৭) commit |
| DB flow: অনুমোদন | (১) transaction। (২) event `FOR UPDATE`, `pending` কি না। (৩) `decided_by <> requested_by` আর `accounting.period.approve` permission, নইলে `JRN_SOD_VIOLATION`। (৪) `sod_poster_cannot_sole_close` চালু থাকলে: ওই period-এর posted journal-এর **সব** `posted_by` সেই একই ব্যক্তি হলে সে একমাত্র অনুমোদনকারী হতে পারে না (যদি অনুরোধকারী ভিন্ন হয় তবু)। এটা `SodGuard` ডাকে। (৫) period `FOR UPDATE`, যাচাই **আবার** চালানো (অনুরোধ আর অনুমোদনের মাঝে নতুন journal এসে থাকতে পারে)। ব্যর্থ হলে event `rejected` (স্বয়ংক্রিয়, কারণ সহ)। (৬) সবুজ হলে period `closed`, `closed_at`, `closed_by`, `version + 1`। (৭) event `approved`। (৮) Audit। (৯) commit |
| DB flow: আবার খোলা | অনুরোধ (`reason` বাধ্যতামূলক, `accounting.period.manage`) ও অনুমোদন (`accounting.period.approve`, অন্য ব্যক্তি) একই কাঠামোয়। অনুমোদনে period `closed` থেকে `open`, `reopened_at`, `reopened_by`। আংশিক খোলা (শুধু নির্দিষ্ট তারিখ) এই version-এ নেই, পুরো period-ই। সংশোধনের পর আবার বন্ধ করতে হয় (নতুন close অনুরোধ) |
| নিয়ম | R3.13, R3.14। আগের period বন্ধ না হলে পরেরটা বন্ধ করা যাবে কি না তা এখন বাধ্যতামূলক নয়। Client চাইলে নতুন setting `period_close_sequential`। আগের period আবার খোলা হলে পরের closed period-এর `checks` বাতিল হয় না, কিন্তু সতর্কতা দেখায় |
| UI | Period-এর তালিকা (অবস্থা, কে কখন বন্ধ করেছে), "বন্ধ করুন" যাচাইয়ের ফল সহ (কোনটা সবুজ, কোনটা লাল আর কেন), অনুমোদনের অপেক্ষায় তালিকা, "আবার খুলুন" কারণের ঘর সহ, soft close বাটন |
| Error | `PRD_CLOSE_BLOCKED`, `PRD_REOPEN_DENIED`, `JRN_SOD_VIOLATION`, `JRN_STATE_INVALID` |
| Audit | `accounting.period.close_requested`, `.closed`, `.reopen_requested`, `.reopened` |
| Acceptance | AC3.7 |
| Test | যাচাই ব্যর্থ হলে বন্ধ হয় না, অনুরোধ ও অনুমোদনের মাঝে নতুন journal এলে স্বয়ংক্রিয় reject, নিজের অনুরোধ নিজে অনুমোদন (নিষেধ), SoD-poster, কারণ ছাড়া reopen, দুজন একসাথে একই period-এ অনুরোধ, soft close |
| Deliverable | Migration ও trigger, service, API, UI |
| Sub-tasks | migration, soft close, close অনুরোধ, close অনুমোদন (পুনঃযাচাই), reopen, UI, test |

### P3-12: Period lock-এর কঠোর রূপ (PeriodGuard)

| Field | Specification |
| --- | --- |
| ধরন | `service` |
| নির্ভর করে | P3-11, P2-12 |
| অবরুদ্ধ করে | P3-14 |
| OD | **OD-5**, OD-15 |
| উদ্দেশ্য | Phase 2-এর hook-কে পূর্ণ করা: বন্ধ period-এ কী হবে তা `period_lock_mode` ঠিক করে |

| Period-এর অবস্থা | `period_lock_mode = off` | `warn` | `strict` |
| --- | --- | --- | --- |
| `open` | অনুমোদিত | অনুমোদিত | অনুমোদিত |
| `soft_closed` | অনুমোদিত | অনুমোদিত + সতর্কতা + audit | অনুমোদিত + সতর্কতা + audit (শুধু `journal.post` permission-ওয়ালার জন্য, নইলে `JRN_PERIOD_LOCKED`) |
| `closed` | অনুমোদিত | অনুমোদিত + সতর্কতা + audit | `JRN_PERIOD_LOCKED` |

| Field | Specification |
| --- | --- |
| কোথায় ডাকে | (১) **Submit validator** (P2-03): `strict` মোডে `closed` period হলে Submit-এই `JRN_PERIOD_LOCKED`, যাতে Maker দেরিতে না জানে। (২) **Post** (P2-12 ধাপ ৯): আবার, কারণ Submit-এর পরে period বন্ধ হয়ে থাকতে পারে। (৩) Reversal-এর তারিখ ঠিক করার সময় (P2-13): `reversal_default_date` অনুযায়ী পাওয়া তারিখ `strict` ও `closed` period-এ পড়লে `JRN_PERIOD_LOCKED`, তখন Maker খোলা period-এর তারিখ বেছে নেয় (বা Admin period আবার খোলে) |
| DB flow | `PeriodResolver::forDate` → period-এর `status` পড়া। Post-এর ক্ষেত্রে period row `FOR SHARE` (বন্ধ করার transaction আর এই Post একসাথে চললে একজন অপেক্ষা করে, ফলে বন্ধ হওয়ার ঠিক মুহূর্তে Post ফসকে ঢোকা অসম্ভব) |
| Audit | `accounting.period.locked_post_attempt` (সফল, সতর্কতা সহ বা ব্যর্থ, সব চেষ্টা) |
| নিয়ম | R3.15। `period_lock_mode` বদলালে আগে Post হওয়া কিছু প্রভাবিত হয় না |
| Error | `JRN_PERIOD_LOCKED` |
| Acceptance | AC3.8 |
| Test | তিন mode × তিন অবস্থা (নয়টা জোড়া), Post আর period বন্ধের একসাথে চলা (কোনো journal বন্ধ period-এ ঢোকে না), Submit-এ আগাম আটকানো, reversal-এর তারিখ বন্ধ period-এ |
| Deliverable | `PeriodGuard` পূর্ণ, test |
| Sub-tasks | guard নিয়ম, Submit hook, Post-এর lock, reversal hook, test |

### P3-13: Reconciliation framework

| Field | Specification |
| --- | --- |
| ধরন | `schema` + `service` + `job` + `api` + `ui` |
| নির্ভর করে | P3-03, P3-04, P3-05, P3-06, P3-08 |
| অবরুদ্ধ করে | P3-11, P3-14, P5-03 |
| উদ্দেশ্য | হিসাবের ভেতরের মিলগুলো স্বয়ংক্রিয়ভাবে বারবার পরীক্ষা, ফল সংরক্ষণ |

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

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | FK | না |  |
| `check_code` | VARCHAR(8) | না | `RC1` থেকে `RC9` |
| `scope` | VARCHAR(10) | না | `as_of`, `period` |
| `as_of_date` | DATE | না |  |
| `period_id` | FK | হ্যাঁ |  |
| `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` | FK users | হ্যাঁ | 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 করে, সবসময় সব নয় (নিচে) |

| Field | Specification |
| --- | --- |
| DB flow | প্রতিটা check একটা আলাদা class, নিজের snapshot transaction-এ। ফল `acc_reconciliation_runs`-এ `INSERT` (একটা run-এ ৯টা row, একই `started_at`)। `error` মানে check নিজেই চলেনি (exception), `fail` মানে অমিল পাওয়া গেছে |
| RC9 কৌশল | Posted journal অনেক হতে পারে, তাই রাতে একটা period করে ঘোরানো (সব period ক্রমে, সপ্তাহে একবার সব)। চলতি আর আগের period প্রতিদিন। অমিল পাওয়া journal-এর id `detail`-এ |
| কখন চলে | (১) হাতে: `POST /accounting/reconciliation/run`। (২) Period বন্ধের অনুরোধ ও অনুমোদনের সময় (P3-11)। (৩) রাতে সব সক্রিয় company-র জন্য (job, company context সহ)। (৪) প্রতিটা report চালানোর সময় সংশ্লিষ্ট check-গুলো (RC1 থেকে RC5) শুধু ফলের পাশে দেখানোর জন্য, সংরক্ষণ ছাড়া |
| নিয়ম | R3.9। কোনো check ব্যর্থ হলে `ReconciliationFailed` event, যা P5-03 alert-এ বাঁধা। Report আটকায় না |
| Contract | `POST /accounting/reconciliation/run`, `GET /accounting/reconciliation/latest`, `GET /accounting/reconciliation/runs?check=&from=&to=` |
| UI | Dashboard: প্রতিটা check-এর সর্বশেষ ফল (সবুজ, লাল, ধূসর), কবে চলেছে, কে চালিয়েছে, পার্থক্য, অমিলের তালিকায় click → সংশ্লিষ্ট account বা journal। "এখন চালান" বাটন |
| Permission | `accounting.reconcile.run` |
| Audit | `accounting.recon.run` |
| Acceptance | AC3.1, AC3.3, AC3.4, AC3.10 |
| Test | প্রতিটা check-এর জন্য সুস্থ data-য় `pass`, ইচ্ছা করে ভাঙা data-য় `fail` (যেমন DB guard সাময়িক বন্ধ করে একটা line বদলে দেওয়া, শুধু test পরিবেশে, তারপর RC9 আর RC6 ধরে কি না), `error` পথ, রাতের job, RC9-এর batch |
| Deliverable | Migration, আটটা check class (RC1 থেকে RC7, RC9; RC8 Phase 4-এ), job, API, dashboard |
| Sub-tasks | migration, RC1 থেকে RC7, RC9, job, API, dashboard, test |

### P3-14: Performance ও Phase 3-এর test suite

| Field | Specification |
| --- | --- |
| ধরন | `test` + `ops` |
| নির্ভর করে | P3-01 থেকে P3-13 |
| অবরুদ্ধ করে | P3-15, Phase 4 শুরু |
| উদ্দেশ্য | সব report-এর AC প্রমাণ, আর বড় data-তেও গ্রহণযোগ্য গতি |

**Performance পরিমাপ।** Test পরিবেশে (কখনো production নয়) একটা seeder command বাস্তবসম্মত বড় data বানায়। **নিচের সংখ্যাগুলো প্রস্তাব, Phase শুরুতে client ও tech lead নিশ্চিত করবে** (AC3.12-এ "নির্ধারিত আকারের data" বলা আছে)।

| বিষয় | প্রস্তাবিত data | প্রস্তাবিত সীমা |
| --- | --- | --- |
| Data আকার | ৫ বছর, ৫০০ account, ১০ লক্ষ journal line, ৬০টা period | — |
| Trial Balance (এক বছর) | উপরের data | কয়েক সেকেন্ডের মধ্যে |
| Balance Sheet | উপরের data | কয়েক সেকেন্ডের মধ্যে |
| General Ledger, ১ লক্ষ row-এর account, এক পৃষ্ঠা | উপরের data | এক সেকেন্ডের কাছাকাছি |
| Cash Flow | উপরের data | কয়েক সেকেন্ডের মধ্যে |
| Export, ১ লক্ষ row | উপরের data | queue-তে কয়েক মিনিট |

প্রতিটা প্রধান query-র `EXPLAIN` দেখে index ঠিক ব্যবহার হচ্ছে কি না যাচাই। সীমা ছাড়ালে P3-01-এর contingency (line-এ `entry_date` কপি বা OD-14-এর Option B) ও তা সিদ্ধান্তের জন্য tech lead-এর কাছে।

| AC | কী | Test class (প্রস্তাব) |
| --- | --- | --- |
| AC3.1 | TB ও GL posted data-র সাথে মেলে | `LedgerReconciliationTest` (RC1, RC2, RC6 স্বাধীন SQL-এর সাথে) |
| AC3.2 | P&L, BS, CF অনুমোদিত mapping অনুযায়ী | `StatementCorrectnessTest` (হাতে করা সংখ্যার সাথে) |
| AC3.3 | BS সমীকরণ | `BalanceSheetEquationTest` (RC3) |
| AC3.4 | CF-এর নগদ BS-এর সাথে | `CashFlowReconciliationTest` (RC4) |
| AC3.5 | Report শুধু ledger-এর অবস্থার journal দেখায় | `LedgerStatusFilterTest` (Draft, submitted, approved, rejected, cancelled আসে না, posted ও reversed আসে) |
| AC3.6 | Company isolation report ও export-এও | `ReportIsolationTest` |
| AC3.7 | Period বন্ধ ও খোলার নিয়ম | `PeriodCloseTest` |
| AC3.8 | Lock mode | `PeriodGuardMatrixTest` |
| AC3.9 | Export screen-এর সাথে মেলে | `ExportParityTest` |
| AC3.10 | Cache ভুল সংখ্যা দেয় না | `CacheStalenessTest` (RC7) |
| AC3.11 | ভাগহীন account-এ পরিষ্কার error | `CashflowMappingTest` |
| AC3.12 | বড় data-য় গতি | `ReportPerformanceTest` (CI-তে নয়, আলাদা চালানো) |
| AC3.13 | বিদ্যমান ERPFlow অক্ষত | সম্পূর্ণ বিদ্যমান suite |

**Golden suite** (Blueprint ১২.২)-এর ৭টা পর্যন্ত এই Phase-এ সবুজ। সব report test MySQL-এ।

| Field | Specification |
| --- | --- |
| Deliverable | Seeder, performance রিপোর্ট ও সিদ্ধান্ত, সব test class |
| Sub-tasks | seeder, EXPLAIN যাচাই, প্রতিটা test class, performance রিপোর্ট |

### P3-15: Accountant-এর নমুনা report pack ও সই

| Field | Specification |
| --- | --- |
| ধরন | `gov` |
| নির্ভর করে | P3-14 |
| অবরুদ্ধ করে | Phase 4 শুরু, P5-01 |
| উদ্দেশ্য | Client-এর accountant নিজে হিসাব মিলিয়ে report-গুলো মেনে নেওয়া |
| কাজ | (১) একটা নমুনা company তৈরি, একটা পূর্ণ আর্থিক বছরের বাস্তবসম্মত journal (আয়, খরচ, বেতন, ভাড়া, সম্পদ কেনা, ঋণ, reversal, adjustment সহ)। (২) সব report চালানো: TB, GL, P&L, BS, Cash Flow, Account Summary। (৩) Accountant তাঁর নিজের হাতে বা অন্য tool-এ একই data-র হিসাব করে মেলানো। (৪) অমিল পেলে কারণ খোঁজা, ঠিক করা, আবার। (৫) সই করা নমুনা pack |
| শর্ত | OD-1, OD-2, OD-5, OD-14, OD-15, OD-16, OD-20, OD-23 লিখিতভাবে বন্ধ |
| Deliverable | সই করা নমুনা report pack, বন্ধ হওয়া OD-র তালিকা |
| Acceptance | AC3.2, Phase 3 exit criteria (Blueprint ৮.১০) |

## ৭. Phase 4 task cards: Finance Event Intake ও Asset Pilot

Phase 4 শুরু হয় শুধু Phase 3 পুরো সবুজ আর client-এর লিখিত change request অনুমোদনের পরে। এই Phase-এ কিছুই নিজে নিজে post হয় না: Asset-এর write-off event থেকে শুধু একটা Draft Journal তৈরি হয়। ক্রম: অনুমোদন ও সিদ্ধান্ত → inbox → consumer → Asset reader → mapping → Draft তৈরি → Asset-এ ফেরত → approval → ops screen → RC8 → flag → test → runbook।

### P4-01: Change request অনুমোদন ও সিদ্ধান্ত বন্ধ

| Field | Specification |
| --- | --- |
| ধরন | `gov` |
| নির্ভর করে | P3-15 |
| অবরুদ্ধ করে | পুরো Phase 4 |
| OD | OD-18, OD-3, OD-10, OD-11, OD-21, OD-22 |
| উদ্দেশ্য | মূল scope-এ Fixed Assets আলাদা workflow ও অন্য module থেকে automatic posting দুটোই বাদ, তাই এই pilot-এর জন্য client-এর লিখিত অনুমোদন ছাড়া কোনো code লেখা যাবে না |

| কাজ | কে | Deliverable |
| --- | --- | --- |
| Change request (OD-18) সই করানো। যা বদলাচ্ছে: Asset-এর write-off থেকে Draft Journal নিজে তৈরি হবে (post নয়)। যা বদলাচ্ছে না: Asset-এর নিয়ম, বিদ্যমান invoice ও payment | Client management | সই করা নথি |
| OD-10: write-off-এর Debit account, Credit account, আর `amount` কি journal-এর একমাত্র amount (accumulated depreciation ছাড়া) | Accountant | সিদ্ধান্ত নথি ও mapping-এর মান |
| OD-3: Asset-এর currency আর company-র base currency মিলবে কি, না মিললে কী | Management ও tech lead | সিদ্ধান্ত নথি |
| OD-11: `accepted` মানে Draft তৈরি, নাকি Post | Tech lead ও accountant | সিদ্ধান্ত নথি |
| OD-21: `effective_date` ও `schema_version`: Asset-এর outbox-এ যোগ হবে, নাকি Accounting-এর adapter write-off source থেকে পড়বে | Tech lead | সিদ্ধান্ত নথি |
| OD-22: সম্পন্ন write-off পরে বাতিল বা সংশোধিত হলে কী | Accountant ও tech lead | সিদ্ধান্ত নথি |

| Field | Specification |
| --- | --- |
| Acceptance | ছয়টাই লিখিতভাবে বন্ধ |
| প্রভাব | OD-21 (ক) হলে P4-07 Asset-এ একটা ছোট migration ও writer-এর পরিবর্তন করে। (খ) হলে P4-04 Asset-এর `asset_write_offs` table পড়ে আর Asset অপরিবর্তিত। OD-11 (খ) (Post হলে `accepted`) হলে P4-07 Asset-এর status বদল Post-এর পরে করে, আর Draft অনেকক্ষণ আটকে থাকলে Asset-এ event `pending` দেখায়। OD-22 (খ) বা (গ) হলে Asset-এ আরও কাজ, এই Pack-এ নেই |

### P4-02: Finance Event Inbox

| Field | Specification |
| --- | --- |
| ধরন | `schema` + `service` |
| নির্ভর করে | P4-01, P1-12, P2-12 |
| অবরুদ্ধ করে | P4-03, P4-05, P4-06, P4-09 |
| উদ্দেশ্য | অন্য module থেকে আসা event-এর একটা সাধারণ গ্রহণের জায়গা, source-নিরপেক্ষ |

**Schema: `acc_finance_event_inbox`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | FK | না |  |
| `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)।

| Field | Specification |
| --- | --- |
| DB flow: গ্রহণ | `InboxService::receive(SourceEvent)`: (১) transaction। (২) `INSERT ... ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id)`, নতুন হলে `status = 'received'`। (৩) নতুন না হলে row `FOR UPDATE`: `payload_hash` সমান হলে কিছু না বদলে আগের অবস্থা ফেরত (`EVT_DUPLICATE`)। ভিন্ন হলে `status = 'conflict'` আর `conflict_payload` বসানো, মূল `payload` অক্ষত (`EVT_CONFLICT`)। (৪) Audit। (৫) commit |
| নিয়ম | R4.1 থেকে R4.3। মূল `payload` একবার লিখলে আর বদলায় না (কেবল `conflict` সমাধানে মানুষ গ্রহণ করলে `payload`-এ `conflict_payload` বসে, audit সহ) |
| Error | `EVT_DUPLICATE`, `EVT_CONFLICT`, `EVT_STATE_INVALID` (নতুন) |
| Audit | `accounting.event.received`, `.conflict` |
| Acceptance | AC4.1 |
| Test | একই event ১০ বার, একই identity ভিন্ন payload, প্রতিটা বৈধ ও অবৈধ অবস্থা-জোড়া, দুই connection থেকে একসাথে গ্রহণ |
| Deliverable | Migration, `InboxService`, `InboxTransitionService` |
| Sub-tasks | migration, receive, transition table, conflict, test |

### P4-03: Consumer job

| Field | Specification |
| --- | --- |
| ধরন | `schema` + `job` |
| নির্ভর করে | P4-02, P4-04, P1-12, P1-01 |
| অবরুদ্ধ করে | P4-06, P4-12 |
| উদ্দেশ্য | Source থেকে event তুলে inbox-এ আনা আর প্রক্রিয়া করা, ভেঙে গেলেও ঠিক জায়গা থেকে চলা |

**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-এর |

| Field | Specification |
| --- | --- |
| Schedule | প্রতি মিনিটে `accounting:consume-events` command। তিনটা flag চালু (P4-11) এমন প্রতিটা company-র জন্য `ConsumeFinanceEventsJob(companyId)` dispatch |
| একই company-তে একসাথে দুটো নয় | Job-এর শুরুতে `GET_LOCK('acc_intake_{companyId}', 0)`। না পেলে চুপচাপ বেরিয়ে যায়। শেষে `RELEASE_LOCK` (`finally`-তে) |
| DB flow: এক run | (১) company context (P1-12)। (২) Flag আবার পড়া, বন্ধ হলে থামা। (৩) `acc_intake_heartbeats`-এ শুরু। (৪) **তোলা:** `FinanceEventSource::fetchPending(companyId, intake_batch_size)` (P4-04) থেকে প্রতিটা event `InboxService::receive`। (৫) **প্রক্রিয়া:** `SELECT id FROM acc_finance_event_inbox WHERE company_id = ? AND status = 'received' AND (next_retry_at IS NULL OR next_retry_at <= NOW()) ORDER BY id LIMIT ? FOR UPDATE SKIP LOCKED` দিয়ে row দখল। (৬) **প্রতিটা row নিজের আলাদা transaction-এ** `DraftBuilder::build(row)` (P4-06)। একটা row-এর ব্যর্থতা বাকিদের থামায় না। (৭) প্রতিটা loop-এর শুরুতে flag আর সময়সীমা (একটা run সর্বোচ্চ ৫০ সেকেন্ড) যাচাই। (৮) শেষে heartbeat-এ ফল |
| ফলের শ্রেণিবিভাগ | `EVT_MAPPING_MISSING` → `held_mapping`। `EVT_CURRENCY_MISMATCH` → `event_currency_policy` অনুযায়ী `held_currency` বা `ignored`। `EVT_PAYLOAD_INVALID`, `EVT_COMPANY_MISMATCH`, amount শূন্য বা ঋণাত্মক → `failed` (retry নেই)। অপ্রত্যাশিত exception → `attempts + 1`, `next_retry_at` = `intake_retry_backoff_seconds`-এর `attempts`-তম মান। `attempts >= intake_max_attempts` হলে `failed`, নতুন code `EVT_PROCESSING_ERROR` |
| নিয়ম | R4.11, R4.12। Source থেকে তোলা আবার ঘটলে (Asset-এ ফেরত জানানো ব্যর্থ হয়ে `pending` থেকে গেলে) `receive` শুধু `EVT_DUPLICATE`, ক্ষতি নেই |
| Error | `EVT_PROCESSING_ERROR` (নতুন) |
| Audit | `accounting.event.*` (P4-02, P4-06) |
| Acceptance | AC4.5, AC4.6, AC4.10 |
| Test | দুটো job একসাথে (দ্বিতীয়টা বেরিয়ে যায়), flag চলতে চলতে বন্ধ, একটা row ভাঙলে বাকিরা চলে, মাঝপথে process kill করে আবার চালানো (row `received` থেকে যায়, একটাই Draft হয়), backoff-এর ক্রম, দুই company-র event মেশে না |
| Deliverable | Migration, command, job, heartbeat |
| Sub-tasks | migration, command ও schedule, advisory lock, তোলা, দখল ও প্রক্রিয়া, শ্রেণিবিভাগ, heartbeat, test |

### P4-04: Asset event reader (adapter)

| Field | Specification |
| --- | --- |
| ধরন | `service` |
| নির্ভর করে | P4-01 (OD-21) |
| অবরুদ্ধ করে | P4-03 |
| উদ্দেশ্য | Asset-এর outbox শুধু **পড়া**, Accounting সেখানে কিছুই লেখে না |
| Schema | নতুন table নেই। পড়া হয় `asset_finance_events` (শুধু `event = 'write_off'`, `status = 'pending'`) |

| Field | Specification |
| --- | --- |
| Contract | Interface `FinanceEventSource`: `sourceType(): string`, `fetchPending(companyId, limit): iterable<SourceEvent>`, `fetchOne(sourceId): ?SourceEvent`। DTO `SourceEvent`: `company_id`, `source_type`, `source_id`, `event_type`, `schema_version`, `effective_date`, `amount`, `currency`, `dimensions` (cost center), `payload`, `occurred_at`, `integrity_flags` (যেমন `company_mismatch`) |
| `AssetFinanceEventSource` | `SELECT * FROM asset_finance_events WHERE company_id = ? AND event = 'write_off' AND status = 'pending' ORDER BY id LIMIT ?`। `source_type = 'asset_finance_event'`, `source_id` = ওই row-এর `id`, `schema_version = 1` (adapter-এ ধ্রুবক, OD-21-এর (খ)-তে)। `payload` = row-এর সব column হুবহু |
| `effective_date` | OD-21 (ক) হলে Asset-এর যোগ করা column থেকে। (খ) হলে `reference_id` ধরে Asset-এর write-off source record থেকে তারিখ পড়া (শুধু `SELECT`)। দুটোই না পেলে `effective_date = null`, ফলে `EVT_PAYLOAD_INVALID` |
| Company যাচাই (R4.4) | Event-এর `serialized_asset_id`-র asset আর write-off source-এর `company_id` পড়ে event-এর `company_id`-র সাথে মেলে কি না, না মিললে `integrity_flags.company_mismatch = true` |
| **শুধু-পড়া সুরক্ষা** | এই class-এ কোনো `insert`, `update`, `delete` বা model `save` নেই। একটা static test এই class-এর source-এ এসব নিষিদ্ধ শব্দ খোঁজে। বিদ্যমান Asset code অপরিবর্তিত |
| নিয়ম | `fetchPending` শুধু committed row দেখে (REPEATABLE READ বা READ COMMITTED নির্দিষ্ট করে), Asset-এর নিজের transaction শেষ না হওয়া event আসে না |
| Error | কোনো business error নেই। অবৈধ তথ্য `integrity_flags`-এ |
| Acceptance | AC4.8 |
| Test | Pending ও non-pending row, শুধু `write_off` আসে (`disposal`, `acquisition` নয়), অন্য company-র row নয়, company অমিল ধরা, তারিখ-পাওয়া ও না-পাওয়া, static test |
| Deliverable | Interface, DTO, `AssetFinanceEventSource`, test |
| Sub-tasks | interface ও DTO, adapter, তারিখ নির্ণয়, company যাচাই, static test, test |

### P4-05: Account mapping (schema, API, UI)

| Field | Specification |
| --- | --- |
| ধরন | `schema` + `service` + `api` + `ui` |
| নির্ভর করে | P4-02, P1-06, P1-04, P2-10 |
| অবরুদ্ধ করে | P4-06 |
| OD | **OD-10** |
| উদ্দেশ্য | কোন event-এর কোন অংশ কোন account-এ যাবে, তা Admin বা Finance Manager-এর হাতে |

**Schema: `acc_account_mappings`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | FK | না |  |
| `source_type` | VARCHAR(40) | না |  |
| `event_type` | VARCHAR(40) | না |  |
| `mapping_key` | VARCHAR(40) | না | `debit_account`, `credit_account` |
| `account_id` | FK | না | Composite FK `(company_id, account_id)` |
| `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)`।

| Field | Specification |
| --- | --- |
| প্রয়োজনীয় mapping-এর তালিকা | কোডে `MappingRequirements`: `asset_finance_event` + `write_off` → `debit_account`, `credit_account`। এটাই Setup Checklist-এর `I01` আর `DraftBuilder`-এর ভিত্তি |
| যাচাই | Account ওই company-র, active, leaf (`JRN_ACCOUNT_INVALID`)। সতর্কতা (error নয়): `debit_account` Expense বা Asset-এর বাইরে, `credit_account` Asset-এর বাইরে হলে |
| DB flow: বসানো বা বদলানো | `account_mappings` সংবেদনশীল তালিকায় থাকায় (`sensitive_settings`) আর `settings_require_second_approval` চালু থাকলে সরাসরি লেখা নয়: P1-04-এর change request, key `mapping:asset_finance_event.write_off.debit_account` ধরনের, `proposed_value` = account id। P1-04-এর service-এ একটা `ChangeApplier` রেজিস্ট্রি, যেখানে `mapping:` prefix-এর জন্য এই card একটা applier নিবন্ধন করে (আলাদা approval table লাগে না)। অনুমোদনে: transaction, mapping row `FOR UPDATE`, `version` মেলানো, `UPDATE` বা `INSERT`, audit আগে ও পরে সহ। বন্ধ থাকলে সরাসরি একই লেখা |
| বদলের প্রভাব | নতুন event নতুন mapping পায়। আগে তৈরি Draft অপরিবর্তিত (R4.14)। `held_mapping` অবস্থার event আপনা-আপনি ছাড়া পায় না: UI সংরক্ষণের পর জানায় "৩টা event আটকে আছে, এখন ছাড়বেন?" (P4-09-এর release) |
| SoD | যে mapping বদলেছে তার নাম `acc_account_mappings.updated_by`-এ। Draft তৈরির সময় সেই মান journal-এর সাথে জোড়া `mapping_snapshot`-এ সংরক্ষণ (P4-06), যাতে পরে mapping আবার বদলালেও SoD-র তথ্য ঠিক থাকে |
| Contract | `GET /accounting/mappings`, `PUT /accounting/mappings/{source}/{event}/{key}` (`account_id`, `version`, `reason`), `POST .../deactivate` |
| UI | প্রয়োজনীয় mapping-এর তালিকা (source ও event ধরে), প্রতিটার অবস্থা (বসানো, বাকি), account picker (leaf ও active), সতর্কতা, history drawer, অপেক্ষমাণ অনুমোদনের অনুরোধ, আটকে থাকা event ছাড়ার প্রস্তাব |
| Permission | `accounting.mapping.manage` |
| Error | `JRN_ACCOUNT_INVALID`, `JRN_CONFLICT`, `SET_STALE_REQUEST` |
| Audit | `accounting.mapping.changed` |
| Acceptance | AC4.2, AC4.11 |
| Test | বৈধ ও অবৈধ account, দ্বিতীয় অনুমোদন ছাড়া বদল হয় না, নিজে অনুমোদন (নিষেধ), বদলের পর পুরোনো Draft অপরিবর্তিত, আটকে থাকা event-এর সংখ্যা ঠিক |
| Deliverable | Migration, service, `ChangeApplier` রেজিস্ট্রি, API, UI |
| Sub-tasks | migration, requirements, validator, applier, API, UI, test |

### P4-06: Event থেকে Draft Journal

| Field | Specification |
| --- | --- |
| ধরন | `schema` + `service` |
| নির্ভর করে | P4-03, P4-05, P2-02, P2-03 |
| অবরুদ্ধ করে | P4-07, P4-08, P4-10 |
| OD | **OD-10** (লাইনের নিয়ম), OD-3 (currency) |
| উদ্দেশ্য | `received` event থেকে একটা সম্পূর্ণ, ভুল-মুক্ত Draft। নইলে কিছুই নয় |

**Schema: `acc_finance_event_journal_links`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | FK | না |  |
| `inbox_id` | FK | না | Composite FK `(company_id, inbox_id)` |
| `journal_id` | FK | না | Composite FK `(company_id, journal_id)` |
| `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 |

| Field | Specification |
| --- | --- |
| DB flow: `DraftBuilder::build(inboxRow)` (একটাই transaction, P4-03 দখল করা row-এ) | (১) row `FOR UPDATE`, `status = 'received'` না হলে বেরিয়ে যাওয়া। (২) Payload যাচাই: `effective_date`, `amount > 0`, `currency` আছে, `integrity_flags` ফাঁকা। নইলে `EVT_PAYLOAD_INVALID` বা `EVT_COMPANY_MISMATCH` → `failed`। (৩) `currency = base_currency` না হলে `EVT_CURRENCY_MISMATCH`। (৪) `MappingRequirements` অনুযায়ী দুটো mapping পড়া, active ও বৈধ। না থাকলে `EVT_MAPPING_MISSING` (কোন key বাকি তা `error_message`-এ) → `held_mapping`। (৫) লাইন বানানো। (৬) `JournalService::createDraft`: `journal_type = 'event_generated'`, `journal_date = effective_date` (তৈরির তারিখ নয়, R4.8), `narration` = "Asset write-off #" + source\_id, `reference_no` = `AWO-` + source\_id, `source_type`, `source_id`, `source_event` বসানো, `created_by` null। `SaveValidator` চলে। Account অবৈধ বেরোলে (যেমন mapping-এর account এখন inactive) `EVT_MAPPING_MISSING` ধরনে `held_mapping`। (৭) `INSERT` journal-এ `uq_acc_journals_source_active` ভাঙলে (দুই worker একসাথে): আগের journal পড়ে সেটাই ব্যবহার, নতুন কিছু না বানিয়ে। (৮) `INSERT` link (`mapping_snapshot` সহ)। (৯) `InboxTransitionService`: `received` → `draft_created`। (১০) Audit। (১১) commit |
| Draft বাতিল হলে | Journal `cancelled` হলে একটা listener (`JournalCancelled`) transaction-এ link `is_live = false` করে আর inbox `draft_created` → `received` (R4.10)। এতে event আবার প্রক্রিয়া হয় আর নতুন Draft বানায়। চাইলে মানুষ আগে `ignore` করতে পারে |
| Draft সংশোধন | Event-তৈরি Draft যেকোনো `journal.create`-ওয়ালা সংশোধন করতে পারে (P2-02-এ সংশোধিত নিয়ম)। amount বদলালে audit-এ আগে ও পরে, আর UI-তে "event-এর amount থেকে আলাদা" চিহ্ন। Post হওয়ার পর RC8 (P4-10) পার্থক্য ধরে |
| নিয়ম | R4.1 থেকে R4.10, R4.14। অসম্পূর্ণ journal কখনো নয়। নিজে post কখনো নয় (AC4.4) |
| Error | `EVT_PAYLOAD_INVALID`, `EVT_COMPANY_MISMATCH`, `EVT_CURRENCY_MISMATCH`, `EVT_MAPPING_MISSING` |
| Audit | `accounting.event.draft_created`, `.held`, `.failed` |
| Acceptance | AC4.2, AC4.3, AC4.4, AC4.7 |
| Test | স্বাভাবিক Draft, প্রতিটা বাদ-যাওয়া mapping, inactive account, currency অমিল, দুই worker একই row-এ, Draft বাতিলের পর আবার Draft, amount শূন্য, link ও `mapping_snapshot`-এর সঠিকতা, Draft কখনো `posted` নয় |
| Deliverable | Migration, `DraftBuilder`, listener |
| Sub-tasks | migration, payload যাচাই, currency, mapping, লাইন, `createDraft`, concurrency পথ, link, `JournalCancelled` listener, test |

### P4-07: Asset-এ ফেরত জানানো ও Asset-এর ছোট পরিবর্তন

| Field | Specification |
| --- | --- |
| ধরন | `service` + `job` (Accounting ও Asset দুই দিকে) |
| নির্ভর করে | P4-06, P4-01 (OD-11, OD-21) |
| অবরুদ্ধ করে | P4-12 |
| উদ্দেশ্য | Draft তৈরি হলে (বা OD-11 অনুযায়ী Post হলে) Asset-এর event `accepted` করা, Accounting সরাসরি Asset-এর table না ছুঁয়ে |

**Accounting-এর দিক। Schema যোগ (expand migration)** `acc_finance_event_inbox`-এ: `acknowledged_at TIMESTAMP NULL`, `ack_attempts SMALLINT UNSIGNED NOT NULL DEFAULT 0`, `ack_error VARCHAR(500) NULL`।

| Field | Specification |
| --- | --- |
| Interface | Accounting-এ `FinanceEventAcknowledger`: `accepted(sourceId): void` (idempotent)। `source_type` অনুযায়ী container থেকে সঠিক implementation |
| Job `AcknowledgeEventJob(inboxId)` | (১) company context। (২) inbox `FOR UPDATE`, `status = 'draft_created'` আর `acknowledged_at IS NULL` না হলে বেরিয়ে যাওয়া। (৩) **OD-11 (ক):** commit-এর পর (P4-06-এর `afterCommit`)। **(খ):** `JournalPosted` event-এ, সংশ্লিষ্ট link ধরে। (৪) `acknowledger->accepted(source_id)`। (৫) সফল হলে `acknowledged_at = NOW()`। ব্যর্থ হলে `ack_attempts + 1`, `ack_error`, backoff সহ আবার। ব্যর্থতা journal বা inbox-এর অবস্থা বদলায় না (R4.13) |

**Asset-এর দিক (Asset module-এ, শুধু যেটুকু দরকার):**

| পরিবর্তন | বিবরণ |
| --- | --- |
| `AssetFinanceEventAcknowledger` | Asset-এর নতুন service, Accounting-এর interface বাস্তবায়ন করে। `accepted(eventId)`: `UPDATE asset_finance_events SET status = 'accepted' WHERE id = ? AND status = 'pending'`। Affected rows 0 হলে আগে থেকেই `accepted` (idempotent, ঠিক আছে) বা অন্য অবস্থা (সতর্কতা log)। কোনো journal id বা GL column যোগ হয় না (Asset-এর outbox ledger-সদৃশ নয়, এটা ইচ্ছাকৃত) |
| OD-21 (ক) হলে | Migration: `asset_finance_events.effective_date DATE NULL`, `schema_version SMALLINT UNSIGNED NOT NULL DEFAULT 1`। `AssetFinanceEventWriter::write` write-off-এর ক্ষেত্রে `effective_date` বসায় (write-off-এর আসল তারিখ)। বিদ্যমান `pending` row-এর জন্য একবার চালানো idempotent backfill (source record থেকে তারিখ)। Unique key আর বাকি schema অপরিবর্তিত |
| ধ্রুব নিয়ম | Asset-এর write-off-এর business rule, executor-এর আচরণ আর বিদ্যমান test অপরিবর্তিত। এই পরিবর্তনের জন্য Asset-এর বিদ্যমান test suite সবুজ আর একটা আচরণ-তুলনা test (পরিবর্তনের আগে ও পরে write-off চালিয়ে asset-এর অবস্থা আর outbox-এর row একই, নতুন column বাদে) |

| Field | Specification |
| --- | --- |
| Error | কোনো user-মুখী error নেই (ব্যাকগ্রাউন্ড) |
| Audit | `accounting.asset_status.sent`, `.failed` |
| Acceptance | AC4.8, AC4.13 |
| Test | সফল `accepted`, দুইবার (idempotent), Asset-এ ব্যর্থতা (journal ঠিক থাকে, পরে আবার চেষ্টায় সফল), Asset-এর বিদ্যমান suite, আচরণ-তুলনা test, OD-11-এর দুই রূপ |
| Deliverable | Interface, job, migration, Asset-এর acknowledger ও (প্রযোজ্য হলে) migration ও writer-এর পরিবর্তন, backfill |
| Sub-tasks | interface, job, acknowledger, OD-21 migration ও writer, backfill, আচরণ-তুলনা test, test |

### P4-08: Event-journal-এর approval ও SoD

| Field | Specification |
| --- | --- |
| ধরন | `service` |
| নির্ভর করে | P4-06, P2-08, P2-10 |
| অবরুদ্ধ করে | P4-12 |
| উদ্দেশ্য | Event-তৈরি journal সাধারণ journal-এর মতোই approval পায়, সাথে mapping-এর SoD |
| কাজ | (১) Approval rule `journal_type = 'event_generated'` (বা `any`) তৈরি করা যায় (P2-06 আগে থেকেই সমর্থন করে)। কোনো rule স্বয়ংক্রিয়ভাবে বানানো হয় না, না থাকলে `APR_NO_RULE`। (২) `SodGuard::assert('approve', actor, journal)`-এ নতুন শাখা: `journal.source_type` থাকলে লিঙ্কের `mapping_snapshot.updated_by` পড়ে, approver সেই ব্যক্তি হলে `sod_mapping_editor_cannot_approve` অনুযায়ী `JRN_SOD_VIOLATION`। (৩) Event-journal-এ Maker = `submitted_by` (কারণ `created_by` null), তাই SoD "Maker নিজে approve নয়" `submitted_by`-র ওপর। (৪) Event-journal Post-এর সময়ও SoD `sod_maker_cannot_post` `submitted_by`-র ওপর |
| DB flow | কোনো নতুন লেখা নেই। পড়া: `acc_finance_event_journal_links.mapping_snapshot`, `acc_journals.submitted_by` |
| Error | `JRN_SOD_VIOLATION`, `APR_NO_RULE` |
| Acceptance | AC4.11 |
| Test | Mapping-বদলকারী ওই journal approve করতে পারে না, setting বন্ধ করলে পারে, `submitted_by`-ভিত্তিক Maker-নিয়ম, rule না থাকলে আটকে |
| Deliverable | `SodGuard`-এর শাখা, test |
| Sub-tasks | guard শাখা, `submitted_by` নিয়ম, test |

### P4-09: Inbox-এর ops screen ও কাজের ইতিহাস

| Field | Specification |
| --- | --- |
| ধরন | `schema` + `service` + `api` + `ui` |
| নির্ভর করে | P4-02, P4-03, P4-05, P4-06 |
| অবরুদ্ধ করে | P4-12 |
| উদ্দেশ্য | আটকে থাকা, ব্যর্থ আর conflict event মানুষ নিজে দেখে সমাধান করবে |

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

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

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

| Field | Specification |
| --- | --- |
| Contract | `GET /accounting/intake/events?status=&source=&error=&from=&to=`, `GET /accounting/intake/events/{id}`, `POST .../{id}/retry`, `.../release`, `.../ignore`, `.../resolve-conflict` (`accept` বা `reject`), `POST /accounting/intake/release-held` (`mapping_key` ধরে একসাথে, যেমন mapping বসানোর পর) |
| DB flow: release বা retry | (১) idempotency। (২) transaction। (৩) inbox row `FOR UPDATE`। (৪) `InboxTransitionService` দিয়ে `received`, `attempts = 0`, `next_retry_at = null`, `error_*` মোছা। যে অবস্থা থেকে সরাসরি বৈধ নয় তাতে `EVT_STATE_INVALID`। (৫) `acc_finance_event_actions` row। (৬) Audit। (৭) commit। আগে থেকেই `received` হলে কিছু না করে সফল (idempotent) |
| DB flow: ignore | একই, কারণ বাধ্যতামূলক, `status = 'ignored'` |
| DB flow: conflict সমাধান | `accept`: transaction-এ `payload` = `conflict_payload`, `payload_hash` নতুন, `conflict_payload = null`, `received`। আগে থেকে Draft থাকলে (`draft_created` থেকে এসে থাকলে) সেটা বাতিল করে নতুনটা বানানো হবে। `reject`: `conflict_payload = null`, আগের অবস্থায় (Draft থাকলে `draft_created`, নইলে `received`) ফেরা, `ignored` নয় |
| UI | (১) গণনা-টাইল: প্রতিটা অবস্থায় কতগুলো। (২) তালিকা (filter সহ), প্রতিটায় কত দিন ধরে আটকে, কারণ। (৩) বিস্তারিত পাতা: payload (সুন্দর করে), error, চেষ্টার সংখ্যা, লিঙ্কযুক্ত journal (লিংক), কাজের ইতিহাস। (৪) বাটন অবস্থা অনুযায়ী (`can()`): Release, Retry, Ignore (কারণ সহ), Conflict সমাধান (পুরোনো ও নতুন payload পাশাপাশি)। (৫) Consumer-এর স্বাস্থ্য-বক্স: সর্বশেষ কখন চলেছে (`acc_intake_heartbeats`), কত পিছিয়ে। (৬) গত ৩০ দিনে ignored event-এর তালিকা (Head of Finance পর্যালোচনার জন্য) |
| Permission | দেখা: `accounting.intake.view`। কাজ: `accounting.intake.manage` |
| Error | `EVT_STATE_INVALID`, `ACC_FORBIDDEN` |
| Audit | `accounting.event.retried`, `.released`, `.ignored` |
| Acceptance | AC4.2, AC4.10 |
| Test | প্রতিটা বৈধ ও অবৈধ কাজ, `ignore` কারণ ছাড়া, conflict-এর দুই পথ, একই `release` দুইবার, bulk release, অন্য company-র event (নিষেধ) |
| Deliverable | Migration, service, API, UI |
| Sub-tasks | migration, release ও retry, ignore, conflict, bulk release, API, UI চারটা অংশ, test |

### P4-10: Reconciliation RC8 (Event থেকে ledger পর্যন্ত)

| Field | Specification |
| --- | --- |
| ধরন | `service` |
| নির্ভর করে | P4-06, P3-13 |
| অবরুদ্ধ করে | P4-12 |
| উদ্দেশ্য | কোনো event হারায়নি, দুইবার হয়নি, amount বদলে যায়নি তা প্রমাণ |

| উপ-যাচাই | কী মেলায় | ব্যর্থ হলে `detail`-এ |
| --- | --- | --- |
| RC8.1 | প্রতিটা `draft_created` inbox row-এর একটা সক্রিয় link আর একটা অ-বাতিল journal আছে | inbox id-র তালিকা |
| RC8.2 | প্রতিটা `event_generated` journal (পোস্টকৃত) একটা সক্রিয় link-এ জোড়া | journal id-র তালিকা |
| RC8.3 | পোস্টকৃত event-journal-এর `total_debit` = inbox-এর `amount` | (journal, inbox, পার্থক্য) |
| RC8.4 | কোনো inbox identity-র দুটো সক্রিয় journal নেই (DB constraint থাকলেও স্বাধীন যাচাই) | inbox id |
| RC8.5 | Source-এর দিকে: Asset-এ `accepted` অথচ Accounting-এ inbox row বা `draft_created` নেই, বা উল্টো `draft_created` কিন্তু Asset-এ এখনো `pending` আর `ack_attempts` শেষ | (source id, কারণ) |
| RC8.6 (সতর্কতা, `fail` নয়) | `draft_created` Draft যা `monitor_approval_stuck_days`-এর বেশি আটকে | journal id |

RC8.1 থেকে RC8.5-এর যেকোনো একটা ব্যর্থ হলে RC8 `fail`। RC8.6 শুধু `detail.warnings`-এ। P3-13-এর framework-এর একটা check class, একই রাতের job ও dashboard-এ।

| Field | Specification |
| --- | --- |
| DB flow | Snapshot transaction। RC8.1 থেকে RC8.4 শুধু Accounting-এর table-এ join। RC8.5 Asset-এর outbox পড়ে (P4-04-এর reader দিয়ে, আলাদা read-only method `fetchAcceptedIds`)। কোনো লেখা নেই, শুধু ফল `acc_reconciliation_runs`-এ |
| Acceptance | AC4.12 |
| Test | সুস্থ data-য় `pass`, প্রতিটা উপ-যাচাইয়ের জন্য ইচ্ছা করে ভাঙা data-য় `fail` (test পরিবেশে) |
| Deliverable | `RC8` check class, test |
| Sub-tasks | ছয়টা উপ-যাচাই, source-দিকের reader method, framework-এ নিবন্ধন, test |

### P4-11: তিন স্তরের flag ও প্রথমবার চালু

| Field | Specification |
| --- | --- |
| ধরন | `service` + `api` + `ui` |
| নির্ভর করে | P1-01, P1-05, P1-04, P4-05 |
| অবরুদ্ধ করে | P4-03 (চালানোর জন্য), P4-12 |
| উদ্দেশ্য | Intake চালু ও বন্ধ নিরাপদে, প্রথমবার দুজনের সম্মতিতে |

| Flag | সংরক্ষণ | চালুর শর্ত |
| --- | --- | --- |
| `accounting.enabled` | `Setting` (P1-01) | Setup Checklist `S01`–`S10` সবুজ |
| `event_intake.enabled` | `Setting` | `accounting.enabled` চালু + Checklist `I01`, `I02` সবুজ |
| `event_intake.sources.asset_write_off` | `Setting` | `event_intake.enabled` চালু + `I03` সবুজ + OD-18 অনুমোদিত (ডকুমেন্টে লেখা, system যাচাই করে না) |

| Field | Specification |
| --- | --- |
| Service | `AccountingFeatureService::intakeEnabled(company) = enabled && intake.enabled`, `::sourceEnabled(company, source) = intakeEnabled && sources.x`। Consumer শুধু `sourceEnabled` দেখে (P4-03) |
| DB flow: চালু | `POST /accounting/flags/{flag}` (`enabled = true`, `reason`)। (১) `accounting.enable.manage` permission। (২) Checklist শর্ত (`ChecklistService`, নতুন)। (৩) **প্রথমবার** (এই company-র এই flag কখনো চালু ছিল না): সরাসরি লেখা নয়, P1-04-এর change request (key `flag:event_intake.enabled`), অন্য একজন (`accounting.settings.approve`) অনুমোদন করলে তবে চালু (`ChangeApplier` `flag:` prefix)। পরের বার (বন্ধ করার পর আবার) একজনেই চালাতে পারে। (৪) `Setting` লেখা। (৫) Audit `accounting.flag.enabled` |
| DB flow: বন্ধ | সঙ্গে সঙ্গে, অনুমোদন ছাড়া (জরুরি নিরাপত্তা), `reason` বাধ্যতামূলক। Consumer-এর বর্তমান run পরের loop-এ থামে। আগের Draft, inbox কিছু মোছা হয় না |
| UI | Settings-এর "Event intake" অংশ: তিন flag-এর অবস্থা, প্রতিটার শর্ত (সবুজ বা লাল তালিকা), "চালু" বাটন শর্ত পূরণে সক্রিয়, "বন্ধ" সবসময় সক্রিয় |
| Permission | `accounting.enable.manage` (চালু বা বন্ধ), `accounting.settings.approve` (প্রথমবারের অনুমোদন) |
| Error | `SET_INCOMPLETE`, `ACC_FORBIDDEN`, `JRN_SOD_VIOLATION` |
| Audit | `accounting.flag.enabled`, `.disabled` |
| Acceptance | AC4.5 |
| Test | তিন flag-এর সব ২³ সমন্বয়ে consumer কী করে, শর্ত পূরণ না হলে চালু হয় না, প্রথমবার নিজে অনুমোদন (নিষেধ), বন্ধ করলে চলমান run থামে, বন্ধের পর data অক্ষত |
| Deliverable | Service, API, UI |
| Sub-tasks | service, চালুর শর্ত, প্রথমবারের অনুমোদন (applier), বন্ধ, UI, test |

### P4-12: Phase 4-এর test suite

| Field | Specification |
| --- | --- |
| ধরন | `test` |
| নির্ভর করে | P4-02 থেকে P4-11 |
| অবরুদ্ধ করে | P4-13, Phase 5 শুরু |

| AC | Test class (প্রস্তাব) | মূল দৃশ্য |
| --- | --- | --- |
| AC4.1 | `EventReplayTest` | একই event ১০ বার, একটাই journal |
| AC4.2 | `MappingMissingTest` | অসম্পূর্ণ journal কখনো তৈরি হয় না |
| AC4.3 | `EventTraceabilityTest` | Event থেকে posted line আর উল্টো |
| AC4.4 | `NoAutoPostTest` | Consumer ও `DraftBuilder` থেকে `PostingService`-এ কোনো পথ নেই (static, code-ঘুরে) |
| AC4.5 | `IntakeFlagMatrixTest` | তিন স্তরের সব সমন্বয় |
| AC4.6 | `IntakeIsolationTest` | দুই company-র event মেশে না |
| AC4.7 | `CurrencyHoldTest` | `held_currency` |
| AC4.8 | Asset-এর বিদ্যমান suite আর `AssetBehaviorParityTest` | Asset-এর আচরণ অপরিবর্তিত |
| AC4.9 | সম্পূর্ণ regression | Project, Payroll, invoice, payment অক্ষত |
| AC4.10 | `IntakeFailureInjectionTest` | Draft তৈরির পর ও Asset-এ ফেরতের আগে process kill, তারপর আবার, কিছু হারায় না |
| AC4.11 | `MappingSodTest` | Mapping-বদলকারী approve করতে পারে না |
| AC4.12 | `EventReconciliationTest` | RC8 |
| AC4.13 | `AcknowledgeFailureTest` | ফেরত জানানো ব্যর্থ হলে কিছু নষ্ট হয় না |

আরও একটা পূর্ণ শেষ থেকে শেষ test: Asset-এ আসল write-off চালানো → outbox → consumer → Draft → Submit → approve → Post → TB ও GL-এ আসা → RC8 সবুজ। Golden suite-এর ৮টা সবকটা এই Phase-এ সবুজ। সব MySQL-এ।

| Field | Specification |
| --- | --- |
| Deliverable | সব test class, শেষ থেকে শেষ test, test report |
| Sub-tasks | প্রতিটা class, failure-injection helper, শেষ থেকে শেষ test |

### P4-13: Runbook, demo ও release প্রস্তুতি

| Field | Specification |
| --- | --- |
| ধরন | `ops` |
| নির্ভর করে | P4-12 |
| অবরুদ্ধ করে | Phase 5 শুরু |
| কাজ | (১) Runbook-এ Intake অংশ: আটকে থাকা event, ব্যর্থ, conflict, consumer থেমে গেলে, কীভাবে নিরাপদে বন্ধ করা, ফেরত-জানানোর ব্যর্থতা। (২) Accountant demo: স্বাভাবিক write-off, mapping না থাকা ও ঠিক করে ছাড়া, duplicate event, conflict। (৩) Asset-এর ভবিষ্যৎ event (disposal, acquisition)-এর জন্য কী কী লাগবে তার তালিকা (Phase 4-এর বাইরে, শুধু নথি)। (৪) সব flag সব company-তে বন্ধ অবস্থায় release প্রস্তুত, প্রমাণসহ। (৫) Merge gate |
| Deliverable | Runbook-এর Intake অংশ, demo-র মতামত, ভবিষ্যৎ event-এর তালিকা, সই করা merge checklist |
| Acceptance | Phase 4 exit criteria (Blueprint ৯.১০) |

## ৮. Phase 5 task cards: UAT ও Controlled Release

Phase 5-এ নতুন feature আসে না। কাজ হলো প্রমাণ, মহড়া, নজরদারি আর ধাপে ধাপে চালু। মনে রাখতে হবে: `development`-এ merge মানে production-এ deploy, তাই merge আর "চালু করা" আলাদা কাজ, আর flag বন্ধ রেখেই merge। ক্রম: UAT → cutover মহড়া → monitoring → rollback → security ও performance → release gate → training → pilot চালু।

### P5-01: UAT pack ও সই

| Field | Specification |
| --- | --- |
| ধরন | `test` + `gov` |
| নির্ভর করে | P3-15, P4-13 |
| অবরুদ্ধ করে | P5-06 |
| OD | OD-17 (কে সই করবে) |
| উদ্দেশ্য | Client-এর accountant ও finance team নিজের হাতে সত্যিকারের পরিস্থিতিতে পুরো system যাচাই করবে |
| পরিবেশ | Staging, production-এর মতো কাঠামো। আসল বা মুখোশ-পরানো data। UAT-এর ফল কোনো DB table-এ নয়, QA tool-এ (এই Pack-এ নতুন table নেই) |

| # | পরিস্থিতি | কভার করে |
| --- | --- | --- |
| U01 | নতুন company চালু: settings, CoA, checklist | AC1.1 থেকে AC1.6 |
| U02 | Opening balance না মিললে আটকায়, মিললে Post | AC1.3, AC2.12 |
| U03 | সাধারণ journal: Draft থেকে Post | AC2.1 থেকে AC2.4 |
| U04 | Amount অনুযায়ী ভিন্ন approval ধাপ, parallel ও sequential | AC2.2 |
| U05 | Reject ও সংশোধন করে আবার পাঠানো | AC2.10 |
| U06 | Delegation ও emergency override ও পর্যালোচনা | AC2.3 |
| U07 | Reversal ও Adjustment | AC2.8 |
| U08 | সব report-এর মিল (TB, GL, P&L, BS, Cash Flow) | AC3.1 থেকে AC3.4 |
| U09 | Period বন্ধ, বন্ধ period-এ Post, আবার খোলা | AC3.7, AC3.8 |
| U10 | Export Excel ও PDF-এর সংখ্যা মেলে | AC3.9 |
| U11 | Asset write-off থেকে Draft: স্বাভাবিক, mapping নেই, duplicate, conflict | AC4.1, AC4.2 |
| U12 | Reconciliation dashboard-এ সব সবুজ, ইচ্ছা করে একটা ভাঙলে লাল | AC3.10, AC4.12 |
| U13 | অনুমতিহীন চেষ্টা: অন্য company, অন্য role | AC1.1, AC2.3 |
| U14 | Flag বন্ধ করলে সব থামে, data অক্ষত | AC1.10, AC4.5 |

| Field | Specification |
| --- | --- |
| কাজ | প্রতিটা পরিস্থিতির ধাপ-ধাপ script লেখা (QA), accountant ও QA একসাথে চালানো, প্রতিটার ফল (পাস, ব্যর্থ), প্রমাণ (screenshot বা export), defect তালিকা। গুরুতর defect থাকলে সংশোধনের পর সংশ্লিষ্ট সব পরিস্থিতি আবার চালানো, আংশিক সই নয় |
| Acceptance | AC5.1, AC5.3 |
| Deliverable | UAT script, ফল ও প্রমাণ, defect তালিকা, সই করা নথি (OD-17-এর ব্যক্তি) |
| Sub-tasks | script ×14, চালানো, defect সংশোধন ও আবার চালানো, সই |

### P5-02: Cutover মহড়া

| Field | Specification |
| --- | --- |
| ধরন | `ops` |
| নির্ভর করে | P3-15, P2-14, P3-11 |
| অবরুদ্ধ করে | P5-06 |
| OD | OD-9 (cutover তারিখ ও কোন কাগজ থেকে মেলানো) |
| উদ্দেশ্য | Production-এ যা করা হবে, তার ঠিক একই রূপে আগে staging-এ করে সময় আর ফাঁক মাপা |

| ধাপ | কী | মাপ |
| --- | --- | --- |
| 1 | Staging-এ একটা company-র Setup Checklist সম্পূর্ণ করা | কত সময় লাগল, কী ঠেকল |
| 2 | আসল (বা আসলের মতো) CoA বানানো |  |
| 3 | Opening balance হাতে ঢোকানো (Opening Balance গ্রিড) | account কত, কত সময় |
| 4 | Submit, approve, post |  |
| 5 | Trial Balance বের করে পুরোনো system-এর সাথে account ধরে মেলানো | কতগুলো অমিল |
| 6 | অমিল থাকলে কারণ বের করে ঠিক করা (opening-এ ভুল, CoA ম্যাপিং ভুল ইত্যাদি) |  |
| 7 | Balance Sheet মেলানো (Asset = Liability + Equity) |  |

| Field | Specification |
| --- | --- |
| নিয়ম | Production-এ কোনো লেখা নেই। Staging-এর data মুছে আবার শুরু করে অন্তত দুইবার চালানো (দ্বিতীয়বার দ্রুত ও নির্ভুল হওয়া উচিত)। Opening Balance ভুল হলে Post-এর পরে আর বদলানো যায় না, তাই মহড়াই একমাত্র সুযোগ |
| Acceptance | AC5.2 |
| Deliverable | মহড়ার রিপোর্ট (ধাপ ধরে সময়, অমিল ও তার কারণ, চূড়ান্ত তুলনা), Production cutover-এর ধাপ-ধাপ checklist |
| Sub-tasks | ধাপ ১ থেকে ৭, দ্বিতীয় মহড়া, রিপোর্ট, production checklist |

### P5-03: Monitoring ও alert

| Field | Specification |
| --- | --- |
| ধরন | `schema` + `job` + `ops` |
| নির্ভর করে | P3-13, P4-03, P4-10, P2-15 |
| অবরুদ্ধ করে | P5-08 |
| উদ্দেশ্য | কিছু ভাঙলে মানুষ জানার আগে system জানাবে, কিন্তু একই alert বারবার ঝড় তুলবে না |

**Schema: `acc_alert_state`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | FK | না |  |
| `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` |

| Field | Specification |
| --- | --- |
| DB flow | `accounting:health-check` প্রতি পাঁচ মিনিটে, প্রতিটা চালু company-র জন্য company context-এ একটা job। প্রতিটা alert-এর জন্য শর্ত দেখে `acc_alert_state` upsert: শর্ত সত্য আর আগে `ok` ছিল → `firing`, `first_fired_at`, বিজ্ঞপ্তি পাঠানো, `last_notified_at`। আগে থেকেই `firing` → আবার বিজ্ঞপ্তি শুধু নির্ধারিত বিরতির পর (প্রস্তাব ৬ ঘণ্টা)। শর্ত মিথ্যা হলে `ok` (আর একটা সমাধানের বিজ্ঞপ্তি) |
| বিজ্ঞপ্তির মাধ্যম | ERPFlow-এর বিদ্যমান notification বা ইমেইল ব্যবস্থা (P0-02-এ যা পাওয়া গেছে)। না থাকলে সবচেয়ে সহজ: নির্ধারিত ইমেইল তালিকা। কে পাবে: Head of Finance আর tech on-call |
| Contract | `GET /accounting/health` (সব alert-এর বর্তমান অবস্থা, UI-র জন্য) |
| Acceptance | AC5.4 |
| Test | প্রতিটা alert ইচ্ছা করে ঘটিয়ে `firing` ও বিজ্ঞপ্তি, ঝড় নেই (একই শর্তে আবার বিজ্ঞপ্তি নির্ধারিত বিরতির আগে নয়), শর্ত মিটলে `ok` |
| Deliverable | Migration, command, job, health API ও UI, বিজ্ঞপ্তির ব্যবস্থা |
| Sub-tasks | migration, প্রতিটা alert-এর checker, state-machine ও ঝড়-নিয়ন্ত্রণ, বিজ্ঞপ্তি, API ও UI, test |

### P5-04: Rollback ও জরুরি বন্ধ পরিকল্পনা

| Field | Specification |
| --- | --- |
| ধরন | `ops` |
| নির্ভর করে | P4-13 |
| অবরুদ্ধ করে | P5-08 |
| উদ্দেশ্য | কিছু ভুল হলে কী করতে হবে তা আগে থেকে লেখা ও মহড়া দেওয়া |

| স্তর | কখন | কী করে | কী হারায় না |
| --- | --- | --- | --- |
| ১. Flag বন্ধ | যেকোনো সমস্যায়, প্রথম পদক্ষেপ | `event_intake` বা `accounting.enabled` বন্ধ (কারণ লিখে)। সঙ্গে সঙ্গে নতুন কাজ থামে | সব Draft, submitted, posted, event, audit |
| ২. Code আগের version-এ | Code-এ ভুল পাওয়া গেলে | CI/CD-র সুযোগ অনুযায়ী আগের SHA-তে ফেরা। **Migration rollback হয় না**, তাই প্রতিটা migration আগের code-এর সাথেও সামঞ্জস্যপূর্ণ (আগে যোগ, পরে বদল, শেষে মোছা) | DB-র সব data (migration ফেরানো হয় না) |
| ৩. Module বন্ধ | চরম ক্ষেত্রে, `modules_statuses.json`-এ `Accounting` বন্ধ | Route ও UI বন্ধ। Table, trigger, data থেকে যায় | সব |

**যা কখনো করা যাবে না (production):** (১) Trigger বা unique constraint-এর migration-এর `down()` চালানো। (২) Accounting-এর কোনো table-এর row সরাসরি SQL-এ বদলানো বা মোছা। (৩) Posted journal ঠিক করতে trigger বন্ধ করা। ভুল ঠিক করার একমাত্র পথ Reversal ও Adjustment।

| Field | Specification |
| --- | --- |
| Migration সামঞ্জস্য তালিকা | প্রতিটা Accounting migration-এর জন্য একটা সারি: কী যোগ করে, পুরোনো code-এর সাথে চলে কি না, `down()` নিরাপদ কি না। এই Pack-এর ৩০টা table আর expand migration সব এতে |
| মহড়া | Staging-এ: (১) N-1 code deploy, (২) N-এর migration চালানো, (৩) আবার N-1 code-এ ফেরা, কিছু ভাঙেনি দেখা। (৪) Flag বন্ধ করে চালু করা, data অক্ষত |
| Acceptance | AC5.5, AC5.6 |
| Deliverable | Rollback পরিকল্পনা, migration সামঞ্জস্য তালিকা, মহড়ার রেকর্ড |
| Sub-tasks | স্তর ১ থেকে ৩ লেখা, "করা যাবে না" তালিকা, migration তালিকা, মহড়া |

### P5-05: শেষ security ও performance যাচাই

| Field | Specification |
| --- | --- |
| ধরন | `test` |
| নির্ভর করে | P4-12, P3-14 |
| অবরুদ্ধ করে | P5-06 |
| উদ্দেশ্য | চালুর আগে শেষবার সব দরজা আর গতি দেখা |

| যাচাই | কীভাবে |
| --- | --- |
| Company isolation, সব endpoint | Route তালিকা থেকে স্বয়ংক্রিয়ভাবে: প্রতিটা `accounting` route অন্য company-র id দিয়ে ডেকে `ACC_COMPANY_FORBIDDEN` বা 404 আসছে কি না (নতুন route যোগ হলে test নিজে থেকে তাকে ধরে) |
| Permission, সব role × সব endpoint | Matrix, প্রত্যাশিত তালিকার সাথে মেলানো |
| Mass assignment ও input | প্রতিটা write endpoint: অনুমোদিত নয় এমন field (`company_id`, `status`, `posted_by`) পাঠিয়ে উপেক্ষা হচ্ছে কি না |
| SQL injection | Filter ও search parameter-এ |
| File upload | Attachment: ধরন, আকার, নাম, path traversal, (থাকলে) virus scan |
| Export download | অন্য company বা অন্য user-এর export ডাউনলোড, মেয়াদী লিংকের মেয়াদ |
| Audit tamper | Audit ও history table-এ সরাসরি `UPDATE` ও `DELETE` (trigger আটকায় কি না) |
| Rate limit | Report ও export endpoint-এ অতিরিক্ত অনুরোধ |
| Performance | Staging-এ বাস্তব আকারের data (P3-14-এর সীমা) আবার, slow query log দেখা, index পর্যালোচনা |

| Field | Specification |
| --- | --- |
| Acceptance | কোনো গুরুতর ফাঁক নেই, performance সীমার মধ্যে |
| Deliverable | Security ও performance রিপোর্ট, ফাঁক সংশোধন |
| Sub-tasks | route-ঘোরা isolation test, permission matrix, input যাচাই, upload, export, tamper, rate limit, performance |

### P5-06: Release gate ও চালুর রেকর্ড

| Field | Specification |
| --- | --- |
| ধরন | `schema` + `service` + `api` + `ui` |
| নির্ভর করে | P5-01, P5-02, P5-05, P1-05, P4-11 |
| অবরুদ্ধ করে | P5-08 |
| উদ্দেশ্য | কোনো company-তে `accounting.enabled` চালু হবে শুধু সব শর্ত লিখিতভাবে সবুজ হলে, আর প্রতিটা চালু ও বন্ধের পূর্ণ রেকর্ড |

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

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | FK | না |  |
| `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` | FK users | না |  |
| `approved_by` | FK users | হ্যাঁ |  |
| `reason` | VARCHAR(500) | না |  |
| `gate_snapshot` | JSON | হ্যাঁ | চালুর মুহূর্তের Checklist আর গেটের অবস্থা |
| `created_at` | TIMESTAMP | না |  |

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

| Field | Specification |
| --- | --- |
| গেট | তিনটা: `gate:uat` (P5-01-এর সই), `gate:cutover` (P5-02-এর মহড়া ও production cutover-এর প্রস্তুতি), `gate:security` (P5-05)। প্রতিটা সই `POST /accounting/release-gates/{gate}` (Finance owner বা OD-17-এর নির্ধারিত ভূমিকা, `accounting.enable.manage`), কারণ ও প্রমাণের লিংক সহ। বাতিল (`gate_revoked`) সম্ভব |
| চালুর শর্ত | `accounting.enabled` চালু করতে: (১) তিন গেটই `gate_passed` (সর্বশেষ কাজ), (২) Checklist `S01`–`S10` সবুজ (P1-05), (৩) দুজন (P1-04-এর change request, key `flag:accounting.enabled`, Maker–Checker)। প্রতিটা শর্ত না মিললে `SET_INCOMPLETE` বা `ACC_FORBIDDEN` (কোনটা বাকি তা বলে) |
| DB flow | transaction, সব শর্ত পড়া, `Setting` লেখা, `acc_enablement_log`-এ row (`gate_snapshot` সহ), audit। বন্ধ করা: এক ব্যক্তি, কারণ বাধ্যতামূলক, সঙ্গে সঙ্গে, `off` row |
| নিয়ম | R5.1 থেকে R5.5, R5.8, R5.10। একই সময়ে একটার বেশি company চালু করা নয় (প্রক্রিয়াগত, system আটকায় না, Release manager আটকায়) |
| UI | Release পাতা: তিন গেট, Checklist, চালুর বাটন (শর্ত পূরণে সক্রিয়), ইতিহাস |
| Error | `SET_INCOMPLETE`, `ACC_FORBIDDEN`, `JRN_SOD_VIOLATION` |
| Audit | `accounting.enablement.changed`, `accounting.flag.*` |
| Acceptance | AC5.1, AC5.3 |
| Test | কোনো গেট ছাড়া চালু (নিষেধ), গেট বাতিলের পর, নিজে অনুমোদন (নিষেধ), Checklist লাল, বন্ধ করলে data অক্ষত আর `off` row |
| Deliverable | Migration, service, API, UI |
| Sub-tasks | migration ও append-only, গেট সই, চালুর শর্ত, change request সংযোগ, UI, test |

### P5-07: Training ও support

| Field | Specification |
| --- | --- |
| ধরন | `ops` |
| নির্ভর করে | P5-01 |
| অবরুদ্ধ করে | P5-08 |
| উদ্দেশ্য | ব্যবহারকারীরা জানবে কী করতে হবে, আর সমস্যায় কাকে জানাবে |

| উপকরণ | কার জন্য | বিষয় |
| --- | --- | --- |
| Accountant-এর নির্দেশিকা | Accountant | Journal লেখা, attachment, submit, reject পেলে কী করা, reversal, queue |
| Approver-এর নির্দেশিকা | Finance Manager, Head of Finance | Approve ও reject, delegation, override ও পর্যালোচনা, post |
| Admin-এর নির্দেশিকা | Admin | Settings, Setup Checklist, CoA, approval rule, mapping, flag, release |
| Auditor-এর নির্দেশিকা | Auditor | Report, Audit Report, reconciliation dashboard |
| Finance-এর নির্দেশিকা | Finance | Period বন্ধ ও খোলা, intake-এর আটকে থাকা event |
| Error বার্তার সহায়িকা | সবাই | প্রতিটা error code-এর সহজ ভাষায় মানে ও কী করবেন (Section ২.৩ থেকে) |
| Support playbook | Support ও tech on-call | সমস্যার ধরন, কে দেখে, কী কী তথ্য চাইবে (`correlation_id`), কখন flag বন্ধ করবে |

| Field | Specification |
| --- | --- |
| Training | ভূমিকা ধরে ছোট অধিবেশন, উপস্থিতির রেকর্ড |
| Acceptance | AC5.9 |
| Deliverable | সব নির্দেশিকা, Error সহায়িকা, Support playbook, অধিবেশন ও উপস্থিতি |
| Sub-tasks | প্রতিটা নির্দেশিকা, Error সহায়িকা, playbook, অধিবেশন |

### P5-08: Pilot company চালু ও ধাপে ধাপে বিস্তার

| Field | Specification |
| --- | --- |
| ধরন | `ops` |
| নির্ভর করে | P5-03, P5-04, P5-06, P5-07 |
| অবরুদ্ধ করে | — (শেষ card) |
| উদ্দেশ্য | একটা company-তে সতর্কভাবে চালু, পর্যবেক্ষণ, তারপর বাকিদের |

| ধাপ | কী | শর্ত |
| --- | --- | --- |
| 1 | Production-এ Setup Checklist সম্পূর্ণ, opening balance Post (মহড়ার checklist মেনে) | `gate:cutover` |
| 2 | `accounting.enabled` চালু (দুজন) | P5-06-এর সব শর্ত |
| 3 | নির্ধারিত পর্যবেক্ষণ-সময় (client-এর সাথে ঠিক): সীমিত ব্যবহার, রোজ RC1 থেকে RC9-এর সারাংশ দেখা, alert-এর সাড়া | কোনো RC ব্যর্থ নয় |
| 4 | পর্যবেক্ষণ শেষে Finance owner-এর লিখিত সই | গুরুতর defect নেই |
| 5 | (আলাদা সিদ্ধান্তে) `event_intake.enabled` ও `sources.asset_write_off` চালু (P4-11-এর প্রথমবারের নিয়ম) | OD-18 অনুমোদিত, ধাপ ৪ সম্পন্ন |
| 6 | Asset intake-এর জন্য আলাদা পর্যবেক্ষণ-সময় | RC8 সবুজ |
| 7 | পরের company: ধাপ ১ থেকে আবার | আগেরটা স্থিতিশীল |

| Field | Specification |
| --- | --- |
| নিয়ম | R5.4, R5.9: Asset intake কখনো accounting চালুর সাথে একসাথে নয়। যেকোনো RC ব্যর্থ হলে পরের company-র চালু থামে |
| Acceptance | AC5.7, AC5.8, AC5.10 |
| Deliverable | চালুর রিপোর্ট, পর্যবেক্ষণের দৈনিক সারাংশ, সই, বাকি company-র সময়সূচি |
| Sub-tasks | production cutover, চালু, পর্যবেক্ষণ, সই, Asset intake ধাপ, পরের company |

## ৯. পুরো DB flow, ধাপে ধাপে

এই section-এ প্রতিটা বড় কাজ শুরু থেকে শেষ পর্যন্ত কোন table-এ কী ক্রমে পড়ে ও লেখে আর কোন lock নেয়, তা এক জায়গায়। Card-এর ভেতরের DB flow-গুলো এখান থেকেই নেওয়া। Deadlock এড়ানোর মূল নিয়ম নিচে।

### ৯.১ Lock-এর সর্বজনীন ক্রম

যে transaction একাধিক row আটকায়, সে **সবসময় এই ক্রমে**। কেউ ক্রম ভাঙলে deadlock হয়। এটা code review-র একটা বাধ্যতামূলক পরীক্ষা।

| ক্রম | কী | কীভাবে |
| --- | --- | --- |
| 1 | `acc_journals` (যে journal নিয়ে কাজ) | `FOR UPDATE` |
| 2 | `acc_approval_requests` | `FOR UPDATE` |
| 3 | `acc_accounts` (journal-এর line-এর account) | `FOR SHARE`, `id` ছোট থেকে বড় ক্রমে |
| 4 | `acc_periods` (journal-এর তারিখের period) | `FOR SHARE` (Post-এ)। Period বন্ধ-খোলায় `FOR UPDATE` |
| 5 | `acc_voucher_counters` | `ON DUPLICATE KEY UPDATE` (অন্তর্নিহিত exclusive) |
| 6 | দ্বিতীয় journal (reversal-এর ক্ষেত্রে মূল journal) | `FOR UPDATE`, reversal-এর পরে |

Blueprint-এর P2-08 ও P2-11-এ লেখা ক্রম "journal → request → counter" এর সাথে এটা সঙ্গতিপূর্ণ, শুধু ধাপ 3 আর 4 স্পষ্ট করা হলো। ধাপ 3-এর কারণ: Post-এর সময় কেউ account inactive বা বদলাতে পারলে ভুল Post হতো, `FOR SHARE` থাকলে account বদলানো Post-এর commit পর্যন্ত অপেক্ষা করে।

### ৯.২ Post (লেজারে বদল আনার একমাত্র পথ)

| # | Table | কাজ | Lock | ব্যর্থ হলে |
| --- | --- | --- | --- | --- |
| 0 | `acc_idempotency_keys` | `INSERT` (`processing`)। এটা মূল transaction-এর আগে, ছোট আলাদা transaction-এ commit | — | আগের ফল ফেরত |
| 1 | `acc_journals` | `SELECT ... FOR UPDATE` | ক্রম 1 | — |
| 2 | `acc_approval_requests` | `SELECT ... FOR UPDATE`, `status = 'approved'` মিলানো | ক্রম 2 | `JRN_NOT_APPROVED` |
| 3 | `acc_accounts` | `SELECT ... WHERE id IN (...) ORDER BY id FOR SHARE`, active ও leaf দেখা | ক্রম 3 | `JRN_ACCOUNT_INVALID` |
| 4 | `acc_periods` | `SELECT ... WHERE start_date <= ? AND end_date >= ? FOR SHARE`, `PeriodGuard` | ক্রম 4 | `JRN_PERIOD_LOCKED`, `JRN_DATE_INVALID` |
| 5 | `acc_journal_lines` | `SELECT SUM(debit), SUM(credit)`, header-এর সাথে মেলানো | — | `JRN_UNBALANCED` |
| 6 | `acc_voucher_counters` | `INSERT ... ON DUPLICATE KEY UPDATE last_no = LAST_INSERT_ID(last_no + 1)` | ক্রম 5 | rollback-এ নম্বর ফেরে |
| 7 | `acc_journals` | `UPDATE ... SET status = 'posted', voucher_no, period_id, posted_at, posted_by, posted_hash, version = version + 1 WHERE id = ? AND status = 'approved' AND version = ?` (DB trigger অনুমোদন করে: `approved` থেকে `posted`) | — | `JRN_CONFLICT` |
| 8 | `acc_journals` (মূল, শুধু reversal Post-এ) | `UPDATE ... SET status = 'reversed', reversed_at, reversed_by_journal_id WHERE id = ? AND status = 'posted'` (trigger: `posted` থেকে `reversed`, শুধু অনুমোদিত column) | ক্রম 6 | `JRN_STATE_INVALID` |
| 9 | `acc_journal_status_history` | `INSERT` (reversal হলে দুটো, প্রতিটা journal-এর জন্য) | — | — |
| 10 | `activity_logs` | `INSERT` `accounting.journal.posted`, `accounting.voucher.assigned` | — | — |
| 11 |  | **commit** | সব lock ছাড়ে | — |
| 12 | `acc_periods` | commit-এর **পরে**: `balance_version + 1` (P3-02 listener)। ব্যর্থ হলে আবার চেষ্টা, Post-কে প্রভাবিত করে না | — | — |
| 13 | `acc_idempotency_keys` | `done`, response সংরক্ষণ | — | — |
| 14 |  | `AcknowledgeEventJob` dispatch (OD-11 (খ) হলে, event-journal-এর জন্য) | — | — |

ধাপ 1 থেকে 10-এর মাঝে যেকোনো একটা ব্যর্থ হলে পুরো transaction rollback: journal `approved`-এই থাকে, voucher counter আগের মানে, কোনো history বা audit row নেই (ব্যর্থ চেষ্টার audit আলাদা transaction-এ)।

### ৯.৩ Submit থেকে Post পর্যন্ত, journal-এর পুরো যাত্রা

| # | কে | কাজ | পড়ে | লেখে | Journal-এর `status` পরে |
| --- | --- | --- | --- | --- | --- |
| 1 | Maker | Draft বানায় | `acc_accounts`, `acc_settings`, `acc_periods` | `acc_journals`, `acc_journal_lines`, `acc_journal_status_history` | `draft` |
| 2 | Maker | Submit | `acc_approval_rules`, `acc_approval_rule_levels` | `acc_approval_requests` (`rule_snapshot` সহ), `acc_journals`, `acc_journal_status_history` | `submitted` |
| 3 | Approver (ধাপ ১) | Approve | `acc_approval_requests`, `acc_approval_delegations` | `acc_approval_actions` | `submitted` (আরও ধাপ থাকলে) |
| 4 | Approver (শেষ ধাপ) | Approve | একই | `acc_approval_actions`, `acc_approval_requests` (`approved`), `acc_journals`, `acc_journal_status_history` | `approved` |
| 5 | Poster | Post | (৯.২) | (৯.২) | `posted` |
| 6 | Reader | Report দেখে | `acc_journals`, `acc_journal_lines` (শুধু `posted`, `reversed`) | `acc_report_exports` (export হলে) | — |

Reject হলে ধাপ 3 বা 4-এর জায়গায়: `acc_approval_actions` (`reject`), `acc_approval_requests` (`rejected`), `acc_journals` (`rejected`), history। Maker সংশোধন করে আবার ধাপ 2 (নতুন `submission_no`)।

### ৯.৪ Event থেকে Draft ও Asset-এ ফেরত

| # | কে | কাজ | Table ও কাজ | Lock |
| --- | --- | --- | --- | --- |
| 1 | Asset (বিদ্যমান) | Write-off executor | `asset_finance_events` `INSERT` (`pending`), Asset-এর নিজের transaction-এ, Accounting জানে না | Asset-এর |
| 2 | Scheduler | প্রতি মিনিটে | তিন flag দেখে প্রতিটা চালু company-র জন্য job dispatch | — |
| 3 | Job | Company-র lock | `GET_LOCK('acc_intake_{company}', 0)` | advisory |
| 4 | Job | তোলা | `asset_finance_events` `SELECT` (শুধু পড়া) | — |
| 5 | Job | গ্রহণ | `acc_finance_event_inbox` `INSERT ... ON DUPLICATE KEY` | row |
| 6 | Job | দখল | `acc_finance_event_inbox` `SELECT ... FOR UPDATE SKIP LOCKED` | row |
| 7 | `DraftBuilder` (প্রতি row আলাদা transaction) | পড়া | `acc_account_mappings`, `acc_accounts`, `acc_settings` | — |
| 8 | `DraftBuilder` | লেখা | `acc_journals` (`event_generated`, `source_*`), `acc_journal_lines`, `acc_journal_status_history`, `acc_finance_event_journal_links` (`mapping_snapshot`), `acc_finance_event_inbox` (`draft_created`) | নতুন row |
| 9 | `AcknowledgeEventJob` | ফেরত | Asset-এর `FinanceEventAcknowledger` → `asset_finance_events` `UPDATE status = 'accepted' WHERE status = 'pending'`; পরে `acc_finance_event_inbox.acknowledged_at` | Asset-এর |
| 10 | Accountant | Submit, approve, post | ৯.৩ ও ৯.২ | ৯.১-এর ক্রম |

ধাপ 8 পর্যন্ত যেকোনো ভাঙনে ওই row-এর transaction rollback, inbox `received`-ই থাকে, পরের run আবার চেষ্টা করে। ধাপ 8-এর পর ও 9-এর আগে ভাঙলে Draft আছে কিন্তু Asset এখনো `pending`: পরের run-এ ধাপ 4-এ আবার আসে, ধাপ 5-এ `EVT_DUPLICATE`, ধাপ 9 আবার চেষ্টা। কিছু হারায় না, দুইবার হয় না।

### ৯.৫ Period বন্ধ, বিশাল যাচাই সত্ত্বেও Post আটকে না রেখে

Section ৯.১-এ Post period `FOR SHARE` নেয়, আর বন্ধ করা `FOR UPDATE`। বন্ধ করার transaction ভারী যাচাই (RC1 থেকে RC9) চালানোর পুরো সময় lock ধরে রাখলে সব Post আটকে যেত। তাই:

| # | কাজ | Lock |
| --- | --- | --- |
| 1 | `acc_periods.balance_version` পড়া (`v`) | নেই |
| 2 | ভারী যাচাই: RC1 থেকে RC9, `no_pending_journals` | নেই (snapshot transaction) |
| 3 | নতুন ছোট transaction শুরু: period `FOR UPDATE` | Post-এর `FOR SHARE`-এর সাথে বিরোধ, তাই চলমান Post শেষ হওয়া পর্যন্ত অপেক্ষা, নতুন Post আটকে |
| 4 | `balance_version` আবার পড়া | — |
| 5 | `balance_version = v` হলে মাঝে কোনো Post হয়নি, তাই ধাপ 2-এর ফল এখনো সত্য। Period `closed`, event `approved`, audit, commit | — |
| 6 | `balance_version ≠ v` হলে lock ছেড়ে ধাপ 1 থেকে আবার (সর্বোচ্চ তিনবার)। তারপরও না মিললে `PRD_CLOSE_BLOCKED` ("চলতি Post-এর কারণে স্থির হচ্ছে না, আবার চেষ্টা করুন") | — |

এতে lock ধরে রাখার সময় কয়েক মিলিসেকেন্ড, ভারী কাজ lock-এর বাইরে। P3-11-এর "যাচাই আবার চালানো" এই নকশাতেই করা হবে। `balance_version` কেবল commit-এর পরে বাড়ে (P3-02), তাই একটা Post যা commit হয়েছে কিন্তু এখনো version বাড়ায়নি, সেই সংক্ষিপ্ত ফাঁকে বন্ধ ফসকে যেতে পারে। এটা ঠেকাতে ধাপ 5-এ `no_pending_journals`-এর মতো সস্তা যাচাই আর `MAX(posted_at)` তুলনাও আবার চলে (period-র journal-এর সর্বশেষ `posted_at` ধাপ 2-এর সময়ের পরে হলে ব্যর্থ)।

### ৯.৬ Reversal-এর শৃঙ্খল

| # | কে | কাজ | Table | `status` |
| --- | --- | --- | --- | --- |
| 1 | User | `POST /{id}/reverse` | মূল journal `FOR UPDATE` পড়া। `acc_journals`-এ নতুন row (`reversal`, `reverses_journal_id`) আর উল্টো line। `uq_acc_journals_reversal` DB-তে দ্বিতীয় reversal ঠেকায় | মূল `posted`, নতুন `draft` |
| 2 | Maker, Approver | সাধারণ Submit ও approve | ৯.৩ | নতুন `approved` |
| 3 | Poster | Post | ৯.২-এর ধাপ 8 সহ | নতুন `posted`, মূল `reversed` |
| 4 | Reader | Report | মূল আর reversal দুটোই ledger-এ (R3.2), নিট প্রভাব শূন্য | — |

Reversal Draft বাতিল বা reject হলে মূল `posted`-ই থাকে, `uq_acc_journals_reversal` ছেড়ে দেয় (`cancelled` হলে), নতুন reversal বানানো যায়।

## ১০. Master build sequence, WBS-এ রূপান্তর ও সংশোধন-তালিকা

### ১০.১ Master build sequence

সব card এক তালিকায়, যে ক্রমে বানাতে হবে। **একই `ক্রম` নম্বরের card গুলো একসাথে চলতে পারে**, যদি তাদের `নির্ভর করে`-এর সব card আগের ক্রমে `Done` হয়ে থাকে। "বাকি OD" কলামে সেই Open Decision, যা বন্ধ না হলে card `Blocked`।

দুটো ব্যতিক্রম মনে রাখতে হবে:

- **P2-09 দুই ভাগ:** `P2-09a` = শুধু `ApproverEligibility` service (P2-08 এটা ব্যবহার করে, তাই আগে লাগে)। `P2-09b` = delegation-এর CRUD, override, পর্যালোচনা ও UI (P2-08-এর পরে)।
- **P4-03 ও P4-11:** Consumer job-এর code P4-03-এ লেখা যায় আগে, কিন্তু চালানো যায় শুধু P4-11 (তিন স্তরের flag) শেষ হলে।

| ক্রম | Card | কাজ | নির্ভর করে | বাকি OD |
| --- | --- | --- | --- | --- |
| 1 | P0-01 | Blueprint ও ADR অনুমোদন | — | — |
| 2 | P0-02 | Repo-র convention নিশ্চিত | P0-01 | — |
| 2 | P0-03 | Feature branch ও release নিয়ম | P0-01 | — |
| 2 | P0-04 | Client-এর Setup Checklist ও decision list | P0-01 | — |
| 3 | P0-05 | DB guard ও ledger-উৎস spike | P0-02 | OD-13, OD-14 |
| 4 | P1-01 | Module scaffold ও flag | P0-02, P0-03 | — |
| 5 | P1-12 | Tenancy, audit, error-এর সাধারণ ব্যবস্থা | P1-01, P0-02 | — |
| 5 | P1-02 | Permission slug ও role seed | P1-01 | — |
| 6 | P1-03 | Settings store, registry, history, lock | P1-01, P0-04 | — |
| 7 | P1-04 | Settings UI ও দ্বিতীয় অনুমোদন | P1-03, P1-02 | — |
| 7 | P1-06 | CoA schema ও service | P1-03 | OD-7 (আংশিক) |
| 7 | P1-08 | Period কাঠামো | P1-03 | OD-4 |
| 8 | P1-05 | Setup Checklist | P1-03, P1-04 | — |
| 8 | P1-07 | CoA API ও UI | P1-06, P1-02 | — |
| 8 | P1-09 | Journal-এর ভিত্তি table ও idempotency | P1-06, P1-08, P0-05 | OD-23 (P1-11-এর জন্য) |
| 9 | P1-10 | DB guard বাস্তবায়ন | P1-09, P0-05 | OD-13 |
| 10 | P1-11 | Opening balance Draft | P1-07, P1-09, P1-10, P1-12 | OD-9, OD-23 |
| 11 | P1-13 | Phase 1 test suite | P1-01 থেকে P1-12 | — |
| 12 | P1-14 | Runbook ও merge gate | P1-13 | — |
| 13 | P2-01 | Journal state machine | P1-13, P1-10 | — |
| 13 | P2-03 | Journal validator | P1-09, P1-08, P1-03 | — |
| 13 | P2-10 | SoD service | P1-03 | — |
| 14 | P2-04 | Journal status history | P2-01, P1-10 | — |
| 14 | P2-05 | Approval table | P2-01, P1-09 | OD-6 |
| 14 | P2-11 | Voucher নম্বর | P1-08, P1-09, P1-03 | OD-8 |
| 15 | P2-06 | Approval rule service ও UI | P2-05 | OD-6 |
| 15 | P2-09a | `ApproverEligibility` | P2-05 | OD-6 |
| 15 | P2-02 | Draft তৈরি ও সম্পাদনা | P2-01, P2-03, P1-07 | — |
| 16 | P2-07 | Approval resolver | P2-06 | OD-6 |
| 17 | P2-08 | Approval request ও সিদ্ধান্ত | P2-07, P2-10, P2-04, P2-09a | — |
| 18 | P2-12 | Posting service | P2-08, P2-10, P2-11, P1-10, P2-03 | OD-5 (hook), OD-14 |
| 18 | P2-09b | Delegation, override, পর্যালোচনা | P2-08 | — |
| 19 | P2-13 | Reversal ও Adjustment | P2-12 | OD-15 |
| 19 | P2-14 | Opening journal-এর Post | P1-11, P2-12 | — |
| 19 | P2-15 | Queue ও Register | P2-02, P2-08, P2-12 | — |
| 20 | P2-16 | Phase 2 test suite | P2-01 থেকে P2-15 | — |
| 21 | P2-17 | Runbook, demo, সিদ্ধান্ত বন্ধ | P2-16 | — |
| 22 | P3-01 | Ledger query layer | P2-16, P0-05 | OD-14 |
| 23 | P3-02 | Derived balance cache | P3-01, P1-08 | — |
| 24 | P3-03 | General Ledger | P3-01 | — |
| 24 | P3-04 | Trial Balance | P3-01, P3-02 | — |
| 24 | P3-05 | Profit & Loss | P3-01 | OD-1 |
| 25 | P3-06 | Balance Sheet | P3-01, P3-05 | OD-1, OD-20 |
| 26 | P3-07 | Cash Flow mapping UI | P1-07, P1-04 | OD-2 |
| 26 | P3-09 | অন্য report | P3-01, P2-15 | — |
| 27 | P3-08 | Cash Flow Statement | P3-01, P3-07, P3-06 | OD-1, OD-2 |
| 28 | P3-13 | Reconciliation framework | P3-03 থেকে P3-06, P3-08 | — |
| 29 | P3-10 | Export | P3-03 থেকে P3-09 | — |
| 29 | P3-11 | Period বন্ধ ও আবার খোলা | P1-08, P3-13, P2-10, P3-01 | OD-5, OD-15 |
| 30 | P3-12 | Period lock-এর কঠোর রূপ | P3-11, P2-12 | OD-5, OD-15 |
| 31 | P3-14 | Performance ও test suite | P3-01 থেকে P3-13 | — |
| 32 | P3-15 | Accountant-এর নমুনা pack ও সই | P3-14 | OD-1, 2, 5, 14, 15, 16, 20, 23 |
| 33 | P4-01 | Change request ও সিদ্ধান্ত বন্ধ | P3-15 | OD-18, 3, 10, 11, 21, 22 |
| 34 | P4-02 | Finance Event Inbox | P4-01, P1-12, P2-12 | — |
| 34 | P4-04 | Asset event reader | P4-01 | OD-21 |
| 35 | P4-03 | Consumer job | P4-02, P4-04, P1-12, P1-01 | — |
| 35 | P4-05 | Account mapping | P4-02, P1-06, P1-04, P2-10 | OD-10 |
| 36 | P4-06 | Event থেকে Draft Journal | P4-03, P4-05, P2-02, P2-03 | OD-10, OD-3 |
| 36 | P4-11 | তিন স্তরের flag | P1-05, P1-04, P4-05 | — |
| 37 | P4-07 | Asset-এ ফেরত জানানো | P4-06, P4-01 | OD-11, OD-21 |
| 37 | P4-08 | Event-journal-এর approval ও SoD | P4-06, P2-08, P2-10 | — |
| 37 | P4-09 | Inbox ops screen | P4-02, P4-03, P4-05, P4-06 | — |
| 38 | P4-10 | Reconciliation RC8 | P4-06, P3-13 | — |
| 39 | P4-12 | Phase 4 test suite | P4-02 থেকে P4-11 | — |
| 40 | P4-13 | Runbook, demo, release প্রস্তুতি | P4-12 | — |
| 41 | P5-01 | UAT pack ও সই | P3-15, P4-13 | OD-17 |
| 41 | P5-02 | Cutover মহড়া | P3-15, P2-14, P3-11 | OD-9 |
| 41 | P5-03 | Monitoring ও alert | P3-13, P4-03, P4-10, P2-15 | — |
| 41 | P5-04 | Rollback পরিকল্পনা | P4-13 | — |
| 41 | P5-05 | শেষ security ও performance | P4-12, P3-14 | — |
| 42 | P5-06 | Release gate ও চালুর রেকর্ড | P5-01, P5-02, P5-05, P1-05, P4-11 | — |
| 42 | P5-07 | Training ও support | P5-01 | — |
| 43 | P5-08 | Pilot company চালু ও বিস্তার | P5-03, P5-04, P5-06, P5-07 | — |

মোট ৭৩টা card (P2-09 দুই ভাগে ধরলে ৭৪টা কাজ)। ক্রমের গ্রুপ ৪৩টা।

### ১০.২ Card থেকে WBS-এ রূপান্তর

প্রতিটা card WBS-এর একটা **Level 3 (Feature)** আর তার `Sub-tasks` field-এর প্রতিটা টুকরো একটা **Level 4 (Task)**। Workstream (Level 2) নিচের তালিকা অনুযায়ী।

| Phase | Workstream | Card-গুলো |
| --- | --- | --- |
| 0 | 0.1 Governance | P0-01, P0-03, P0-04 |
| 0 | 0.2 Discovery spike | P0-02, P0-05 |
| 1 | 1.1 Module ও access | P1-01, P1-02 |
| 1 | 1.2 Settings ও checklist | P1-03, P1-04, P1-05 |
| 1 | 1.3 Chart of Accounts | P1-06, P1-07 |
| 1 | 1.4 Period | P1-08 |
| 1 | 1.5 Journal-এর ভিত্তি ও guard | P1-09, P1-10 |
| 1 | 1.6 Opening balance | P1-11 |
| 1 | 1.7 Cross-cutting | P1-12 |
| 1 | 1.8 QA ও release | P1-13, P1-14 |
| 2 | 2.1 Journal lifecycle | P2-01, P2-02, P2-03, P2-04 |
| 2 | 2.2 Approval | P2-05, P2-06, P2-07, P2-08, P2-09a, P2-09b, P2-10 |
| 2 | 2.3 Posting | P2-11, P2-12 |
| 2 | 2.4 Correction | P2-13, P2-14 |
| 2 | 2.5 Queue | P2-15 |
| 2 | 2.6 QA ও release | P2-16, P2-17 |
| 3 | 3.1 Ledger core | P3-01, P3-02 |
| 3 | 3.2 Report | P3-03, P3-04, P3-05, P3-06, P3-09 |
| 3 | 3.3 Cash Flow | P3-07, P3-08 |
| 3 | 3.4 Export | P3-10 |
| 3 | 3.5 Period control | P3-11, P3-12 |
| 3 | 3.6 Reconciliation | P3-13 |
| 3 | 3.7 QA ও সই | P3-14, P3-15 |
| 4 | 4.0 Governance | P4-01 |
| 4 | 4.1 Intake | P4-02, P4-03, P4-04 |
| 4 | 4.2 Mapping ও Draft | P4-05, P4-06 |
| 4 | 4.3 Asset-এর দিক | P4-07 |
| 4 | 4.4 Approval ও flag | P4-08, P4-11 |
| 4 | 4.5 Ops | P4-09, P4-10 |
| 4 | 4.6 QA ও release | P4-12, P4-13 |
| 5 | 5.1 UAT ও cutover | P5-01, P5-02 |
| 5 | 5.2 Ops | P5-03, P5-04 |
| 5 | 5.3 Verification | P5-05 |
| 5 | 5.4 Release | P5-06, P5-08 |
| 5 | 5.5 Enablement | P5-07 |

**Import-এর জন্য সুপারিশকৃত কলাম:** WBS ID (`Phase.Workstream.Card.Task`, যেমন `2.2.P2-08.3`), নাম, ধরন, নির্ভরতা (card ID), বাকি OD, Acceptance (AC নম্বর), Estimate, Owner, Sprint, অবস্থা। শেষ চারটা ইচ্ছা করে ফাঁকা (scope doc অনুযায়ী সময় আর পরিমাণ Discovery-র পরে ঠিক হবে)।

### ১০.৩ এই Pack বানাতে গিয়ে যা ঠিক করা বা যোগ করা হয়েছে

Blueprint-এর সাথে আর আগের section-গুলোর সাথে নিচের পার্থক্য আছে। **WBS বানানোর সময় এই Pack-কে সত্যের উৎস ধরতে হবে**, আর পরে Blueprint হালনাগাদ করতে হবে।

**নতুন Open Decision**

| OD | বিষয় | বিকল্প | প্রভাব | কখনের মধ্যে |
| --- | --- | --- | --- | --- |
| OD-23 | Opening balance-এ Revenue বা Expense account থাকতে পারবে কি, আর P&L-এ opening journal-এর line ধরা হবে কি | (ক) যেমন আছে, কোনো বিশেষ আচরণ নেই (এই Pack-এর ধরে নেওয়া) (খ) Opening-এ শুধু Asset, Liability, Equity চলবে (P1-11-এর validator বাড়ে) (গ) Opening-এর Revenue ও Expense line P&L থেকে বাদ | (ক)-তে বছরের মাঝে cutover হলে চালু-মাসের P&L ফুলে যায়। (খ) সবচেয়ে সরল ও নিরাপদ, কিন্তু বছরের শুরুর পর থেকে জমা লাভ Equity-তে হাতে ঢোকাতে হয়। (গ) জটিল | Accountant, P1-11 শুরুর আগে |

**নতুন error code (Section ২.৩-এর তালিকায় যোগ হবে)**

| Code | কখন | HTTP | Card |
| --- | --- | --- | --- |
| `COA_CODE_INVALID` | Account code settings-এর দৈর্ঘ্য বা পরিসর ভাঙে | 422 | P1-06 |
| `SET_STALE_REQUEST` | Setting-এর অনুরোধের পরে মান বদলে গেছে | 409 | P1-04, P4-05 |
| `ACC_APPEND_ONLY` | শুধু-যোগ-হয় table বদলানোর চেষ্টা | 409 | P1-10 |
| `EVT_STATE_INVALID` | Inbox-এর অবৈধ অবস্থা-বদল | 409 | P4-02 |
| `EVT_PROCESSING_ERROR` | বারবার চেষ্টার পরও অপ্রত্যাশিত ব্যর্থতা | তথ্য (`failed`) | P4-03 |
| `RPT_BASIS_UNSUPPORTED` | `accounting_basis = cash` অথচ Cash basis বানানো হয়নি | 422 | P3-05 |

**নতুন table (Section ২.১-এর index-এ যোগ হবে)**

| Table | Phase | কাজ | Card |
| --- | --- | --- | --- |
| `acc_approval_override_reviews` | 2 | Override-এর পর্যালোচনা | P2-09 |
| `acc_intake_heartbeats` | 4 | Consumer-এর স্বাস্থ্য | P4-03 |
| `acc_alert_state` | 5 | Alert-এর অবস্থা, ঝড় ঠেকাতে | P5-03 |

মোট `acc_` table ৩১টা (index-এর ২৮ + এই তিনটা), বিদ্যমান `asset_finance_events` বাদে। P5-04-এর "৩০টা" মানে এই প্রায় সংখ্যা।

**Blueprint থেকে নিয়মের পার্থক্য**

| বিষয় | Blueprint | এই Pack | কেন |
| --- | --- | --- | --- |
| Journal-এ `return` কাজ | নেই (`approved` journal সংশোধনের পথ ছিল না) | `approved` থেকে `rejected` (P2-01) | Post-এর আগে ধরা পড়া সমস্যা ঠিক করার উপায় |
| Report-এ কোন অবস্থা | "শুধু posted" | `posted` ও `reversed` দুটোই | `reversed` আসলে posted, বাদ দিলে reversal-এর প্রভাব ভুল হয় |
| Lock-এর ক্রম | journal, request, counter | journal, request, account (`FOR SHARE`), period (`FOR SHARE`), counter | Post-এর সময় account বা period বদল ঠেকাতে |
| Source identity-র unique | সব journal-এ | বাতিল (`cancelled`) বাদে | Event-এর Draft বাতিল হলে নতুন Draft বানানোর জন্য |
| Flag-এর সংরক্ষণ | `acc_settings` | তিনটা flag ERPFlow-এর `Setting`-এ | `acc_settings` তৈরির আগেই flag কাজ করতে |
| Event-তৈরি Draft কে সম্পাদনা করে | — | যেকোনো `journal.create`-ওয়ালা, Maker = `submitted_by` | System তৈরি করে, `created_by` নেই |
| Approval rule-এ ফাঁক ও overlap | দুটোই "ধরা পড়ে" | Overlap error, ফাঁক সতর্কতা | ফাঁক ইচ্ছাকৃত হতে পারে |
| Period বন্ধের চেষ্টা | যাচাই আবার চালানো | lock-এর বাইরে ভারী যাচাই, ভেতরে `balance_version` ও `MAX(posted_at)` তুলনা | Post আটকে না রেখে |
| Posted journal-এর hash | নেই | `posted_hash` ও RC9 | Trigger না বসানো গেলেও বদল ধরা পড়ে |
| Account ও journal-এর company-র সীমা | Application-এ যাচাই | DB-তেও composite foreign key | অন্য company-র সাথে জোড়া লাগানো DB স্তরেই অসম্ভব |
| নতুন AC | AC1.1 থেকে AC1.11 | AC1.12 (period ফাঁকহীন) | P1-08 |

**বাকি ছোট সংশোধন:** Section ২.২-র ER-এ `acc_settings` ও `acc_periods` নেই (ইচ্ছাকৃত, লেখা আছে)। Section ৯.১-এ "Blueprint-এর P2-08" বলতে এই Pack-এর P2-08।

## ১১. Phase 6 task cards: Journal Management ও Analytical Dimensions

Phase 6-এ ১৫টা card। সব journal তৈরি `JournalService` দিয়ে Draft-এ, আর `acc_journals.status` শুধু `PostingService` (P2-12) লেখে। Post-এ lock ক্রম সবসময়: journal, approval request, account (`FOR SHARE`, id ক্রমে), period (`FOR SHARE`), number series (`FOR SHARE`) তারপর counter, শেষে dimension value (`FOR SHARE`, id ক্রমে)। Capability flag দুটো: `journal_mgmt.enabled`, `dimensions.enabled`, দুটোই default বন্ধ, `Setting` model-এ (group `accounting`), `AccountingFeatureService` দিয়ে। সব migration expand-only। Build ক্রম নিচের "Build ক্রমের সারি" অংশে।

| Card | নাম | ধরন |
| --- | --- | --- |
| P6-01 | Journal type registry schema ও migration | schema |
| P6-02 | Type আচরণ service ও API | service + api |
| P6-03 | Number series schema ও service | schema + service + api |
| P6-04 | Voucher counter migration ও VoucherService facade | command + service |
| P6-05 | Document date, posting date ও backdate | schema + service |
| P6-06 | Journal template | schema + service + api |
| P6-07 | Bulk journal import | schema + job + api |
| P6-08 | Attachment policy, comment, tag, copy, clone, filter, search | schema + job + api |
| P6-09 | Dimension framework schema ও service | schema + service + job |
| P6-10 | Dimension rule, default, validation, balancing | schema + service + api |
| P6-11 | Dimension value lifecycle ও mirror | service + job + api |
| P6-12 | Dimension reclassification | schema + service + api |
| P6-13 | Report filter, group-by, cache, index, security hook | schema + service |
| P6-14 | Admin ও journal form UI | ui |
| P6-15 | Enablement, RC sub-check, runbook, UAT | release |

### P6-01: Journal type registry schema ও migration

| Field | Specification |
| --- | --- |
| ধরন | `schema` |
| নির্ভর করে | P2-03, P1-10, P1-04, P1-08 |
| অবরুদ্ধ করে | P6-02, P6-03, P6-05, সব ভবিষ্যতের phase-এর নতুন journal ধরন |
| OD | OD-24b (phase type কখন seed), OD-24a (custom type) |
| উদ্দেশ্য | `ck_acc_journals_type` সরিয়ে তার জায়গায় `acc_journal_types`-এ composite FK। প্রতি company-তে 16টা type seed। পুরোনো journal-এর আচরণ একই থাকে |
| Schema | নিচের `acc_journal_types` ও `acc_journal_type_account_ranges`; `acc_journals`-এ বদল |
| DB flow (migration, এক ক্রমে) | (1) দুটো table তৈরি। (2) `JournalTypeSeeder::seedAll()`: প্রতি বিদ্যমান company-তে এক transaction, `INSERT ... ON DUPLICATE KEY UPDATE id = id` (পুনরায় চালালে কিছু বদলায় না)। (3) পরিদর্শন: `SELECT DISTINCT company_id, journal_type FROM acc_journals` আর seed করা code-এর পার্থক্য ফাঁকা না হলে থামা (`JTY_MIGRATION_ORPHAN`)। (4) `acc_journals.journal_type` যদি ENUM হয় তবে `VARCHAR(30) NOT NULL`-এ রূপান্তর (online)। (5) index `(company_id, journal_type)` যোগ। (6) composite FK `fk_acc_journals_type`। (7) `ck_acc_journals_type` সরানো। নতুন company: `AccountingSetupService` শুরুতে `seedCompany`। কোনো ledger lock নেয় না। বড় table (সারি সীমা ছাড়ালে) (4) ও (6) online schema change tool দিয়ে। ব্যর্থ হলে প্রতিটা ধাপ আলাদা পুনঃচালুযোগ্য |
| নিয়ম | R6.1 থেকে R6.5, R6.38। Seed মান নিচের তালিকায়। Seed-এ `default_series_code = 'JV'` (সব type আগের মতো একই নম্বর গাঁথে)। পুরোনো কোড বদলায় না: flag বন্ধ থাকলে table থাকে কিন্তু কেউ পড়ে না |
| Contract | `JournalTypeSeeder::seedCompany(companyId)` (idempotent)। Artisan `accounting:journal-types:seed {--company=}`। কোনো HTTP endpoint নেই (API P6-02-এ) |
| Permission | নেই (deploy ও setup path) |
| Error | `JTY_MIGRATION_ORPHAN` (CLI exit code ও তালিকা)। পরের FK ভঙ্গ হলে DB ত্রুটি সরাসরি `JRN_*`-এ ম্যাপ হয় না, কারণ seed সবসময় আগে |
| Audit | `accounting.journal_type.created` (actor system, প্রতি company-তে একবার সারাংশসহ) |
| Acceptance | AC6.1 |
| Test | `JournalTypeMigrationTest`: 3টা company, একটা-র journal আছে। (ক) 16টা type প্রতি company-তে। (খ) দ্বিতীয়বার চালালে কোনো পরিবর্তন নেই। (গ) অজানা type-সহ INSERT FK-তে আটকায়। (ঘ) orphan থাকলে migration থামে। (ঙ) পুরোনো কোড (flag বন্ধ) Phase 5-এর suite পাস |
| Deliverable | Migration, seeder, artisan command, `down()` মন্তব্য (নিরাপদ নয় যদি পরে বাইরের code-এর journal থাকে: CHECK ফেরত শুধু 5টা মুল code থাকলে) |
| Sub-tasks | (1) Table migration ও model। (2) Seeder ও seed তালিকা ফাইল (config, কোডে বাঁধা নয়)। (3) `AccountingSetupService` hook। (4) FK migration + online পদ্ধতি। (5) P1-10 trigger যাচাই (journal\_type অপরিবর্তনীয় আগে থেকে)। (6) Test |

**Seed তালিকা (প্রতি company-তে):**

| code | kind | owner\_phase | is\_active | allow\_manual\_entry | approval\_rule\_type | attachment\_rule | auto\_reverse\_allowed |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `manual` | system | NULL | 1 | 1 | standard | optional | 1 |
| `opening` | system | NULL | 1 | 1 | strict | optional | 0 |
| `reversal` | system | NULL | 1 | 0 | strict | optional | 0 |
| `adjustment` | system | NULL | 1 | 0 | strict | required | 0 |
| `event_generated` | system | NULL | 1 | 0 | standard | none | 0 |
| `accrual` | phase | 12 | 0 | 0 | strict | optional | 1 |
| `recurring` | phase | 12 | 0 | 0 | standard | optional | 0 |
| `fx_revaluation` | phase | 7 | 0 | 0 | strict | none | 1 |
| `depreciation` | phase | 11 | 0 | 0 | standard | none | 0 |
| `closing` | phase | 12 | 0 | 0 | strict | optional | 0 |
| `intercompany` | phase | 15 | 0 | 0 | strict | required | 0 |
| `elimination` | phase | 15 | 0 | 0 | strict | optional | 0 |
| `consolidation` | phase | 15 | 0 | 0 | strict | optional | 0 |
| `bank` | phase | 10 | 0 | 0 | standard | optional | 0 |
| `tax_settlement` | phase | 8 | 0 | 0 | strict | required | 0 |
| `import` | phase | 6 | 0 | 0 | strict | required | 0 |

`import` type `journal_mgmt.enabled` চালুর সময় P6-15 সক্রিয় করে। System type-এর `opening` ও `manual` entry মান বর্তমান P2-03 আচরণ থেকে Entry gate-এ নিশ্চিত করার পর অনুসরণ হবে।

**`acc_journal_types`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | BIGINT UNSIGNED PK AI | না |  |
| `company_id` | BIGINT UNSIGNED FK | না | `companies` |
| `code` | VARCHAR(30) | না | CHECK `^[a-z][a-z0-9_]{1,29}$` |
| `name` | VARCHAR(100) | না |  |
| `name_bn` | VARCHAR(100) | হ্যাঁ |  |
| `kind` | ENUM('system','phase','custom') | না |  |
| `owner_phase` | TINYINT UNSIGNED | হ্যাঁ | phase type-এ বাধ্যতামূলক |
| `is_active` | TINYINT(1) | না | Default 1 |
| `allow_manual_entry` | TINYINT(1) | না | Default 0 |
| `approval_rule_type` | VARCHAR(10) | না | `standard` বা `strict` |
| `default_series_code` | VARCHAR(20) | হ্যাঁ | FK P6-03 থেকে |
| `auto_reverse_allowed` | TINYINT(1) | না | Default 0 |
| `attachment_rule` | ENUM('none','optional','required') | না | Default `optional` |
| `attachment_min_count` | TINYINT UNSIGNED | না | Default 1 |
| `requires_document_date` | TINYINT(1) | না | Default 0 |
| `backdate_days_limit` | SMALLINT UNSIGNED | হ্যাঁ | NULL = কোনো অতিরিক্ত সীমা নেই |
| `future_days_limit` | SMALLINT UNSIGNED | হ্যাঁ | ঐ |
| `sort_order` | SMALLINT | না | Default 0 |
| `version` | INT UNSIGNED | না | Default 1, optimistic check |
| `created_by` | BIGINT UNSIGNED | হ্যাঁ | NULL = system |
| `created_at`, `updated_at` | TIMESTAMP | না |  |

- `uq_acc_jt_company_code (company_id, code)` (composite FK-র লক্ষ্য)।
- `uq_acc_jt_company_id (company_id, id)`।
- CHECK: `kind <> 'custom' OR code LIKE 'x\_%'`; `kind <> 'system' OR is_active = 1`; `attachment_min_count BETWEEN 1 AND 10`।
- Index `idx_acc_jt_active (company_id, is_active, kind, sort_order)`।

**`acc_journal_type_account_ranges`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | BIGINT UNSIGNED PK AI | না |  |
| `company_id` | BIGINT UNSIGNED FK | না |  |
| `journal_type_code` | VARCHAR(30) | না |  |
| `effect` | ENUM('allow','deny') | না |  |
| `from_code` | VARCHAR(50) | হ্যাঁ | `acc_accounts.code`-এর সমান collation |
| `to_code` | VARCHAR(50) | হ্যাঁ |  |
| `account_group_id` | BIGINT UNSIGNED | হ্যাঁ | গ্রুপ account (`is_group`) |
| `created_by`, `created_at` |  |  |  |

- FK `(company_id, journal_type_code)` → `acc_journal_types (company_id, code)` `ON DELETE RESTRICT`।
- FK `(company_id, account_group_id)` → `acc_accounts (company_id, id)`।
- CHECK: হয় দুই code পূর্ণ (আর `account_group_id` NULL), নয়তো শুধু `account_group_id`; `CHAR_LENGTH(from_code) = CHAR_LENGTH(to_code)`; `from_code <= to_code`।
- `uq (company_id, id)`; index `(company_id, journal_type_code, effect)`।

**`acc_journals` (expand):** `fk_acc_journals_type FOREIGN KEY (company_id, journal_type) REFERENCES acc_journal_types (company_id, code)`; index `idx_acc_journals_type (company_id, journal_type)`; `ck_acc_journals_type` বাদ।

### P6-02: Type আচরণ service ও API

| Field | Specification |
| --- | --- |
| ধরন | `service + api` |
| নির্ভর করে | P6-01, P2-03, P2-08, P1-04 |
| অবরুদ্ধ করে | P6-05, P6-06, P6-07, P6-08, P6-14 |
| OD | OD-24a (custom type) |
| উদ্দেশ্য | Type-এর flag-কে আসল আচরণে রূপ দেওয়া। `JournalTypeRegistry` এই তথ্যের একমাত্র পড়ার দরজা। P2-03 validator তিন মোডে (Draft, Submit, Post) এই service ডাকে। Admin API দিয়ে type দেখা ও বদলানো |
| Schema | নতুন table নেই। P6-01-এর table ব্যবহার, `acc_journal_types.version` optimistic check |
| DB flow | **Draft লেখা:** নতুন lock নেই। Type ও ranges cache থেকে পড়া (key `acc:jt:{company}:{code}:{version}`), cache মিস হলে সাধারণ `SELECT`। **Submit:** journal সারি `FOR UPDATE` (lock ক্রম ১), type পড়ায় lock নেই। **Post:** lock ক্রম অপরিবর্তিত। Type শুধু transaction শুরুর snapshot-এ পড়া, কোনো type lock নেই (Post চলার সময় type বদলালে সেই Post পুরোনো snapshot ধরে চলে, R6.5)। **Type বদল (API):** এক transaction: `UPDATE acc_journal_types SET ..., version = version + 1 WHERE company_id = ? AND code = ? AND version = ?`, ০ সারি বদলালে `JTY_CONFLICT`। তারপর audit, commit-এর পর cache মুছা |
| নিয়ম | R6.1 থেকে R6.9, R6.38। **Validator hook:** (ক) Draft, Submit, Post: type আছে ও `is_active` (`JTY_TYPE_INACTIVE`)। (খ) Draft: `allow_manual_entry = 0` হলে manual API পথে (`origin = manual`) তৈরি আটকায় (`JTY_MANUAL_NOT_ALLOWED`)। Template, import, reclass, event পথ এই পরীক্ষা এড়ায়, কারণ সেখানে `origin` অন্য মান। (গ) Draft, Submit, Post: প্রতি line-এর account ranges-এ (`JTY_ACCOUNT_RANGE_VIOLATION`, field `lines[n].account_id`)। (ঘ) Draft: `auto_reverse_on` ভরা কিন্তু `auto_reverse_allowed = 0` হলে `JTY_AUTO_REVERSE_NOT_ALLOWED`। Column Phase 12-এর, column না থাকলে hook নিষ্ক্রিয়। (ঙ) Submit: `strict` type-এ approval rule না মিললে `JTY_APPROVAL_RULE_MISSING`। কোনো type Approval এড়ায় না। (চ) Submit ও Post: সংযুক্তির নিয়ম P6-08-এর `AttachmentPolicyService` ডাকে (এই card শুধু hook বসায়)। (ছ) Hook-এর ফল এক তালিকায়, সব ত্রুটি একসাথে। `journal_mgmt.enabled` বন্ধ থাকলে সব hook নিষ্ক্রিয় (R6.38)। System type-এর `allow_manual_entry`, `kind`, `code` PATCH-এ বদলায় না (R6.2)। `kind = custom` তৈরি `journal_types.allow_custom` চালু না থাকলে আটকায় (`JTY_CUSTOM_DISABLED`, নিচে তালিকায়)। Phase type activate/deactivate শুধু `owner_phase`-এর enable পথ ডাকতে পারে (service-এ `activatedBy` প্যারামিটার), HTTP `activate` endpoint system ও custom type-এ। Deactivate-এ ওই type-এর কোনো Draft বা Submitted journal থাকলে সতর্কতা সহ আটকায় |
| Contract | `JournalTypeRegistry::get(company, code): JournalTypeDto` (cache-সহ, না পেলে `JTY_TYPE_INACTIVE` নয়, `JTY_TYPE_NOT_FOUND` ছোড়ে), `::assertUsable(company, code, mode)`, `::checkLines(company, code, lines): AccountingError[]`। API (prefix `/accounting`, write-এ `Idempotency-Key`): `GET /journal-types` (filter: kind, is\_active), `POST /journal-types` (শুধু custom), `GET /journal-types/{code}`, `PATCH /journal-types/{code}` (body-তে `version`), `PUT /journal-types/{code}/account-ranges` (পুরো তালিকা বদলায়, এক transaction), `POST /journal-types/{code}/activate`, `POST /journal-types/{code}/deactivate` |
| Permission | `accounting.journal_type.view`, `.manage`, `.approve`। Maker-Checker: `strict` থেকে `standard`-এ নামানো, সংযুক্তি `required` থেকে নরম করা, ranges বদল, backdate/future সীমা বদল `.approve` লাগে (P1-04 change request, prefix `jtype:`) |
| Error | `JTY_TYPE_INACTIVE` (422), `JTY_TYPE_NOT_FOUND` (404), `JTY_MANUAL_NOT_ALLOWED` (422), `JTY_ACCOUNT_RANGE_VIOLATION` (422), `JTY_AUTO_REVERSE_NOT_ALLOWED` (422), `JTY_APPROVAL_RULE_MISSING` (422), `JTY_CUSTOM_DISABLED` (403), `JTY_CONFLICT` (409)। সবার shape {code, message, field, details, correlation\_id} |
| Audit | `accounting.journal_type.created`, `.updated`, `.activated`, `.deactivated`, `.ranges_changed` (আগে ও পরের মান সহ), `accounting.journal_type.setting_changed` |
| Acceptance | AC6.2, AC6.5-এর type অংশ |
| Test | `JournalTypeBehaviorTest`: (ক) inactive type-এ Draft `JTY_TYPE_INACTIVE`। (খ) `adjustment` manual API-তে `JTY_MANUAL_NOT_ALLOWED`, reclass পথে সফল। (গ) ranges: `allow` 5000-5999, line account 6100 তে আটকায়, `deny` overlap-এ জেতে। (ঘ) ছয়টা ত্রুটি এক Submit-এ একসাথে আসে। (ঙ) দুজন একসাথে PATCH করলে একজন `JTY_CONFLICT`। (চ) Maker-Checker একই ব্যক্তি অনুমোদন করলে SoD ত্রুটি। (ছ) flag বন্ধ থাকলে সব hook নিষ্ক্রিয়। (জ) company isolation: অন্য company-র type code অনুমানে 404 |
| Deliverable | Service, validator hook (P2-03 এ), controller, request class, OpenAPI, cache invalidation, test |
| Sub-tasks | (1) `JournalTypeRegistry` ও DTO ও cache। (2) Validator hook তিন মোডে। (3) Controller ও request validation। (4) `jtype:` ChangeApplier prefix। (5) OpenAPI। (6) Test |

### P6-03: Number series schema ও service

| Field | Specification |
| --- | --- |
| ধরন | `schema + service + api` |
| নির্ভর করে | P6-01, P2-11, P2-12 |
| অবরুদ্ধ করে | P6-04, P6-14, P6-15 |
| OD | OD-25a (gapless default), OD-25c (reset ও বছর) |
| উদ্দেশ্য | Series ধরে নম্বর বরাদ্দ। Gapless নম্বর শুধু Post transaction-এর ভেতরে। P2-11-এর রূপ (`JV-{CO}-{FY}-{SEQ}`) হুবহু default series `JV` হিসেবে থাকে |
| Schema | নিচের চারটা table। `acc_journals`-এ expand: `series_id BIGINT UNSIGNED NULL` ও composite FK `fk_acc_journals_series (company_id, series_id) → acc_number_series (company_id, id)` |
| DB flow | **`assign(journal)`** (Post transaction-এর ভেতরে, lock ক্রমে journal, approval request, account, period-এর পর): (১) series বাছাই, শুধু পড়া `acc_number_series_assignments` ও `acc_number_series` `FOR SHARE`। (২) `counter_key` ঠিক করা (yearly = period-এর fiscal\_year, monthly = period\_key, never = `ALL`)। (৩) `INSERT ... ON DUPLICATE KEY UPDATE id = id` দিয়ে counter সারি নিশ্চিত, তারপর `SELECT ... FOR UPDATE` সেই সারি। (৪) `last_no + 1`, overflow পরীক্ষা (`padding` অংকের বেশি হলে `NSR_SERIES_EXHAUSTED`)। (৫) `UPDATE counters SET last_no = ?`। (৬) `INSERT acc_voucher_allocations` (status `used`)। (৭) `voucher_no` ফেরত। Counter সারির lock commit পর্যন্ত থাকে, তাই ক্রম তৈরি হয়। `allocate_on = submit` series (শুধু non-gapless) একই কাজ Submit transaction-এ করে, ব্যর্থ হলে allocation `voided`। Lock ক্রম: series `FOR SHARE`, তারপর counter `FOR UPDATE` (এটি তালিকার শেষের কাছে, dimension value lock-এর আগে)। Deadlock-এ P2-12 পুরো Post বার তিনেক আবার চালায় |
| নিয়ম | R6.10 থেকে R6.16। `preview(company, seriesCode, date)` কিছু লেখে না, পরের সম্ভাব্য নম্বর দেখায় ("আনুমানিক" চিহ্নিত)। `format` save-এ parser যাচাই: `{SEQ}` বাধ্যতামূলক, অজানা token আটকায়, দৈর্ঘ্য ≤ 40 অক্ষর, উৎপন্ন নম্বর কখনো 40 অক্ষর ছাড়ায় না। দুই active series-এর resolved prefix সংঘর্ষ করলে save আটকায় (`NSR_PREFIX_CONFLICT`)। Assignment: একই (type, source\_type, priority)-এ overlap `effective_from` দুটো আটকায়। প্রথম allocation-এর পর `format`, `padding`, `reset_rule`, `gapless`, `allocate_on` লক (`NSR_SERIES_LOCKED`)। `numbering.default_gapless = true`, `numbering.default_reset_rule = yearly` (নতুন series-এর default) |
| Contract | `NumberSeriesService::next(company, seriesCode, date)` (Post transaction না থাকলে `NSR_NOT_IN_POST_TX`), `::assign(journal)`, `::preview(...)`, `::void(allocationId)` (শুধু submit-time series)। API: `GET/POST /number-series`, `GET/PATCH /number-series/{id}` (body-তে `version`), `PUT /number-series/{id}/assignments` (পুরো তালিকা), `GET /number-series/preview?type=&date=`, `GET /number-series/{id}/allocations` (cursor pagination) |
| Permission | `accounting.number_series.view`, `.manage`, `.approve`। Series তৈরি/বদল Maker-Checker (prefix `series:`) |
| Error | `NSR_NOT_IN_POST_TX` (500, প্রোগ্রামিং ত্রুটি), `NSR_SERIES_LOCKED` (409), `NSR_SERIES_NO_ASSIGNMENT` (422), `NSR_SERIES_INACTIVE` (422), `NSR_SERIES_EXHAUSTED` (422), `NSR_PREFIX_CONFLICT` (422), `NSR_CONFLICT` (409, version) |
| Audit | `accounting.number_series.created`, `.updated`, `.assigned`, `accounting.number_series.setting_changed`। প্রতি allocation-এর জন্য আলাদা audit নেই (allocation table নিজেই append-only প্রমাণ) |
| Acceptance | AC6.4, AC6.5, G6.1 |
| Test | `NumberSeriesTest`, `NumberSeriesFormatTest`, `GaplessConcurrencyTest`: G6.1 (50 Post, 7 ব্যর্থ, গর্ত নেই)। Format token সব। Priority ও `effective_from` বাছাই। Post পুনরায় চালালে একই নম্বর। `next` Post-এর বাইরে ডাকলে ত্রুটি। `valid_to` পার হওয়া series। Unique constraint ভাঙার চেষ্টা DB-তে আটকায় |
| Deliverable | Migration (৪ table + `acc_journals` expand), models, service, controller, test, OpenAPI |
| Sub-tasks | (1) Migration ও model। (2) Format parser ও token। (3) `assign`, `next`, `preview`। (4) Default series `JV` seed প্রতি company-তে ও P6-01-এর `default_series_code` সংযোগ। (5) Assignment resolver। (6) API। (7) Concurrency test |

**`acc_number_series`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | BIGINT UNSIGNED PK AI | না |  |
| `company_id` | BIGINT UNSIGNED FK | না |  |
| `code` | VARCHAR(20) | না | CHECK `^[A-Z][A-Z0-9_]{1,19}$` |
| `name` | VARCHAR(100) | না |  |
| `journal_type_code` | VARCHAR(30) | হ্যাঁ | NULL = সব type |
| `format` | VARCHAR(60) | না | যেমন `JV-{CO}-{FY}-{SEQ}` |
| `prefix_literal` | VARCHAR(30) | না | format থেকে হিসাব করা, সংঘর্ষ পরীক্ষার জন্য |
| `padding` | TINYINT UNSIGNED | না | 3 থেকে 12, default 6 |
| `gapless` | TINYINT(1) | না | Default 1 |
| `allocate_on` | ENUM('post','submit') | না | Default `post`। `submit` শুধু `gapless = 0` হলে |
| `reset_rule` | ENUM('yearly','monthly','never') | না | Default `yearly` |
| `valid_from`, `valid_to` | DATE | হ্যাঁ |  |
| `is_active` | TINYINT(1) | না | Default 1 |
| `first_allocated_at` | TIMESTAMP | হ্যাঁ | প্রথম allocation-এ বসে, লক চিহ্ন |
| `version` | INT UNSIGNED | না | Default 1 |
| `created_by`, `created_at`, `updated_at` |  |  |  |

- `uq_acc_ns_company_code (company_id, code)`; `uq (company_id, id)`।
- FK `(company_id, journal_type_code)` → `acc_journal_types (company_id, code)` `ON DELETE RESTRICT`।
- CHECK: `gapless = 1 OR allocate_on IN ('post','submit')`; `allocate_on = 'post' OR gapless = 0`; `valid_to IS NULL OR valid_to >= valid_from`; `format LIKE '%{SEQ}%'`।
- Index `(company_id, is_active, journal_type_code)`।
- P6-01-এর `acc_journal_types.default_series_code` পরে FK পায়: `fk_acc_jt_series (company_id, default_series_code) → acc_number_series (company_id, code)` (এই card-এ যোগ, NULL মান অনুমোদিত)।

**`acc_number_series_assignments`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | BIGINT UNSIGNED PK AI | না |  |
| `company_id`, `series_id` | BIGINT UNSIGNED | না |  |
| `journal_type_code` | VARCHAR(30) | না |  |
| `source_type` | VARCHAR(40) | হ্যাঁ | NULL = যেকোনো |
| `priority` | SMALLINT UNSIGNED | না | ছোট সংখ্যা আগে। Default 100 |
| `effective_from` | DATE | না |  |
| `created_by`, `created_at` |  |  |  |

- FK `(company_id, series_id)` → `acc_number_series (company_id, id)`; FK `(company_id, journal_type_code)` → `acc_journal_types`।
- `uq (company_id, journal_type_code, source_type_key, priority, effective_from)` যেখানে `source_type_key` একটা generated column `COALESCE(source_type, '')`।
- Index `(company_id, journal_type_code, effective_from)`।

**`acc_number_series_counters`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | BIGINT UNSIGNED PK AI | না |  |
| `company_id`, `series_id` | BIGINT UNSIGNED | না |  |
| `counter_key` | VARCHAR(10) | না | fiscal\_year, period\_key, অথবা `ALL` |
| `last_no` | BIGINT UNSIGNED | না | Default 0 |
| `cutover_base_no` | BIGINT UNSIGNED | হ্যাঁ | P6-04 লেখে |
| `updated_at` | TIMESTAMP | না |  |

- FK `(company_id, series_id)`; `uq (company_id, series_id, counter_key)`; `uq (company_id, id)`।
- CHECK `last_no <= 999999999999`।

**`acc_voucher_allocations`** (append-only, `Schema::appendOnly()`; UPDATE শুধু `status` `used` থেকে `voided`-এ, trigger-এ)

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | BIGINT UNSIGNED PK AI | না |  |
| `company_id`, `series_id` | BIGINT UNSIGNED | না |  |
| `counter_key` | VARCHAR(10) | না |  |
| `seq_no` | BIGINT UNSIGNED | না |  |
| `voucher_no` | VARCHAR(60) | না |  |
| `journal_id` | BIGINT UNSIGNED | না |  |
| `status` | ENUM('used','voided') | না | Default `used` |
| `allocated_at` | TIMESTAMP | না |  |

- `uq_acc_va_voucher (company_id, voucher_no)`; `uq_acc_va_seq (company_id, series_id, counter_key, seq_no)`; `uq_acc_va_journal (company_id, journal_id)`।
- FK `(company_id, series_id)`; FK `(company_id, journal_id)` → `acc_journals (company_id, id)`।
- Index `(company_id, series_id, counter_key, seq_no)` (RC1n স্ক্যান)।

### P6-04: Voucher counter migration ও VoucherService facade

| Field | Specification |
| --- | --- |
| ধরন | `command + service` |
| নির্ভর করে | P6-03, P2-11, P2-12, P1-05 |
| অবরুদ্ধ করে | P6-15 |
| OD | OD-25b (পুরোনো counter-এর ভাগ্য) |
| উদ্দেশ্য | পুরোনো `acc_voucher_counters` থেকে নতুন series counter-এ নিরাপদ স্থানান্তর। `VoucherService` (P2-11) facade হয়: flag বন্ধে পুরোনো পথ, চালুতে `NumberSeriesService`। পুরোনো নম্বরের রূপ হুবহু, গর্ত বা নকল নয়। Rollback সম্ভব |
| Schema | নতুন table নেই। `acc_number_series_counters.cutover_base_no` (P6-03) লেখা হয়। `acc_voucher_counters` অপরিবর্তিত, শুধু cutover-এর পর পড়ার |
| DB flow | **Dry-run:** শুধু `SELECT`, কোনো lock নয়। **Cutover (প্রতি company এক transaction):** (১) maintenance window চালু আছে কি যাচাই। (২) `SELECT ... FROM acc_voucher_counters WHERE company_id = ? FOR UPDATE` (চলমান পুরোনো-পথ Post commit পর্যন্ত অপেক্ষা করে, তাই কোনো নম্বর হারায় না)। (৩) প্রতি `(fiscal_year, series JV)`: নতুন counter `INSERT ... ON DUPLICATE KEY UPDATE last_no = GREATEST(last_no, VALUES(last_no)), cutover_base_no = VALUES(last_no)`। (৪) দুই পক্ষ তুলনা: নতুন `last_no` = পুরোনো `last_no`, নইলে rollback ও `NSR_CUTOVER_PRECONDITION`। (৫) `acc_enablement_log`-এ সারি। (৬) commit। Flag চালু আলাদা ধাপ (Maker-Checker, P1-04), cutover-এর সাথে একই transaction নয়। **Rollback:** flag আগে বন্ধ, তারপর একই lock ক্রমে নতুন counter `FOR UPDATE`, পুরোনো `last_no = GREATEST(পুরোনো, নতুন)`, এক transaction। Lock ক্রম: পুরোনো counter তারপর নতুন counter (কোনো Post-পথ দুটো একসাথে নেয় না, তাই চক্র নেই) |
| নিয়ম | R6.12, R6.16, R6.17, R6.38। Facade: `VoucherService::next(company, journal)` প্রথমে `AccountingFeatureService::enabled(company, 'journal_mgmt')` দেখে। বন্ধ হলে P2-11-এর কোড হুবহু। চালু হলে `NumberSeriesService::assign`। P2-12 শুধু facade ডাকে, সরাসরি দুটোর কোনোটি নয়। চালুর পর পুরোনো `acc_voucher_counters`-এ কেউ লেখে না (trigger দিয়ে যাচাই: cutover-এর পরের `UPDATE` ব্যর্থ, rollback command ছাড়া)। Legacy table একটা period-close চক্র পর contract migration-এ বাদ (OD-25b প্রস্তাব), তার আগে নয়। Cutover আবার চালালে একই ফল। Flag চালু থাকলে cutover আবার চললে `NSR_CUTOVER_PRECONDITION` |
| Contract | CLI `accounting:numbering-cutover {--company=} {--dry-run} {--confirm}` (--confirm ছাড়া সবসময় dry-run)। CLI `accounting:numbering-rollback {--company=} {--confirm}`। API `POST /number-series/cutover?dry_run=1` (শুধু dry-run, আসল cutover HTTP-তে নেই)। Dry-run উত্তর: `[{company, fiscal_year, legacy_last_no, new_last_no, will_set, mismatch}]`। `VoucherService::next` স্বাক্ষর অপরিবর্তিত |
| Permission | API dry-run: `accounting.number_series.manage`। CLI: operator (shell প্রবেশাধিকার), লগে অপারেটর নাম। Flag চালু: Maker-Checker |
| Error | `NSR_CUTOVER_PRECONDITION` (কারণ তালিকাসহ: অমিল, window নেই, flag আগেই চালু, backup নিশ্চিত নয়)। CLI-তে non-zero exit code |
| Audit | `accounting.number_series.cutover_dry_run`, `accounting.number_series.cutover`, `accounting.number_series.rollback`, `accounting.journal_mgmt.flag_changed` |
| Acceptance | AC6.3, AC6.4, G6.2 |
| Test | `VoucherCutoverTest`: G6.2 (1250 থেকে 1251)। অমিল থাকলে থামে। Cutover চলার সময় ১০টা পুরোনো-পথ Post চলমান থাকলে নম্বর হারায় না, নকল হয় না। Rollback-এর পর পুরোনো পথে পরের নম্বর ক্রমিক। আবার cutover চালালে একই ফল। Flag বন্ধে P2-11-এর পুরোনো test হুবহু পাস। Production-copy-তে রিহার্সাল |
| Deliverable | দুই command, facade, trigger, runbook অংশ (P6-15-এ জমা), test |
| Sub-tasks | (১) Facade ও feature check। (২) Cutover command ও dry-run। (৩) Rollback command। (৪) Legacy write-guard trigger। (৫) P2-12 integration। (৬) `acc_enablement_log` লেখা। (৭) Concurrency test |

### P6-05: Document date, posting date ও backdate

| Field | Specification |
| --- | --- |
| ধরন | `schema + service` |
| নির্ভর করে | P6-02, P2-03, P1-10 |
| অবরুদ্ধ করে | P6-06, P6-07, P6-14 |
| OD | OD-26b (backdate সীমা), OD-26c (document date পরে হলে) |
| উদ্দেশ্য | `journal_date` (posting date) আর `document_date` (মূল নথির তারিখ) আলাদা করা, আর type ধরে backdate ও future সীমা |
| Schema | `acc_journals.document_date DATE NULL` (expand-only)। Index `idx_acc_journals_docdate (company_id, document_date)`। P1-10 trigger-এর অপরিবর্তনীয় তালিকায় `document_date` ও `series_id` যোগ |
| DB flow | Draft লেখা: `document_date` সরাসরি journal সারিতে, কোনো নতুন lock নেই। Type সীমা cache থেকে (P6-02)। Submit ও Post-এ `JournalDateRule::check` আবার, কোনো নতুন lock নয়। Migration শুধু `ADD COLUMN ... NULL` (online, বিদ্যমান সারি NULL থাকে, পড়ার সময় `COALESCE(document_date, journal_date)`) |
| নিয়ম | R6.18, R6.19। `document_date` বাদ দিলে `journal_date` বসে। `requires_document_date = 1` type-এ ফাঁকা থাকলে Submit আটকায় (`JRN_DOCUMENT_DATE_REQUIRED`)। Document date > journal\_date হলে `journal.document_date_policy`: `allow` চুপচাপ, `warn` Draft-এ সতর্কতা (সফল, `warnings[]`), `block` (`JRN_DOCUMENT_DATE_AFTER_POSTING`)। Backdate: `journal_date < today - backdate_days_limit` হলে `JTY_BACKDATE_LIMIT` (field `journal_date`, details: limit, earliest\_allowed)। Future একইভাবে। `today` কোম্পানির timezone-এ। Type সীমা `PeriodGuard` কখনো শিথিল করে না, `PeriodGuard` আগে চলে। সীমা কমালে আগের Draft-এর Submit-এ স্পষ্ট ত্রুটি। System type-এর default সীমা `NULL` (OD-26b (ক)) |
| Contract | `JournalDateRule::check(journal, type, mode): AccountingError[]` (পাশাপাশি `warnings`)। বিদ্যমান journal API-তে নতুন ঐচ্ছিক field `document_date`। Response-এ `document_date` ও `warnings[]` |
| Permission | বিদ্যমান journal draft permission। Policy বদল: `accounting.journal_type.manage` (+ Maker-Checker `block` থেকে `allow`) |
| Error | `JTY_BACKDATE_LIMIT` (422), `JRN_DOCUMENT_DATE_REQUIRED` (422), `JRN_DOCUMENT_DATE_AFTER_POSTING` (422), `JRN_PERIOD_LOCKED` (বিদ্যমান) |
| Audit | `accounting.journal.document_date_policy_changed` (setting), বিদ্যমান draft audit-এ `document_date` আগে-পরে |
| Acceptance | AC6.2, G6.8 |
| Test | `JournalDateRuleTest`: G6.8 (৫ দিন সীমা, ১০-০৮ সফল, ১০-০৭ ব্যর্থ, period বন্ধে `JRN_PERIOD_LOCKED`)। `warn`/`block`/`allow` তিন মোড। Timezone সীমানা (মধ্যরাত)। `document_date` ফাঁকা হলে রিপোর্টে `journal_date`। Post-এর পর `document_date` বদলাতে trigger আটকায়। Flag বন্ধে নতুন ত্রুটি আসে না |
| Deliverable | Migration, rule class, validator hook, API field, OpenAPI, test |
| Sub-tasks | (১) Column migration। (২) P1-10 trigger হালনাগাদ। (৩) `JournalDateRule`। (৪) Draft/Submit/Post hook। (৫) API field ও warnings। (৬) Test |

### P6-06: Journal template

| Field | Specification |
| --- | --- |
| ধরন | `schema + service + api` |
| নির্ভর করে | P6-02, P6-09, P2-03, P1-04 |
| অবরুদ্ধ করে | P6-14, Phase 12 (recurring) |
| OD | OD-26a (সম্পাদনার অনুমোদন) |
| উদ্দেশ্য | বারবার হওয়া journal-এর ছক। Parameter বসিয়ে সবসময় Draft তৈরি। সম্পাদনা নতুন version, দ্বিতীয় অনুমোদন সহ |
| Schema | নিচের ছয়টা table। `acc_journals` বদলায় না (ব্যবহারের সূত্র `_uses`-এ) |
| DB flow | **create-draft (একটা transaction):** (১) `_versions` পড়া (শুধু `active`, lock নেই)। (২) parameter যাচাই ও `ExpressionEvaluator` (DB ছোঁয় না)। (৩) `JournalService::createDraft` (P2-03, Draft মোডে validator সব নিয়ম চালায়, dimension default `applyDefaults` সহ)। (৪) `INSERT acc_journal_template_uses` (`uq (company_id, journal_id)`)। কোনো ledger lock নেই। **সম্পাদনা:** এক transaction: `_templates` `FOR UPDATE`, `version` মেলানো, নতুন `_versions` সারি `status = pending`, change request `jtemplate:<id>` (P1-04)। অনুমোদনে এক transaction: `_templates` `FOR UPDATE`, পুরোনো `active` → `superseded`, নতুন `pending` → `active`, `_templates.current_version` বদল। ব্যবহৃত version-এর সারি কখনো UPDATE হয় না (trigger শুধু `status` বদলাতে দেয়) |
| নিয়ম | R6.20, R6.21, R6.22। Expression sandbox: সংখ্যা, parameter নাম, `+ - * /`, `( )`, `ROUND(x, n)`, `MIN`, `MAX`, `ABS`। কোনো function call বা property access নেই। Parser নিজের (eval নয়)। সব গণনা DECIMAL(20,4)-এ (BCMath), ভাগে শূন্য `JTY_TEMPLATE_EXPR_INVALID`। প্রতি line একটা Debit বা Credit expression। Line-এ account স্থির (`account_id`) বা parameter (`account_param`)। Retired বা inactive account থাকলে create-draft-এ ধরা পড়ে (তৈরির সময় নয়, ব্যবহারের সময় যাচাই, R6.21)। Share scope `private`, `role`, `company`। `company` scope-এ নিতে বা সম্পাদনায় `template.edit_requires_approval` সবসময় লাগু। Template ব্যবহার Draft-এ সীমিত: সব আবশ্যিক field (tax, কর, ইত্যাদি) validator-এ। Retire করলে নতুন Draft হয় না, পুরোনো Draft অক্ষত |
| Contract | `JournalTemplateService::createDraft(company, templateId, params, journalDate, idempotencyKey): Journal`। API: `GET/POST /journal-templates`, `GET /journal-templates/{id}`, `POST /journal-templates/{id}/versions` (সম্পাদনার অনুরোধ), `POST /journal-templates/{id}/retire`, `PUT /journal-templates/{id}/shares`, `POST /journal-templates/{id}/create-draft`। `GET /journal-templates?usable=1` শুধু ব্যবহারযোগ্যগুলো |
| Permission | `accounting.journal_template.view`, `.manage`, `.use`, `.approve`। Company scope-এ নেওয়া ও সম্পাদনা অনুমোদন `.approve`, সম্পাদক ≠ অনুমোদক |
| Error | `JTY_TEMPLATE_PARAM_INVALID` (422, field `params.<name>`), `JTY_TEMPLATE_EXPR_INVALID` (422, details: position), `JTY_TEMPLATE_NOT_FOUND` (404), `JTY_TEMPLATE_PENDING_APPROVAL` (409, শুধু তথ্য: আগের version চলে), `JTY_CONFLICT` (409) |
| Audit | `accounting.journal_template.created`, `.edit_requested`, `.edit_approved`, `.retired`, `.used`, `accounting.journal_template.setting_changed` |
| Acceptance | AC6.6, G6.9 |
| Test | `JournalTemplateTest`, `TemplateExpressionTest`: G6.9 (333.3333 / 333.3333 / 333.3334)। Injection: `system('x')`, `base.__class__`, `1/0`। অনুমোদনের আগে আগের version। ব্যবহৃত version বদলানো ট্রিগারে আটকায়। Share scope ধরে দৃশ্যতা (অন্য role-এ 404)। দুজন একসাথে সম্পাদনায় `JTY_CONFLICT`। Idempotency-Key একই হলে একই Draft। Template থেকে Post সরাসরি অসম্ভব |
| Deliverable | Migration (৬ table), models, evaluator, service, controller, OpenAPI, test |
| Sub-tasks | (১) Migration ও trigger। (২) `ExpressionEvaluator`। (৩) Service: create, edit, approve, retire, createDraft। (৪) `jtemplate:` ChangeApplier prefix। (৫) Share ও দৃশ্যতা scope। (৬) API। (৭) Test |

**`acc_journal_templates`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | BIGINT UNSIGNED PK AI | না |  |
| `company_id` | BIGINT UNSIGNED FK | না |  |
| `code` | VARCHAR(40) | না |  |
| `name` | VARCHAR(150) | না |  |
| `journal_type_code` | VARCHAR(30) | না | Draft-এর type |
| `share_scope` | ENUM('private','role','company') | না | Default `private` |
| `owner_id` | BIGINT UNSIGNED | না |  |
| `current_version` | INT UNSIGNED | হ্যাঁ | NULL = এখনো কোনো active নেই |
| `status` | ENUM('active','retired') | না |  |
| `version` | INT UNSIGNED | না | optimistic check |
| `created_at`, `updated_at` | TIMESTAMP | না |  |

- `uq (company_id, code)`; `uq (company_id, id)`; FK `(company_id, journal_type_code)` → `acc_journal_types`। Index `(company_id, status, share_scope)`।

**`acc_journal_template_versions`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | BIGINT UNSIGNED PK AI | না |  |
| `company_id`, `template_id` | BIGINT UNSIGNED | না |  |
| `version_no` | INT UNSIGNED | না |  |
| `status` | ENUM('pending','active','superseded','rejected') | না |  |
| `params` | JSON | না | `[{name, type, required, min, max, default}]` |
| `narration_tpl` | VARCHAR(500) | হ্যাঁ |  |
| `change_request_id` | BIGINT UNSIGNED | হ্যাঁ | P1-04 |
| `created_by`, `created_at`, `approved_by`, `approved_at` |  |  |  |

- FK `(company_id, template_id)`; `uq (company_id, template_id, version_no)`; `uq (company_id, id)`। কোনো `template_id`-তে `pending` সর্বোচ্চ একটা (generated column `pending_key` ও unique)।

**`acc_journal_template_lines`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id`, `company_id`, `version_id` | BIGINT UNSIGNED | না |  |
| `line_no` | SMALLINT UNSIGNED | না |  |
| `account_id` | BIGINT UNSIGNED | হ্যাঁ | স্থির account |
| `account_param` | VARCHAR(40) | হ্যাঁ | parameter নাম (দুটোর ঠিক একটা ভরা, CHECK) |
| `debit_expr`, `credit_expr` | VARCHAR(300) | হ্যাঁ | ঠিক একটা ভরা (CHECK) |
| `narration_tpl` | VARCHAR(300) | হ্যাঁ |  |

- FK `(company_id, version_id)` → `_versions (company_id, id)`; FK `(company_id, account_id)` → `acc_accounts`; `uq (company_id, version_id, line_no)`।

**`acc_journal_template_line_dimensions`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id`, `company_id`, `template_line_id`, `dimension_id` | BIGINT UNSIGNED | না |  |
| `value_id` | BIGINT UNSIGNED | হ্যাঁ | স্থির value |
| `value_param` | VARCHAR(40) | হ্যাঁ | parameter নাম (একটা ভরা) |

- FK `(company_id, template_line_id)`, `(company_id, dimension_id)`, `(company_id, value_id)` সব composite। `uq (company_id, template_line_id, dimension_id)`।

**`acc_journal_template_shares`**: `id`, `company_id`, `template_id`, `role_id`; FK composite; `uq (company_id, template_id, role_id)`।

**`acc_journal_template_uses`**: `id`, `company_id`, `template_id`, `version_id`, `journal_id`, `used_by`, `used_at`; `uq (company_id, journal_id)`; FK `(company_id, journal_id)` → `acc_journals`; index `(company_id, template_id, used_at)`।

### P6-07: Bulk journal import

| Field | Specification |
| --- | --- |
| ধরন | `schema + job + api` |
| নির্ভর করে | P6-02, P6-10, P2-03, P1-12 |
| অবরুদ্ধ করে | P6-14, Phase 16 (data migration) |
| OD | OD-27a (সীমা ও মোড) |
| উদ্দেশ্য | CSV বা xlsx থেকে অনেক journal staging-এ, যাচাই, তারপর আংশিক বা পুরো গ্রহণ, ফল শুধু Draft। কখনো auto-post নয় |
| Schema | নিচের তিনটা table |
| DB flow | **Upload:** ফাইল webroot-এর বাইরে, `acc_journal_import_batches` সারি (`uploaded`)। **Parse job (queue `accounting-import`, company প্রতি একটা):** পৃষ্ঠা ধরে ১,০০০ সারি করে `INSERT` `_rows` ও `_groups`, status `parsed`। **Validate job:** প্রতি group আলাদা, কোনো ledger lock নেই। `JournalImportService::validate` = account/dimension code মানচিত্র (মেমোরিতে), তারপর পুরো Draft-মোড validator dry-run (P2-03 `validateDraft(payload)`, কিছু লেখে না)। Status `validated`। **Accept:** `POST .../accept` batch `FOR UPDATE` নিয়ে `accepting` করে, job queue-তে। **Commit job:** প্রতি group এক transaction: `SELECT group FOR UPDATE`, `JournalService::createDraft` (`origin = import`, `source_type = import_batch`, `source_id = group id`), `UPDATE group SET journal_id, status = 'committed'`। `source_active` unique (P2-03) একই group-এর দ্বিতীয় Draft আটকায়। ব্যর্থ group `failed`, অন্যরা এগোয়। শেষে batch `completed` বা `partially_accepted`। Lock ক্রম প্রতি group-এ: group → journal (নতুন) → বাকি ledger lock আসে না (শুধু Draft) |
| নিয়ম | R6.23, R6.7। সীমা `journal_import.max_rows` (default 5,000)। Mode `all_or_nothing` বা `partial` (batch-এ override, accept-এ lock)। `all_or_nothing`-এ একটা invalid group থাকলে accept আটকায়। Duplicate ফাইল: একই `file_sha256`, একই company, ৯০ দিন, `force=1` ও কারণ ছাড়া `JTY_IMPORT_DUPLICATE_FILE`। ফাইল নিয়ম: শুধু `.csv` ও `.xlsx`, `.xlsm` অগ্রহণ, MIME sniff, আকার ≤ `attachment.max_bytes`-এর import-সীমা (20 MB), ZIP আনপ্যাক সীমা ১০০ MB ও অনুপাত ১০০:১, UTF-8 (BOM সহ)। কলাম: `group_ref`, `journal_date`, `document_date`, `account_code`, `debit`, `credit`, `narration`, `dim:<code>` (যত খুশি, সক্রিয় dimension-এর)। সংখ্যা DECIMAL(20,4), বেশি দশমিক গোল নয় বরং ত্রুটি। প্রতি group = এক journal, balance দরকার। Import type `import`: `P6-15`-এ সক্রিয়। Import-কারী নিজে সেই Draft Post করতে পারে না (SoD P2-10)। Formula injection: নিজের export-এ `=`/`+`/`-`/`@` আগে `'` |
| Contract | API: `POST /journal-imports` (multipart; `file`, `journal_type`, `accept_mode`, `force`, `force_reason`), `GET /journal-imports/{id}`, `GET /journal-imports/{id}/rows?group=&status=` (cursor), `POST /journal-imports/{id}/accept` (body: `group_ids[]` অথবা `all_valid=1`), `POST /journal-imports/{id}/cancel`, \`GET /journal-imports/sample-file?format=csv |
| Permission | `accounting.journal_import.view`, `.manage` (upload, cancel), `.commit` (accept) |
| Error | `JTY_IMPORT_FILE_INVALID` (422), `JTY_IMPORT_DUPLICATE_FILE` (409), `JTY_IMPORT_TOO_MANY_ROWS` (422), `JTY_IMPORT_ROW_INVALID` (সারির `error_codes` তালিকায়, HTTP-তে নয়), `JTY_IMPORT_NOT_VALIDATED` (409), `JTY_IMPORT_MODE_VIOLATION` (409, `all_or_nothing`-এ invalid group), `JTY_CONFLICT` |
| Audit | `accounting.journal_import.uploaded`, `.validated`, `.accepted`, `.committed`, `.cancelled`, `accounting.journal_import.setting_changed` |
| Acceptance | AC6.7, G6.7 |
| Test | `JournalImportTest`: G6.7 (১২০ সারি, ৩০ group, ৪ invalid, ২৬ Draft, পুনরায় accept-এ ২৬)। `all_or_nothing` ও একটা invalid। ৫,০০১ সারি। Duplicate ফাইল ও `force`। `xlsm`, ZIP bomb, ভুল encoding। Commit-এর মাঝে job মরলে পুনঃচালু নকলহীন। Auto-post কোথাও নেই (static ও runtime)। ৫,০০০ সারির যাচাই ৬০ সেকেন্ডের মধ্যে (benchmark)। Import-কারী নিজে Post করতে গেলে SoD |
| Deliverable | Migration, jobs, service, controller, sample file (CSV ও xlsx) ও কলাম বর্ণনা, OpenAPI, test |
| Sub-tasks | (১) Migration। (২) Upload ও ফাইল যাচাই। (৩) Parse job। (৪) Validate (dry-run validator hook P2-03-এ `validateDraft`)। (৫) Accept ও commit job। (৬) Sample file। (৭) Security test। (৮) Benchmark |

**`acc_journal_import_batches`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id`, `company_id` | BIGINT UNSIGNED | না |  |
| `file_name` | VARCHAR(255) | না |  |
| `file_sha256` | CHAR(64) | না |  |
| `storage_path` | VARCHAR(500) | না | webroot-এর বাইরে |
| `journal_type_code` | VARCHAR(30) | না | সাধারণত `import` |
| `status` | ENUM('uploaded','parsed','validating','validated','accepting','completed','partially\_accepted','failed','cancelled') | না |  |
| `accept_mode` | ENUM('all\_or\_nothing','partial') | না |  |
| `row_count`, `group_count`, `valid_group_count`, `committed_group_count` | INT UNSIGNED | না |  |
| `force_reason` | VARCHAR(300) | হ্যাঁ |  |
| `created_by`, `created_at`, `updated_at` |  |  |  |

- `uq (company_id, id)`; index `(company_id, file_sha256, created_at)`, `(company_id, status)`। FK `(company_id, journal_type_code)`।

**`acc_journal_import_groups`**: `id`, `company_id`, `batch_id`, `group_ref VARCHAR(60)`, `status ENUM('pending','valid','invalid','committed','failed')`, `journal_id NULL`, `error_codes JSON`, `total_debit`, `total_credit DECIMAL(20,4)`। FK `(company_id, batch_id)`, `(company_id, journal_id)`; `uq (company_id, batch_id, group_ref)`; `uq (company_id, id)`।

**`acc_journal_import_rows`**: `id`, `company_id`, `batch_id`, `group_id`, `row_no INT UNSIGNED`, `raw JSON`, `account_id NULL`, `debit`, `credit DECIMAL(20,4)`, `error_codes JSON NULL`। FK `(company_id, group_id)`; `uq (company_id, batch_id, row_no)`; index `(company_id, group_id)`।

### P6-08: Attachment policy, comment, tag, copy, clone, filter, search

| Field | Specification |
| --- | --- |
| ধরন | `schema + job + api` |
| নির্ভর করে | P6-02, P2-02, P2-03, P0-02, P1-12 |
| অবরুদ্ধ করে | P6-14, P6-15 |
| OD | OD-27b (malware scanner) |
| উদ্দেশ্য | সংযুক্তির নিয়ম ও malware scan, comment, tag, copy, clone, saved filter ও search। এগুলো ledger নয়, `posted_hash`-এর অংশ নয় |
| Schema | সাতটা নতুন table (নিচে)। বিদ্যমান journal সংযুক্তি table (P2-02-তে যে নামে, Entry gate-এ মূল কোডে নিশ্চিত করা) অপরিবর্তিত, স্ক্যান অবস্থা `acc_attachment_scans`-এ আলাদাভাবে |
| DB flow | **Upload:** বিদ্যমান upload path-এ policy যাচাই (ext, size), সারি `acc_attachment_scans(status = pending)` একই transaction-এ, তারপর `AttachmentScanJob` queue-তে (queue `accounting-scan`)। **Scan job:** সারি `FOR UPDATE` (আলাদা, কোনো ledger lock নয়), scanner ডাকা, `clean` বা `infected` (quarantine স্থানান্তর) বা বন্ধে `pending` থেকে backoff। **Submit:** শুধু পড়া (`clean` গণনা)। **Post:** আবার পড়া, সাধারণ স্ন্পশট, কোনো নতুন lock নেই। **Comment/tag/filter:** নিজেদের table-এ INSERT। **Copy/clone:** `JournalService::createDraft` + `acc_journal_copy_links` INSERT, এক transaction |
| নিয়ম | R6.7, R6.24, R6.25, R6.26। Policy বাছাই: type-ভিত্তিক আগে, তারপর company default (`journal_type_code IS NULL`)। Scan সবুজ (`clean`) না হলে download ও Submit-এ গণনা বন্ধ। Scanner বন্ধ থাকলে `pending` (fail-closed)। `infected` সংযুক্তি Submit আটকায় (`JTY_ATTACH_INFECTED`)। Posted journal-এর সংযুক্তি `retention_until` পর্যন্ত মুছা যায় না। Download শুধু signed short-lived URL (60 সেকেন্ড), journal দেখার permission থাকলে। Comment plain text ≤ 2,000 অক্ষর, HTML বাদ। লেখক `journal.comment_edit_minutes` পর্যন্ত সম্পাদনা করতে পারে (আগের পাঠ `edit_history` JSON-এ)। Comment মুছা যায় না। Copy করলে line, dimension, narration কপি; নতুন তারিখ, Draft; `voucher_no`, `status`, `posted_*`, `source_*` কখনো নয়। Clone = copy + tag + সংযুক্তির সূত্র (ফাইল আলাদা কপি নয়, একই storage ও নতুন scan সারি)। Saved filter: JSON whitelist (`status`, `journal_type`, `date_from/to`, `account_id`, `dimension`, `tag`, `amount_from/to`, `created_by`, `text`) । তালিকার বাইরে key গ্রহণ নয়। Filter কখনো access বাড়ায় না (query-তে company/permission scope সবসময় প্রথমে বসে)। Search: `GET /journals/search?q=` voucher\_no প্রিফিক্স, narration FULLTEXT (ngram parser, বাংলা ও ইংরেজি), tag, অঙ্ক, তারিখ। Page সাইজ ≤ 100, cursor |
| Contract | API: `GET/PUT /attachment-policies`, `GET/POST /journal-tags`, `PUT /journals/{id}/tags`, `GET /journals/{id}/attachments/{aid}/download`, `GET/POST /journals/{id}/comments`, `PATCH /journals/{id}/comments/{cid}`, `POST /journals/{id}/copy`, `POST /journals/{id}/clone`, `GET /journals/search`, `GET/POST/PATCH/DELETE /saved-filters`। Job `AttachmentScanJob` |
| Permission | `accounting.attachment_policy.manage`, `accounting.journal_comment.create`, `accounting.journal_tag.manage`; copy/clone-এ বিদ্যমান Draft তৈরি permission; search-এ বিদ্যমান journal দেখার permission; saved filter ব্যক্তিগত (শুধু নিজের দেখা ও বদলানো) |
| Error | `JTY_ATTACH_INFECTED` (422), `JTY_ATTACH_NOT_SCANNED` (409, download/Submit সবুজ নয়), `JTY_ATTACH_TYPE_NOT_ALLOWED` (422), `JTY_ATTACH_TOO_LARGE` (413), `JTY_ATTACH_RETAINED` (409, মুছতে চাইলে), `JTY_COMMENT_EDIT_EXPIRED` (409), `JTY_FILTER_INVALID` (422), `JTY_COPY_SOURCE_INVALID` (422) |
| Audit | `accounting.attachment_policy.changed`, `accounting.journal_attachment.scanned`, `.quarantined`, `.downloaded`, `accounting.journal_comment.created`, `.edited`, `accounting.journal_tag.changed`, `accounting.journal.copied`, `.cloned` |
| Acceptance | AC6.13 |
| Test | `AttachmentPolicyTest`: EICAR ফাইল প্রতিরোধ, scanner বন্ধে `pending` ও Submit আটকায়, `xlsm` অগ্রহণ, retention থেকে মুছা আটকায়, IDOR (অন্য company-র `aid` 404)। Comment সম্পাদনা সময়সীমা। Copy: `voucher_no`, `status` কপি হয় নি। Filter JSON injection। Search বাংলা narration। সবগুলোতে `posted_hash` অপরিবর্তিত। Tag/comment Posted journal-তে যোগ হয় |
| Deliverable | Migration (৭ table), scan job, scanner adapter interface (বাস্তব সংযোগ OD-27b অনুযায়ী), services, controllers, OpenAPI, test |
| Sub-tasks | (১) Migration। (২) Policy resolver ও upload hook। (৩) Scan job ও adapter। (৪) Submit/Post gate। (৫) Download signed URL। (৬) Comment, tag। (৭) Copy, clone। (৮) Saved filter ও search। (৯) Test |

**`acc_attachment_policies`**: `id`, `company_id`, `journal_type_code NULL`, `scan_required TINYINT(1)`, `max_bytes INT UNSIGNED`, `allowed_ext JSON`, `retention_years TINYINT UNSIGNED`, `version`, `updated_by`, `updated_at`। `uq (company_id, journal_type_code_key)` (generated `COALESCE(journal_type_code, '')`), `uq (company_id, id)`, FK `(company_id, journal_type_code)`। CHECK `retention_years BETWEEN 1 AND 30`।

**`acc_attachment_scans`**: `id`, `company_id`, `attachment_id`, `status ENUM('pending','clean','infected','error')`, `scanner VARCHAR(40)`, `attempts TINYINT`, `scanned_at NULL`, `retention_until DATE NULL`, `quarantine_path NULL`। `uq (company_id, attachment_id)`; index `(company_id, status, id)` (backlog দেখা)।

**`acc_journal_comments`**: `id`, `company_id`, `journal_id`, `author_id`, `body VARCHAR(2000)`, `edit_history JSON NULL`, `created_at`, `edited_at NULL`। FK `(company_id, journal_id)`; index `(company_id, journal_id, created_at)`।

**`acc_journal_tags`**: `id`, `company_id`, `name VARCHAR(40)`, `color CHAR(7) NULL`, `is_active`। `uq (company_id, name)`; `uq (company_id, id)`। **`acc_journal_tag_links`**: `company_id`, `journal_id`, `tag_id`, `created_by`, `created_at`; PK `(company_id, journal_id, tag_id)`; FK composite দুটো; index `(company_id, tag_id, journal_id)`।

**`acc_saved_filters`**: `id`, `company_id`, `user_id`, `name VARCHAR(80)`, `scope ENUM('journals','ledger')`, `filter JSON`, `created_at`, `updated_at`। `uq (company_id, user_id, scope, name)`। **`acc_journal_copy_links`**: `id`, `company_id`, `source_journal_id`, `new_journal_id`, `mode ENUM('copy','clone')`, `created_by`, `created_at`। FK composite দুটো; `uq (company_id, new_journal_id)`; index `(company_id, source_journal_id)`। Journal narration-এ FULLTEXT index `ft_acc_journals_narration` বিদ্যমান table-এ expand।

### P6-09: Dimension framework schema ও service

| Field | Specification |
| --- | --- |
| ধরন | `schema + service + job` |
| নির্ভর করে | P1-10, P3-01, P2-03 |
| অবরুদ্ধ করে | P6-06, P6-10, P6-11, P6-12, P6-13 |
| OD | OD-28b (hierarchy ইতিহাস), OD-28c (mirror) |
| উদ্দেশ্য | সাধারণ dimension কাঠামো। Built-in `cost_center` (বিদ্যমান `cost_center_id`-এর প্রতিফলন)। প্রতি line-এ সর্বোচ্চ ৪টা dimension, `acc_journal_line_dimensions`-এ। পুরোনো line-এর backfill |
| Schema | নিচের চারটা table। `acc_journal_lines` বদলায় না |
| DB flow | **Dimension/value তৈরি:** এক transaction, সারি `INSERT`, tree হলে parent `FOR SHARE` তারপর `path` বসানো। **Line লেখা (`DimensionLineWriter`):** journal-এর অংশে একই transaction-এ line INSERT, তারপর প্রতি line-এ `acc_journal_line_dimensions` INSERT ও `cost_center_id` সমানভাবে বসানো (`cost_center` dimension-এর মান = `cost_center_id`, দুটোর অমিলে rollback)। Draft PUT-এ পুরো line-সেট বদলালে পুরোনো dimension সারি `DELETE` + নতুন `INSERT` (Draft-এ অনুমতি, trigger Posted line-এর আটকায়)। **Backfill job (cursor):** `DimensionBackfillJob` প্রতি বারে ১০,০০০ line (id সর্টেড), `INSERT IGNORE ... SELECT` যেখানে `cost_center_id IS NOT NULL` আর সারি নেই, cursor `acc_dimension_backfill_state`-এ (company, last\_line\_id)। Posted line-এর backfill trigger একদিকে একটা প্রতিক্রিয়ার অনুমতি (প্রবেশ করার session চিহ্ন `@acc_backfill = 1`), সব INSERT-only। Lock ক্রম: কোনো ledger lock নেই |
| নিয়ম | R6.27, R6.28, R6.29, R6.30, R6.33। `dimension.max_count` সীমা (`DIM_LIMIT_EXCEEDED`)। `cost_center` মুছা/বন্ধ যায় না। Dimension `code` ছোট হরফ ও `_`, rename অনুমতি (ইতিহাস `_value_history`-এ)। Value tree: গভীরতা ≤ `dimension.max_depth`, `path` materialized (`/1/5/9/`)। এক line-এ এক dimension-এ এক value (`uq`)। Dimension-এর `kind`: `builtin`, `manual`, `mirror`। Backfill শেষে RC1m সবুজ থাকতে হবে (আগে `dimensions.enabled` নয়)। সব write পথে `cost_center_id` ও dimension সারি একই মানে থাকে |
| Contract | `DimensionLineWriter::write(lineId, dims)`, `DimensionService::get(company, code)`, `::list(company)`। API: `GET/POST /dimensions`, `PATCH /dimensions/{id}` (body-তে `version`), `POST /dimensions/backfill` (শুরু/অবস্থা), `GET /dimensions/health` (backfill %, mirror বয়স, RC1m শেষ ফল)। Journal PUT/GET-এ প্রতি line-এ `dimensions: {code: value_code}` |
| Permission | `accounting.dimension.view`, `.manage` |
| Error | `DIM_LIMIT_EXCEEDED` (422), `DIM_CODE_TAKEN` (409), `DIM_BUILTIN_PROTECTED` (409), `DIM_CONFLICT` (409), `DIM_LINE_LIMIT` (422, line-প্রতি ৪টার বেশি), `DIM_COST_CENTER_MISMATCH` (500, অভ্যন্তরীণ) |
| Audit | `accounting.dimension.created`, `.updated`, `.deactivated`, `.backfilled`, `accounting.dimension.setting_changed` |
| Acceptance | AC6.8, G6.5 |
| Test | `DimensionBackfillTest`, `DimensionTreeTest`: G6.5 (৩ line, ৩ সারি, TB by CC5 = 600)। Backfill পুনঃচালু ও নকলহীন, মাঝপথে বন্ধ করে আবার চালালে একই ফল। Tree cycle, গভীরতা। Posted line-এর dimension UPDATE/DELETE trigger-এ আটকায়। `cost_center_id` ও সারি অমিলে rollback। ২০,০০০ line/সেকেন্ড backfill benchmark। ১ লাখ line-এ dimension INSERT গতি |
| Deliverable | Migration (৪ table + backfill state), trigger হালনাগাদ (P1-10), models, services, job, controller, OpenAPI, test |
| Sub-tasks | (১) Migration ও built-in `cost_center` seed। (২) P1-10 trigger। (৩) `DimensionLineWriter` ও mirror rule। (৪) Value tree service। (৫) Backfill job ও cursor। (৬) Health endpoint। (৭) API। (৮) Test ও benchmark |

**`acc_dimensions`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id`, `company_id` | BIGINT UNSIGNED | না |  |
| `code` | VARCHAR(30) | না | CHECK `^[a-z][a-z0-9_]{1,29}$` |
| `name`, `name_bn` | VARCHAR(100) | না / হ্যাঁ |  |
| `kind` | ENUM('builtin','manual','mirror') | না |  |
| `source_type` | VARCHAR(40) | হ্যাঁ | mirror-এ বাধ্যতামূলক |
| `is_hierarchical` | TINYINT(1) | না |  |
| `is_balancing` | TINYINT(1) | না | Default 0, `cost_center`-এ সবসময় 0 (CHECK) |
| `is_active` | TINYINT(1) | না |  |
| `sort_order` | SMALLINT | না |  |
| `mirror_synced_at` | TIMESTAMP | হ্যাঁ |  |
| `version` | INT UNSIGNED | না |  |
| `created_at`, `updated_at` | TIMESTAMP | না |  |

- `uq (company_id, code)`; `uq (company_id, id)`; CHECK `code <> 'cost_center' OR (kind = 'builtin' AND is_active = 1 AND is_balancing = 0)`; CHECK `kind <> 'mirror' OR source_type IS NOT NULL`।

**`acc_dimension_values`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id`, `company_id`, `dimension_id` | BIGINT UNSIGNED | না |  |
| `code` | VARCHAR(40) | না |  |
| `name` | VARCHAR(150) | না |  |
| `parent_id` | BIGINT UNSIGNED | হ্যাঁ | একই dimension-এর |
| `path` | VARCHAR(255) | না | `/id/id/` |
| `depth` | TINYINT UNSIGNED | না |  |
| `effective_from`, `effective_to` | DATE | হ্যাঁ |  |
| `is_active` | TINYINT(1) | না |  |
| `source_type`, `source_id` | VARCHAR(40) / BIGINT UNSIGNED | হ্যাঁ | mirror চাবি |
| `source_hash` | CHAR(40) | হ্যাঁ | sync-এর পরিবর্তন ধরা |
| `version` | INT UNSIGNED | না |  |
| `created_at`, `updated_at` | TIMESTAMP | না |  |

- `uq (company_id, id)`; `uq (company_id, dimension_id, code)`; `uq (company_id, dimension_id, source_type, source_id)`।
- FK `(company_id, dimension_id)` → `acc_dimensions (company_id, id)`; FK `(company_id, parent_id)` → `acc_dimension_values (company_id, id)`।
- CHECK `effective_to IS NULL OR effective_to >= effective_from`। Index `(company_id, dimension_id, parent_id)`, `(company_id, dimension_id, path)`।

**`acc_dimension_value_history`** (append-only): `id`, `company_id`, `value_id`, `field ENUM('code','name','parent','active','effective')`, `old_value`, `new_value VARCHAR(255)`, `changed_by NULL`, `changed_at`, `reason NULL`। FK `(company_id, value_id)`; index `(company_id, value_id, changed_at)`।

**`acc_journal_line_dimensions`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `company_id` | BIGINT UNSIGNED | না |  |
| `journal_line_id` | BIGINT UNSIGNED | না | Master Plan §৬-এর নাম |
| `dimension_id`, `value_id` | BIGINT UNSIGNED | না |  |

- PK `(company_id, journal_line_id, dimension_id)`।
- FK `(company_id, journal_line_id)` → `acc_journal_lines (company_id, id)`; FK `(company_id, dimension_id)`; FK `(company_id, value_id)` → `acc_dimension_values`।
- Index `idx_ald_value (company_id, dimension_id, value_id, journal_line_id)` (filter/group-by), `idx_ald_line (company_id, journal_line_id)`।
- Trigger (P1-10): Posted journal-এর line-এ UPDATE/DELETE আটকায়, INSERT আটকায় (backfill session চিহ্ন ছাড়া)।

### P6-10: Dimension rule, default, validation, balancing

| Field | Specification |
| --- | --- |
| ধরন | `schema + service + api` |
| নির্ভর করে | P6-09, P2-03, P2-12 |
| অবরুদ্ধ করে | P6-06, P6-07, P6-11, P6-12, P6-13 |
| OD | OD-28a (balancing), OD-28d (reversal-এ inactive value) |
| উদ্দেশ্য | কোন account-এ কোন dimension mandatory, optional বা forbidden তা ঠিক করা। Default মান। Draft, Submit ও Post-এ যাচাই। ঐচ্ছিক dimension-level balancing |
| Schema | নিচের দুটো table। `acc_accounts`-এ expand: `default_dimension_rule_id BIGINT UNSIGNED NULL`, composite FK `(company_id, default_dimension_rule_id)` → `acc_dimension_rule_sets (company_id, id)` |
| DB flow | **Draft ও Submit (`validateLines`, সব line একসাথে):** rule set সমেত তথ্য একবারে লোড (২টা query: account থেকে group থেকে type চেইন, rules)। সব হিসাব in-memory, কোনো lock নেই। Value তথ্য এক query-তে (`WHERE id IN`)। **Post (`lockAndRecheck`, lock ক্রমের একদম শেষে):** journal-এর সব value id সংগ্রহ, id ক্রমে `SELECT ... FROM acc_dimension_values WHERE company_id = ? AND id IN (...) ORDER BY id FOR SHARE`। তারপর active, effective date, নিয়ম ও balancing আবার। ব্যর্থ হলে পুরো Post rollback (counter সহ)। **Rule set save:** এক transaction, `acc_dimension_rule_sets` `FOR UPDATE`, `version` মেলানো, rules পুরো বদল (DELETE + INSERT), conflict যাচাই, `version + 1`। কোনো ledger lock নেই |
| নিয়ম | R6.31, R6.32, R6.34, R6.37 অংশ, OD-28d। **Resolver:** প্রতি (account, dimension) জোড়ায় নিয়ম বাছাই: account-এর নিজস্ব set, না থাকলে সবচেয়ে কাছের account group, না থাকলে account type-এর set, না থাকলে `dimension.default_requirement`। প্রথম যেখানে dimension-টা আছে সে জেতে। Journal type-এর set শুধু ফাঁকা জায়গায় `mandatory` যোগ করে, `forbidden` হারায় না। **Default:** `applyDefaults` Draft লেখার সময় ফাঁকা dimension-এ বসায়, দেওয়া মান কখনো overwrite নয়। Default অকার্যকর (inactive) হলে ফাঁকা থাকে, mandatory হলে `DIM_REQUIRED`। **Balancing:** `acc_dimensions.is_balancing = 1` হলে প্রতি journal-এ প্রতি value-র Debit = Credit; valueহীন line আলাদা এক বাঁধা। চালু/বন্ধ দ্বিতীয় অনুমোদন সহ। শুধু নতুন journal। **Reversal (OD-28d):** `reversal` type-এ মূলের value inactive হলেও চলে, শুধু মূল journal-এর হুবহু value। **Conflict:** একই rule set-এ একই dimension দুবার নয়। একই account-এ দুই set নয় (account-এ একটা `default_dimension_rule_id`)। Forbidden ও default একসাথে নয় (`DIM_RULE_CONFLICT`)। `dimension.allow_balancing` বন্ধ থাকলে `is_balancing` চালু হয় না |
| Contract | `DimensionService::validateLines(company, lines): AccountingError[]` (সব ত্রুটি একসাথে, field `lines[n].dimensions.<code>`), `::applyDefaults(company, lines)`, `::lockAndRecheck(company, journal)`। `DimensionRuleResolver::resolve(company, accountId): map<dimensionId, rule>` (cache-সহ, key `acc:dr:{company}:{rule_version}`)। API: `GET/POST /dimension-rule-sets`, `GET/PATCH /dimension-rule-sets/{id}`, `PUT /dimension-rule-sets/{id}/rules`, `PUT /accounts/{id}/dimension-rule-set`, `GET /dimension-rule-sets/dry-run?set=` (কতগুলো খোলা Draft ব্যর্থ হবে) |
| Permission | `accounting.dimension.view`, `.manage`, `.approve`। `mandatory` বা `forbidden` যোগ এবং balancing বদলে `.approve` লাগে, Maker ≠ Checker |
| Error | `DIM_REQUIRED`, `DIM_FORBIDDEN`, `DIM_BALANCING_VIOLATION` (details: value ধরে পার্থক্য), `DIM_RULE_CONFLICT`, `DIM_VALUE_NOT_FOUND`, `DIM_VALUE_WRONG_DIMENSION`, `DIM_VALUE_INACTIVE`, `DIM_VALUE_NOT_EFFECTIVE`, `DIM_CONFLICT` (409, version)। সব 422 |
| Audit | `accounting.dimension_rule.changed`, `accounting.dimension.balancing_changed`, `accounting.dimension.setting_changed` |
| Acceptance | AC6.8, AC6.9, G6.3, G6.4 |
| Test | `DimensionRuleResolverTest`: G6.3। `DimensionBalancingTest`: G6.4। `DimensionConcurrencyTest`: Post চলার সময় deactivate অপেক্ষা করে, 1,000 অসম্পর্কিত লেখার stress-এ deadlock 0। `validateLines` 500 line × 4 dimension 150ms-এর মধ্যে। Reversal-এ inactive মূল value চলে (OD-28d)। Default overwrite করে না। Rule save-এর conflict। Flag বন্ধে কোনো নিয়ম খাটে না |
| Deliverable | Migration, resolver, service, controller, OpenAPI, P2-12 integration (ধাপ 11a), test |
| Sub-tasks | (১) Migration। (২) Resolver ও cache। (৩) `validateLines`, `applyDefaults`। (৪) `lockAndRecheck` ও P2-12 hook। (৫) Balancing। (৬) Rule API ও dry-run। (৭) Concurrency ও performance test |

**`acc_dimension_rule_sets`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id`, `company_id` | BIGINT UNSIGNED | না |  |
| `code` | VARCHAR(40) | না |  |
| `name` | VARCHAR(120) | না |  |
| `scope` | ENUM('account','account\_group','account\_type','journal\_type') | না |  |
| `scope_ref` | VARCHAR(40) | হ্যাঁ | group id, account type বা journal type code (scope অনুযায়ী) |
| `is_active` | TINYINT(1) | না |  |
| `version` | INT UNSIGNED | না |  |
| `created_by`, `created_at`, `updated_at` |  |  |  |

- `uq (company_id, code)`; `uq (company_id, id)`; `uq (company_id, scope, scope_ref)` যেখানে scope ≠ `account` (এক group-এ এক set)।

**`acc_dimension_rules`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id`, `company_id`, `rule_set_id`, `dimension_id` | BIGINT UNSIGNED | না |  |
| `requirement` | ENUM('mandatory','optional','forbidden') | না |  |
| `default_value_id` | BIGINT UNSIGNED | হ্যাঁ | `forbidden`-এ NULL (CHECK) |

- FK `(company_id, rule_set_id)`, `(company_id, dimension_id)`, `(company_id, default_value_id)` সব composite।
- `uq (company_id, rule_set_id, dimension_id)`; index `(company_id, dimension_id)`।

### P6-11: Dimension value lifecycle ও mirror

| Field | Specification |
| --- | --- |
| ধরন | `service + job + api` |
| নির্ভর করে | P6-09, P1-12, P5 alert |
| অবরুদ্ধ করে | P6-14, P6-15 |
| OD | OD-28b (hierarchy ইতিহাস), OD-28c (mirror আচরণ) |
| উদ্দেশ্য | Value তৈরি, rename, সরানো, বন্ধ, মুছা। ERPFlow master data (cost center, project, department, location) থেকে mirror। Sync idempotent |
| Schema | নতুন table নেই (P6-09-এর table)। `acc_dimensions.mirror_synced_at` লেখা হয় |
| DB flow | **Value বদল (rename, deactivate):** এক transaction, value সারি `FOR UPDATE`, `version` মেলানো, `_value_history` INSERT। Post-এর `FOR SHARE` থাকায় চলমান Post commit পর্যন্ত অপেক্ষা করে। **সরানো (move):** এক transaction: dimension সারি `FOR UPDATE`, তারপর value সারি id ক্রমে `FOR UPDATE` (যে value ও তার সব সন্তান), cycle ও গভীরতা পরীক্ষা, সব সন্তানের `path`, `depth` একটা `UPDATE ... WHERE path LIKE 'old%'`। **মুছা:** `NOT EXISTS (SELECT 1 FROM acc_journal_line_dimensions WHERE value_id = ?)` আর কোনো rule-এর `default_value_id` নয়, আর mirror নয়, নইলে `DIM_VALUE_IN_USE`। **Mirror sync (`DimensionMirrorSyncJob`, queue `accounting-mirror`, unique (company, source\_type)):** `MasterDataReader` (শুধু পড়া) থেকে পৃষ্ঠা ধরে, প্রতি পৃষ্ঠায় এক transaction, parent আগে। প্রতি সারি `source_hash` তুলনা, বদলালে update + history, নতুন হলে insert, source-এ না পেলে inactive। কোনো ledger lock নেই |
| নিয়ম | R6.28, R6.29, R6.30, R6.36, OD-28b, OD-28c। **Roll-up** বর্তমান গাছ ধরে; সরানোর আগে UI সতর্কতা দেখায় (কোন বন্ধ period-এর report বদলাবে)। বন্ধ period-এর জন্য period বন্ধের সময় tree snapshot export। Mirror value-তে code, name, parent, active শুধু sync থেকে (`DIM_MIRROR_READONLY`); Accounting-এ alias নেই (OD-28c (ক))। `effective_from`/`to` বদল শুধু manual value-তে। Inactive করলে পুরোনো posted line বদলায় না। Cycle বা গভীরতা ভঙ্গ করলে ওই সারি ডিঙিয়ে alert, বাকিগুলো চলে। Sync 3 বিরতি পার হলে `dimension_mirror_stale` alert (`acc_alert_state`)। Source পাওয়া না গেলে শেষ সফল মান থাকে, backoff। `dimension.mirror_sync_interval_min` default 15 |
| Contract | `DimensionMirrorService::sync(company, sourceType)`, `DimensionValueService::create/rename/move/deactivate/delete`। API: `GET/POST /dimensions/{id}/values` (cursor, `q=`, `parent=`, `active=`), `PATCH /dimension-values/{id}`, `POST /dimension-values/{id}/move`, `POST /dimension-values/{id}/deactivate`, `DELETE /dimension-values/{id}`, `POST /dimensions/mirror-sync` |
| Permission | `accounting.dimension.view`, `.manage`। Mirror sync হাতে চালাতে `.manage` |
| Error | `DIM_VALUE_IN_USE` (409), `DIM_MIRROR_READONLY` (409), `DIM_CYCLE_DETECTED` (422), `DIM_DEPTH_EXCEEDED` (422), `DIM_CODE_TAKEN` (409), `DIM_CONFLICT` (409) |
| Audit | `accounting.dimension_value.created`, `.renamed`, `.moved`, `.deactivated`, `.mirrored`, `.deleted` |
| Acceptance | AC6.11 |
| Test | `DimensionMirrorTest`: নতুন, rename, মুছে যাওয়া source ধরা। পুনরায় চালালে পরিবর্তন শূন্য। Mirror value সম্পাদনা `DIM_MIRROR_READONLY`। ব্যবহৃত value মুছা `DIM_VALUE_IN_USE`। Move: cycle, গভীরতা, সব সন্তানের path। Move ও নতুন child একসাথে `DIM_CONFLICT` বা অপেক্ষা। Source বন্ধে শেষ মান ও alert। 3 বিরতিতে `dimension_mirror_stale`। Department ও location source তালিকার ব্যাখ্যা OD-28c-তে |
| Deliverable | Services, job, controller, `MasterDataReader` interface, alert wiring, OpenAPI, test |
| Sub-tasks | (১) Value service ও history। (২) Move ও path। (৩) Delete নিয়ম। (৪) `MasterDataReader` adapter। (৫) Sync job ও diff। (৬) Alert। (৭) API। (৮) Test |

### P6-12: Dimension reclassification

| Field | Specification |
| --- | --- |
| ধরন | `schema + service + api` |
| নির্ভর করে | P6-10, P3-01, P2-14, P2-03 |
| অবরুদ্ধ করে | P6-14, P6-15 |
| OD | OD-29b (reclassification-এর ধরন) |
| উদ্দেশ্য | Posted ledger না বদলে এক value থেকে আরেক value-তে নিট balance সরানো। ফল একটা `adjustment` Draft, সাধারণ approval ও Post-এর পথে |
| Schema | নিচের `acc_dimension_reclass_requests` |
| DB flow | **Preview:** শুধু পড়া। `LedgerQuery::balancesAsOf(company, asOf, dimension filter)` দিয়ে প্রতি account-এ from value-র নিট, কোনো lock নেই। Snapshot hash = SHA-256(account id, নিট, `balance_version` তালিকা)। `INSERT` request সারি (status `previewed`)। **Create-draft:** এক transaction: request `FOR UPDATE`, status `previewed` যাচাই, `JournalService::createDraft` (type `adjustment`, `source_type = dimension_reclass`, `source_id = request id`, প্রতি account-এ দুটো line: to value-তে নিট, from value-তে উল্টো), request-এ `journal_id`, status `drafted`। `source_active` unique এক request-এ এক Draft। **Submit-এ:** `SubmitValidator` ledger আবার হিসাব করে hash তুলনা, অমিল হলে `DIM_RECLASS_STALE` ও request `superseded`। Lock নেই (শুধু পড়া)। Post সাধারণ পথে, lock ক্রম অপরিবর্তিত |
| নিয়ম | R6.35, OD-29b (ক)। As-of তারিখ open period-এ; বন্ধ হলে `JRN_PERIOD_LOCKED`। From ও to একই dimension, from ≠ to। From-এ নিট শূন্য হলে `DIM_RECLASS_EMPTY`। একই account, নিট শূন্য journal। Draft-এ প্রতি line-এর আর সব dimension মূলের সাথে বহাল (শুধু এই dimension বদল)। ঐচ্ছিক account তালিকা (বাকিগুলো বাদ)। Reclass করা ব্যক্তি সেই journal Post করতে পারে না (SoD)। পুরোনো posted line কখনো বদলায় না |
| Contract | `ReclassificationService::preview(company, dimensionId, fromValueId, toValueId, asOf, accountIds?)`, `::createDraft(requestId)`। API: `POST /dimension-reclassifications` (preview), `GET /dimension-reclassifications/{id}`, `POST /dimension-reclassifications/{id}/create-draft` |
| Permission | `accounting.dimension.reclassify` |
| Error | `DIM_RECLASS_EMPTY` (422), `DIM_RECLASS_STALE` (409), `DIM_VALUE_WRONG_DIMENSION` (422), `JRN_PERIOD_LOCKED` (বিদ্যমান), `DIM_CONFLICT` |
| Audit | `accounting.dimension.reclass_previewed`, `accounting.dimension.reclass_drafted` |
| Acceptance | AC6.10, G6.6 |
| Test | `DimensionReclassTest`: G6.6 (Sales 5,000 → 3,000, Marketing 2,000)। Submit-এর আগে নতুন Post এলে `DIM_RECLASS_STALE`। একই request দুবার create-draft একই Draft। বন্ধ period। Preview কিছু লেখে না (ledger row count অপরিবর্তিত)। SoD |
| Deliverable | Migration, service, controller, OpenAPI, validator hook, test |
| Sub-tasks | (১) Migration। (২) Balance হিসাব ও snapshot hash। (৩) Draft তৈরি। (৪) Submit-এ stale যাচাই। (৫) API। (৬) Test |

**`acc_dimension_reclass_requests`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id`, `company_id`, `dimension_id` | BIGINT UNSIGNED | না |  |
| `from_value_id`, `to_value_id` | BIGINT UNSIGNED | না |  |
| `as_of_date` | DATE | না |  |
| `account_filter` | JSON | হ্যাঁ | account id তালিকা |
| `snapshot_hash` | CHAR(64) | না |  |
| `preview` | JSON | না | প্রতি account-এর নিট |
| `status` | ENUM('previewed','drafted','superseded','cancelled') | না |  |
| `journal_id` | BIGINT UNSIGNED | হ্যাঁ |  |
| `source_active` | TINYINT(1) | হ্যাঁ | `1 যখন Draft চলমান, নইলে NULL। একটা request-এ একটা সক্রিয় Draft (unique)` |
| `created_by`, `created_at`, `updated_at` |  |  |  |

- FK `(company_id, dimension_id)`, `(company_id, from_value_id)`, `(company_id, to_value_id)`, `(company_id, journal_id)` সব composite।
- CHECK `from_value_id <> to_value_id`। `uq (company_id, id)`; index `(company_id, status, created_at)`।

### P6-13: Report filter, group-by, cache, index, security hook

| Field | Specification |
| --- | --- |
| ধরন | `schema + service` |
| নির্ভর করে | P6-09, P6-10, P3-01 থেকে P3-06, P3-13 |
| অবরুদ্ধ করে | P6-14, P6-15 |
| OD | OD-29c (projection table), OD-29a (data security বিধি, Phase 13-এ) |
| উদ্দেশ্য | TB, GL, P&L, BS-এ dimension filter ও group-by। Derived cache `acc_dimension_balances`। `DimensionAccessGuard` hook |
| Schema | নিচের `acc_dimension_balances`। বিদ্যমান `LedgerQuery`-র query বদল, নতুন table নয় (একটা ছাড়া) |
| DB flow | **Report query (পড়া, lock নেই):** `acc_journal_lines` posted, `JOIN acc_journal_line_dimensions` প্রতি filter dimension-এ একবার (`idx_ald_value`), `WHERE company_id = ?` সবার আগে। Group-by সর্বোচ্চ ২ dimension। Unassigned = `LEFT JOIN ... IS NULL`। **Cache:** `JournalPosted` listener (afterCommit) `balance_version` ধরে `INSERT ... ON DUPLICATE KEY UPDATE debit = debit + VALUES(debit), credit = credit + VALUES(credit)` প্রতি (period, account, dimension, value)। ব্যর্থ হলে retry, Post প্রভাবিত নয়, `balance_version` মেলে না বলে রিপোর্ট সরাসরি ledger থেকে পড়ে। `DimensionBalanceRebuildJob`: period ধরে মুছে নতুন করে হিসাব, এক period এক transaction |
| নিয়ম | R6.37, R6.33। Parameter: `dim[<code>]=v1,v2` (একই dimension OR, বহু dimension AND), `dim_descendants=1` (বর্তমান গাছ, OD-28b), `dim_unassigned=1` (আলাদা সারি), `group_by=dim:<code>` (সর্বোচ্চ ২)। অজানা code বা value হলে 422। যোগফল: group-by সারি ও unassigned মিলে unfiltered মোট। সত্যের উৎস সবসময় posted line, cache শুধু গতি। Cache ও ledger অমিল হলে (RC1m (গ)) ledger জেতে ও rebuild। \`DimensionAccessGuard::allowedValues(user, dimension): ValueSet |
| Contract | `LedgerQuery` এ `->withDimensions(filters)->groupByDimension(codes)`। বিদ্যমান endpoint: P3-03 থেকে P3-06-এর TB, GL, P&L, BS-এ ঐচ্ছিক parameter (উপরের তালিকা)। উত্তরে সারি: `{dimension_values: {code: {id, code, name}}, debit, credit, balance}`, সবসময় `unassigned` সারি আলাদা |
| Permission | বিদ্যমান report permission (P3), নতুন কিছু নয়। Dimension দেখার জন্য `accounting.dimension.view` |
| Error | `DIM_VALUE_NOT_FOUND` (422, field `dim[code]`), `DIM_GROUPBY_LIMIT` (422, দুইয়ের বেশি), `DIM_REPORT_UNAVAILABLE` (503, flag বন্ধ বা backfill অসম্পূর্ণ) |
| Audit | রিপোর্ট চালানো audit নয় (পড়া)। Rebuild: `accounting.dimension.backfilled`-এর মতো `accounting.dimension.cache_rebuilt` |
| Acceptance | AC6.12, G6.10 |
| Test | `DimensionReportTest`: G6.10 (1,250 মোট, A + descendants = 600, unassigned 250)। চার report জুড়ে যোগফল। `EXPLAIN` প্রতি filter query-তে `idx_ald_value` ব্যবহার। `DimensionPerformanceTest`: 10M line × 4 dimension, TB by dimension (5M line/বছর) p95 3 সেকেন্ড, GL প্রথম পৃষ্ঠা 1 সেকেন্ড। Cache `balance_version` পুরোনো হলে ledger থেকে। `ReconciliationDimensionTest`: RC1m (গ) fail করানো। লক্ষ্য না মিললে OD-29c (খ)-এর সিদ্ধান্ত নথিভুক্ত। `DimensionSecurityHookTest`: guard দিয়ে value সীমিত করলে সারি লুকায়, মোট আলাদা চিহ্নিত |
| Deliverable | Migration, query extension, listener, rebuild job, guard interface, OpenAPI, benchmark report, test |
| Sub-tasks | (১) `acc_dimension_balances` migration। (২) `LedgerQuery` filter ও group-by। (৩) Endpoint parameter ও validation। (৪) Listener ও rebuild job। (৫) `DimensionAccessGuard`। (৬) Index যাচাই ও benchmark। (৭) Test |

**`acc_dimension_balances`** (derived cache, ledger নয়)

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `company_id`, `period_id`, `account_id`, `dimension_id` | BIGINT UNSIGNED | না |  |
| `value_id` | BIGINT UNSIGNED | না | 0 = unassigned |
| `debit`, `credit` | DECIMAL(20,4) | না | Default 0 |
| `balance_version` | BIGINT UNSIGNED | না | P3-02-এর মত |
| `updated_at` | TIMESTAMP | না |  |

- PK `(company_id, period_id, account_id, dimension_id, value_id)`।
- FK `(company_id, period_id)`, `(company_id, account_id)`, `(company_id, dimension_id)` composite। `value_id` 0 বলে FK নেই, এটা জেনেশুনে (RC1m (গ) ধরে)।
- Index `(company_id, dimension_id, value_id, period_id)`।

### P6-14: Admin ও journal form UI

| Field | Specification |
| --- | --- |
| ধরন | `ui` |
| নির্ভর করে | P6-02 থেকে P6-13-এর API |
| অবরুদ্ধ করে | P6-15 |
| OD | নেই (সব OD-র baseline ধরে) |
| উদ্দেশ্য | Type, series, template, import wizard, dimension admin, rule editor, line picker, comment, tag, saved filter পর্দা |
| Schema | নেই |
| DB flow | নেই (শুধু API ডাকে) |
| নিয়ম | প্রতি পর্দা ফিচার flag বন্ধ থাকলে লুকানো (route guard ও menu)। Permission ছাড়া বোতাম নেই, কিন্তু সার্ভারের বাধাই আসল। সব ত্রুটি API-র `field` ধরে ফর্মের জায়গায় দেখানো। বাংলা ও ইংরেজি label। অ্যাক্সেসযোগ্য (কিবোর্ড, ARIA)। Posted journal-এ comment, tag, সংযুক্তি যোগের বোতাম থাকে, সম্পাদনা নেই। বড় তালিকা cursor pagination |
| Contract | **পর্দা:** (১) Journal type তালিকা ও সম্পাদনা, account range editor। (২) Series তালিকা, format preview, assignment, allocation দেখা, cutover dry-run ফলাফল। (৩) Template তালিকা, সম্পাদক (expression ও parameter), সম্পাদনার অনুরোধের অবস্থা, Draft তৈরির পর্দা। (৪) Import wizard: upload, ফাইল ত্রুটি, group তালিকা, সারির ত্রুটি, accept মোড, ফলাফল। (৫) Dimension admin: dimension, value গাছ, mirror অবস্থা, backfill অগ্রগতি, health। (৬) Rule set editor ও dry-run। (৭) Journal form: document date, প্রতি line-এ dimension picker (mandatory চিহ্ন, default আগে থেকে বসা, inactive ধূসর), সংযুক্তি scan অবস্থা, comment, tag, copy ও clone। (৮) Saved filter ও search। (৯) Reclassification preview ও Draft। (১০) Report পর্দায় dimension filter ও group-by |
| Permission | UI শুধু API-র permission অনুযায়ী দেখায় |
| Error | API ত্রুটি ম্যাপ: `DIM_*`, `JTY_*`, `NSR_*` প্রতিটার বাংলা বার্তা (error catalog-এ) |
| Audit | নেই (UI নয়) |
| Acceptance | AC6.2 থেকে AC6.13-এর UI অংশ |
| Test | Component test প্রতি পর্দায়। E2E: template থেকে Draft, import পূর্ণ পথ, dimension সহ journal Post, reclass, filter সহ TB। Permission ছাড়া বোতাম নেই। Flag বন্ধে menu নেই। ত্রুটি field-এ বসে। 500 line-এর form ধীর হয় না (virtualized) |
| Deliverable | Screens, component, i18n কী, E2E test, screenshot |
| Sub-tasks | (১) Type ও series পর্দা। (২) Template। (৩) Import wizard। (৪) Dimension admin ও rule editor। (৫) Journal form সম্প্রসারণ। (৬) Search ও filter। (৭) Reclass। (৮) Report filter। (৯) i18n ও error map। (১০) E2E |

### P6-15: Enablement, RC sub-check, runbook, UAT

| Field | Specification |
| --- | --- |
| ধরন | `release` |
| নির্ভর করে | P6-01 থেকে P6-14 সব |
| অবরুদ্ধ করে | Phase 7 শুরু (Entry gate) |
| OD | সব OD-24a থেকে OD-29c-র baseline লিখিত সম্মতি |
| উদ্দেশ্য | দুটো flag নিরাপদে চালু করা। তিনটা RC sub-check। Runbook, UAT, benchmark, gate রিপোর্ট। Phase 1-5 card-এ বদলের merge প্রমাণ |
| Schema | নতুন table নেই। `acc_enablement_log`-এ (P1-05) দুটো নতুন সারি-ধরন |
| DB flow | **Flag চালু (`journal_mgmt.enabled`):** Maker-Checker change request, `ChangeApplier` এক transaction-এ precondition আবার যাচাই করে `Setting` লেখে ও `acc_enablement_log` INSERT। কোনো ledger lock নেই। **`dimensions.enabled`:** একই। **RC sub-check:** P3-13 framework-এ নিবন্ধন, প্রতিটা শুধু পড়া query, snapshot isolation। `import` type সক্রিয়: `JournalTypeRegistry::activate('import', by: phase 6)` |
| নিয়ম | **`journal_mgmt.enabled` চালুর শর্ত (সব সবুজ):** (১) S11 (সব company-তে 16 type seed, orphan 0)। (২) S12 (cutover dry-run অমিল 0, সম্পন্ন cutover)। (৩) RC1n সবুজ। (৪) প্রতি active journal type-এ কমপক্ষে একটা series assignment বা default series। (৫) Production-copy-তে cutover রিহার্সাল সফল। (৬) P1-10 trigger হালনাগাদ প্রমাণ। (৭) P2-03, P2-11, P2-12 বদল merge। **`dimensions.enabled` চালুর শর্ত:** (১) Backfill সম্পূর্ণ, RC1m সবুজ। (২) Mirror প্রথম sync সফল। (৩) Rule conflict শূন্য, `dimension.default_requirement` dry-run-এ ব্যর্থ Draft তালিকা পর্যালোচিত। (৪) RC1d সবুজ (balancing চালু থাকলে)। (৫) P6-13 benchmark সংখ্যা লিখিত। কোনো শর্ত না মিললে `SET_INCOMPLETE`, তালিকাসহ। দুই flag একে অপরের ওপর নির্ভর করে না। বন্ধ করা: `journal_mgmt` বন্ধে rollback command পূর্বশর্ত। `dimensions` বন্ধে নিয়ম যাচাই থামে, তথ্য থাকে। **নতুন RC sub-check (নতুন RC নম্বর নয়):** `RC1d` (balancing dimension-এ প্রতি value-র Debit = Credit), `RC1m` (ক: `cost_center_id` = dimension সারি; খ: value + unassigned যোগফল = ledger মোট; গ: cache = ledger), `RC1n` (counter ক্রমিক, গর্ত ও নকল নেই, legacy ≤ base)। তিনটাই P3-13 ফলাফলে ও scheduled run-এ, fail হলে `acc_alert_state` |
| Contract | S11, S12 checklist (P1-05)। RC `RC1d`, `RC1m`, `RC1n` (id, নাম, query, fail বার্তা)। Runbook অধ্যায়: cutover, rollback, mirror stale, scan backlog, dimension backfill, নম্বরে গর্ত সন্দেহ। Gate রিপোর্ট টেমপ্লেট |
| Permission | Flag বদল: settings manage (P1-04), Maker-Checker। RC চালানো: বিদ্যমান reconciliation permission |
| Error | `SET_INCOMPLETE` (বিদ্যমান, তালিকা details-এ) |
| Audit | `accounting.journal_mgmt.flag_changed`, `accounting.dimension.flag_changed` |
| Acceptance | AC6.14 এবং সব AC6.1 থেকে AC6.13 পাস |
| Test | `FeatureFlagTest`: দুই flag বন্ধে Phase 5-এর পুরো suite পাস। শর্ত না মিললে চালু হয় না। `VoucherGaplessReconTest`, `ReconciliationDimensionTest`: RC1d, RC1m, RC1n pass ও ইচ্ছাকৃত fail। Static test: `acc_journals.status` শুধু `PostingService` লেখে। Static test: কোনো class `JournalService` ছাড়া `acc_journals` INSERT করে না। UAT script: সব workflow WF-A থেকে WF-G। Security suite। Benchmark রিপোর্ট। Cutover রিহার্সাল |
| Deliverable | Enablement ব্যবস্থা, RC sub-check, runbook, UAT script ও ফলাফল, benchmark রিপোর্ট, Gate রিপোর্ট, Phase 1-5 merge প্রমাণ |
| Sub-tasks | (১) S11, S12। (২) Flag precondition। (৩) RC1d, RC1m, RC1n। (৪) Static test। (৫) Runbook। (৬) UAT ও rehearsal। (৭) Benchmark। (৮) Gate রিপোর্ট |

### Registry যোগ

**নতুন table (31টা):**

| গোষ্ঠী | Table | Card |
| --- | --- | --- |
| Type (2) | `acc_journal_types`, `acc_journal_type_account_ranges` | P6-01 |
| Series (4) | `acc_number_series`, `acc_number_series_assignments`, `acc_number_series_counters`, `acc_voucher_allocations` (append-only) | P6-03 |
| Template (6) | `acc_journal_templates`, `acc_journal_template_versions`, `acc_journal_template_lines`, `acc_journal_template_line_dimensions`, `acc_journal_template_shares`, `acc_journal_template_uses` | P6-06 |
| Import (3) | `acc_journal_import_batches`, `acc_journal_import_groups`, `acc_journal_import_rows` | P6-07 |
| সংযুক্তি ও নোট (7) | `acc_attachment_policies`, `acc_attachment_scans`, `acc_journal_comments`, `acc_journal_tags`, `acc_journal_tag_links`, `acc_saved_filters`, `acc_journal_copy_links` | P6-08 |
| Dimension (9) | `acc_dimensions`, `acc_dimension_values`, `acc_dimension_value_history` (append-only), `acc_journal_line_dimensions`, `acc_dimension_backfill_state` | P6-09 |
|  | `acc_dimension_rule_sets`, `acc_dimension_rules` | P6-10 |
|  | `acc_dimension_reclass_requests` | P6-12 |
|  | `acc_dimension_balances` (derived cache) | P6-13 |

**বিদ্যমান table-এ expand (শুধু যোগ):** `acc_journals` (`document_date`, `series_id`, composite FK `fk_acc_journals_type`, `fk_acc_journals_series`, FULLTEXT narration, `ck_acc_journals_type` সরানো), `acc_accounts` (`default_dimension_rule_id` ও composite FK)।

**নতুন permission (৮ সারি, ২০টা কী):** `accounting.journal_type.view/.manage/.approve`; `accounting.number_series.view/.manage/.approve`; `accounting.journal_template.view/.manage/.use/.approve`; `accounting.journal_import.view/.manage/.commit`; `accounting.attachment_policy.manage`; `accounting.journal_comment.create`; `accounting.journal_tag.manage`; `accounting.dimension.view/.manage/.approve/.reclassify`। (মোট ২০টা কী।)

**নতুন error code:**

- `JTY_`: TYPE\_INACTIVE, TYPE\_NOT\_FOUND, MANUAL\_NOT\_ALLOWED, ACCOUNT\_RANGE\_VIOLATION, BACKDATE\_LIMIT, AUTO\_REVERSE\_NOT\_ALLOWED, APPROVAL\_RULE\_MISSING, CUSTOM\_DISABLED, MIGRATION\_ORPHAN, TEMPLATE\_PARAM\_INVALID, TEMPLATE\_EXPR\_INVALID, TEMPLATE\_NOT\_FOUND, TEMPLATE\_PENDING\_APPROVAL, CONFLICT, IMPORT\_FILE\_INVALID, IMPORT\_DUPLICATE\_FILE, IMPORT\_TOO\_MANY\_ROWS, IMPORT\_ROW\_INVALID, IMPORT\_NOT\_VALIDATED, IMPORT\_MODE\_VIOLATION, ATTACH\_INFECTED, ATTACH\_NOT\_SCANNED, ATTACH\_TYPE\_NOT\_ALLOWED, ATTACH\_TOO\_LARGE, ATTACH\_RETAINED, COMMENT\_EDIT\_EXPIRED, FILTER\_INVALID, COPY\_SOURCE\_INVALID।
- `NSR_`: NOT\_IN\_POST\_TX, SERIES\_LOCKED, SERIES\_NO\_ASSIGNMENT, SERIES\_INACTIVE, SERIES\_EXHAUSTED, PREFIX\_CONFLICT, CUTOVER\_PRECONDITION, CONFLICT।
- `DIM_`: REQUIRED, FORBIDDEN, BALANCING\_VIOLATION, VALUE\_NOT\_FOUND, VALUE\_WRONG\_DIMENSION, VALUE\_INACTIVE, VALUE\_NOT\_EFFECTIVE, VALUE\_IN\_USE, MIRROR\_READONLY, RULE\_CONFLICT, RECLASS\_STALE, RECLASS\_EMPTY, CYCLE\_DETECTED, DEPTH\_EXCEEDED, LIMIT\_EXCEEDED, CODE\_TAKEN, BUILTIN\_PROTECTED, LINE\_LIMIT, GROUPBY\_LIMIT, REPORT\_UNAVAILABLE, COST\_CENTER\_MISMATCH, CONFLICT।
- `JRN_` (বিদ্যমান prefix-এ যোগ): DOCUMENT\_DATE\_REQUIRED, DOCUMENT\_DATE\_AFTER\_POSTING।

**নতুন setting key:** Flag `flag:journal_mgmt.enabled`, `flag:dimensions.enabled` (`Setting` model)। `acc_settings`: `journal_types.allow_custom`, `type.approval_rule_type`, `type.attachment_rule`, `type.attachment_min_count`, `type.backdate_days_limit`, `type.future_days_limit`, `type.account_ranges`, `numbering.default_gapless`, `numbering.default_reset_rule`, `series.<code>.format/.gapless/.reset_rule`, `journal.document_date_policy`, `template.edit_requires_approval`, `journal_import.max_rows`, `journal_import.accept_mode`, `attachment.scan_required`, `attachment.max_bytes`, `attachment.allowed_ext`, `attachment.retention_years`, `journal.comment_edit_minutes`, `dimension.max_count`, `dimension.max_depth`, `dimension.allow_balancing`, `dimension.<code>.is_balancing`, `dimension.default_requirement`, `dimension.mirror_sync_interval_min`, `dimension_rule.<set>`। `ChangeApplier` prefix: `jtype:`, `jtemplate:`, `series:`।

**নতুন audit event:** `accounting.journal_type.created/.updated/.activated/.deactivated/.ranges_changed/.setting_changed`; `accounting.number_series.created/.updated/.assigned/.cutover/.cutover_dry_run/.rollback/.setting_changed`; `accounting.journal_template.created/.edit_requested/.edit_approved/.retired/.used/.setting_changed`; `accounting.journal_import.uploaded/.validated/.accepted/.committed/.cancelled/.setting_changed`; `accounting.attachment_policy.changed`; `accounting.journal_attachment.scanned/.quarantined/.downloaded`; `accounting.journal_comment.created/.edited`; `accounting.journal_tag.changed`; `accounting.journal.copied/.cloned/.document_date_policy_changed`; `accounting.dimension.created/.updated/.deactivated/.balancing_changed/.backfilled/.cache_rebuilt/.setting_changed/.flag_changed/.reclass_previewed/.reclass_drafted`; `accounting.dimension_value.created/.renamed/.moved/.deactivated/.mirrored/.deleted`; `accounting.dimension_rule.changed`; `accounting.journal_mgmt.flag_changed`।

**নতুন RC sub-check:** `RC1d`, `RC1m`, `RC1n` (P3-13 framework গ্রহণ করবে)। **Queue:** `accounting-import`, `accounting-scan`, `accounting-mirror`। **Job:** `JournalImportParseJob`, `JournalImportCommitJob`, `AttachmentScanJob`, `DimensionMirrorSyncJob`, `DimensionBackfillJob`, `DimensionBalanceRebuildJob`। **Command:** `accounting:journal-types:seed`, `accounting:numbering-cutover`, `accounting:numbering-rollback`।

### Build ক্রমের সারি

| ক্রম | Card | নির্ভর করে | বাকি OD |
| --- | --- | --- | --- |
| 1 | P6-01 | P2-03, P1-10 | OD-24b |
| 2 | P6-02 | P6-01 | OD-24a |
| 3 | P6-03 | P6-01, P2-11 | OD-25a, OD-25c |
| 4 | P6-04 | P6-03, P2-12 | OD-25b |
| 5 | P6-05 | P6-02 | OD-26b, OD-26c |
| 6 | P6-09 | P1-10, P3-01 | OD-28b, OD-28c |
| 7 | P6-10 | P6-09, P2-03, P2-12 | OD-28a, OD-28d |
| 8 | P6-11 | P6-09 | OD-28b, OD-28c |
| 9 | P6-06 | P6-02, P6-09 | OD-26a |
| 10 | P6-07 | P6-02, P6-10 | OD-27a |
| 11 | P6-08 | P6-02 | OD-27b |
| 12 | P6-12 | P6-10, P3-01, P2-14 | OD-29b |
| 13 | P6-13 | P6-09, P6-10, P3-01 থেকে P3-06, P3-13 | OD-29c, OD-29a |
| 14 | P6-14 | সবার API | নেই |
| 15 | P6-15 | P6-01 থেকে P6-14 | সব baseline-এর সম্মতি |

## ১২. Phase 7 task cards: Multi-Currency ও FX

Phase 7-এর সব card (P7-01 থেকে P7-16), "Registry যোগ" ও "Build ক্রমের সারি" tab-এর সীমার কারণে আলাদা tab-এ: Phase 7 task cards: Multi-Currency ও FX

## ১৩. Phase 8 task cards: Tax Management

Phase 8-এর card P8-01 থেকে P8-18। সব নতুন table `acc_tax_` prefix-সহ, ব্যতিক্রম shared `acc_journal_line_taxes`। Capability flag `tax.enabled` (default বন্ধ)। বন্ধ থাকলে আচরণ Phase 7 শেষের মতোই, table খালি ও কেউ পড়ে না। সব write API `Idempotency-Key` নেয়। সব job `AccountingJob` base ধরে। Journal তৈরি হয় শুধু `JournalService::createDraft`-এ, status বদলায় শুধু `PostingService`।

### P8-01: Jurisdiction, registration ও return box schema

| Field | Specification |
| --- | --- |
| ধরন | `schema` |
| নির্ভর করে | P1-04, P1-10, P2-03 |
| অবরুদ্ধ করে | P8-02, P8-04, P8-11, P8-16 |
| OD | OD-37 (কোন jurisdiction আগে), OD-41 (return period ও frequency) |
| উদ্দেশ্য | Tax কাঠামোর ভিত্তি: কোন কর-কর্তৃপক্ষ, company-র কোন registration, return-এর কোন box। কিছুই seed হয় না। Bangladesh VAT বা TDS-এর মতো উদাহরণ import file হিসেবে থাকে (P8-16), table-এ আগে থেকে বসানো থাকে না |
| Schema | নিচের `acc_tax_jurisdictions`, `acc_tax_registrations`, `acc_tax_return_boxes` |
| DB flow (migration, এক ক্রমে) | (1) তিনটা table তৈরি। (2) `Schema::appendOnly()` লাগে না (এগুলো বদলায়, ইতিহাস `acc_tax_config_history`-এ, P8-02)। (3) কোনো seed নেই, কোনো বিদ্যমান table বদলায় না। Runtime write: jurisdiction `FOR UPDATE` (শুধু নিজের table), `version` মিলিয়ে UPDATE, `acc_tax_config_history` INSERT, audit, commit। Ledger lock নেই। ব্যর্থ হলে পুরো transaction rollback, `TAX_CONFLICT` |
| নিয়ম | R8.1, R8.27, R8.38, R8.41 |
| Contract | `TaxJurisdictionService::create/update/deactivate(company, ...)`। `TaxRegistrationValidator::validate(company, jurisdictionId, taxId): ValidationResult` (hook, Phase 9 party data দিয়ে ব্যবহার করবে)। API: `GET/POST /tax/jurisdictions`, `GET/PUT /tax/jurisdictions/{id}`, `POST /tax/jurisdictions/{id}/deactivate`, `GET/POST /tax/jurisdictions/{id}/registrations`, `PUT /tax/registrations/{id}`, `GET/PUT /tax/jurisdictions/{id}/boxes` (version সহ পুরো তালিকা বদল) |
| Permission | `accounting.tax.view` পড়া। `accounting.tax.manage` তৈরি ও বদল। Deactivate ও box বদলে দ্বিতীয় অনুমোদন `accounting.tax.approve` |
| Error | `TAX_JURISDICTION_NOT_FOUND`, `TAX_JURISDICTION_IN_USE`, `TAX_REGISTRATION_INVALID`, `TAX_CONFLICT`, `TAX_VALIDATION_FAILED` |
| Audit | `accounting.tax_jurisdiction.created`, `.updated`, `.deactivated`; `accounting.tax_registration.changed`; `accounting.tax_box.changed` (আগে ও পরের মান সহ) |
| Acceptance | AC8.1 |
| Test | `TaxJurisdictionTest`: (ক) দুই jurisdiction একই company-তে। (খ) একই code দ্বিতীয়বার আটকায়। (গ) ব্যবহৃত jurisdiction deactivate আটকায়। (ঘ) `version` অমিলে `TAX_CONFLICT`। (ঙ) অন্য company-র id composite FK-তে আটকায় |
| Deliverable | Migration, model, service, API, OpenAPI অংশ, `down()` মন্তব্য (table খালি হলে নিরাপদ) |
| Sub-tasks | (1) Migration ও model। (2) Service ও `version` check। (3) Registration ও pattern validator। (4) Box কাঠামো service। (5) API ও permission। (6) Test |

**`acc_tax_jurisdictions`**

| column | ধরন | null | নোট |
| --- | --- | --- | --- |
| id | BIGINT UNSIGNED PK | না |  |
| company\_id | BIGINT UNSIGNED | না |  |
| code | VARCHAR(30) | না | company-তে unique |
| name | VARCHAR(150) | না |  |
| country\_code | CHAR(2) | হ্যাঁ | তথ্য, logic-এ নয় |
| region | VARCHAR(100) | হ্যাঁ |  |
| authority\_name | VARCHAR(150) | হ্যাঁ |  |
| filing\_frequency | ENUM(monthly, quarterly, half\_yearly, annual) | না | default monthly |
| filing\_due\_days | SMALLINT | না | period শেষের পর কত দিনে due, default 15 |
| tax\_id\_label | VARCHAR(60) | হ্যাঁ | যেমন "Registration No" |
| tax\_id\_pattern | VARCHAR(200) | হ্যাঁ | regex, শুধু format যাচাই |
| rounding\_mode | ENUM(line, document) | হ্যাঁ | NULL হলে `tax.rounding_mode` |
| is\_active | TINYINT(1) | না | default 1 |
| version, created\_by, created\_at, updated\_at |  | না |  |

- `uq (company_id, code)`, `uq (company_id, id)`। CHECK `filing_due_days BETWEEN 0 AND 366`। Index `(company_id, is_active)`।

**`acc_tax_registrations`**

| column | ধরন | null | নোট |
| --- | --- | --- | --- |
| id, company\_id |  | না |  |
| jurisdiction\_id | BIGINT UNSIGNED | না |  |
| registration\_no | VARCHAR(60) | না |  |
| registered\_from | DATE | না |  |
| registered\_to | DATE | হ্যাঁ |  |
| status | ENUM(active, suspended, cancelled) | না | default active |
| is\_primary | TINYINT(1) | না | jurisdiction প্রতি একটাই primary (generated unique) |
| version, created\_by, created\_at, updated\_at |  | না |  |

- FK `fk_acc_taxreg_jur (company_id, jurisdiction_id) → acc_tax_jurisdictions (company_id, id)`। `uq (company_id, jurisdiction_id, registration_no)`, `uq (company_id, id)`। CHECK `registered_to IS NULL OR registered_to >= registered_from`। Index `(company_id, jurisdiction_id, status)`।

**`acc_tax_return_boxes`**

| column | ধরন | null | নোট |
| --- | --- | --- | --- |
| id, company\_id |  | না |  |
| jurisdiction\_id | BIGINT UNSIGNED | না |  |
| box\_code | VARCHAR(30) | না | return-এর ঘরের কোড |
| label | VARCHAR(200) | না |  |
| line\_kind | ENUM(output, input, withholding, adjustment, net, memo) | না |  |
| amount\_basis | ENUM(base, tax, both) | না |  |
| sign | TINYINT | না | 1 বা -1 |
| sort\_no | SMALLINT | না |  |
| is\_active | TINYINT(1) | না |  |
| version | INT | না |  |

- FK `fk_acc_taxbox_jur (company_id, jurisdiction_id)`। `uq (company_id, jurisdiction_id, box_code)`, `uq (company_id, id)`। CHECK `sign IN (1, -1)`।

### P8-02: Tax code, rate ও config history

| Field | Specification |
| --- | --- |
| ধরন | `schema` ও service |
| নির্ভর করে | P8-01, P1-04 (change request), P1-10 |
| অবরুদ্ধ করে | P8-03 থেকে P8-07, P8-11 |
| OD | OD-41 (tax point তারিখ), OD-44b (reverse charge) |
| উদ্দেশ্য | Tax code সাত ধরনে ও তারিখ-ভিত্তিক rate। Rate বদল এমনভাবে যাতে পুরোনো লেনদেন কখনো না বদলায়। প্রতিটা বদলের ইতিহাস append-only |
| Schema | নিচের `acc_tax_codes`, `acc_tax_rates`, `acc_tax_config_history` |
| DB flow (rate তৈরি, এক transaction) | (১) `acc_tax_codes` সারি `FOR UPDATE` (শুধু নিজের table)। (২) `version` মেলানো। (৩) নতুন `acc_tax_rates` সারি `status = pending`, `effective_from` > সব বিদ্যমান সারির `effective_from`। (৪) Retroactive পরীক্ষা: `SELECT MAX(tax_date) FROM acc_journal_line_taxes` ওই code-এ, posted journal join, কোনো lock ছাড়া পড়া। `effective_from <= সর্বশেষ tax_date` হলে `TAX_RATE_RETROACTIVE`। (৫) change request `taxrate:<id>` (P1-04), history INSERT। **অনুমোদনে (আলাদা transaction):** code `FOR UPDATE`, (৪) আবার, আগের open সারির `effective_to = new.effective_from - 1 দিন` (trigger-অনুমোদিত একমাত্র বদল), নতুন সারি `active`, `acc_tax_codes.rate_epoch` +1, history, audit। Ledger lock নেই। ব্যর্থ হলে সব rollback |
| নিয়ম | R8.2 থেকে R8.7, R8.11, R8.15, R8.16, R8.17, R8.41 |
| Contract | `TaxCodeService::create/update/deactivate`, `TaxRateService::propose(codeId, rate, effectiveFrom)`, `::activate(changeRequestId)`। `TaxRateResolver::resolve(company, codeId, date): TaxRate`। API: `GET/POST /tax/codes`, `GET/PUT /tax/codes/{id}`, `POST /tax/codes/{id}/deactivate`, `GET /tax/codes/{id}/rates`, `POST /tax/codes/{id}/rates` (প্রস্তাব), `POST /tax/rates/{id}/cancel` (pending হলে), `GET /tax/config-history?entity=&id=` |
| Permission | `accounting.tax.view`; `accounting.tax.manage` প্রস্তাব ও code বদল; `accounting.tax.approve` rate সক্রিয় (প্রস্তাবকারী ≠ অনুমোদনকারী) |
| Error | `TAX_CODE_NOT_FOUND`, `TAX_CODE_INACTIVE`, `TAX_CODE_IN_USE` (type বা jurisdiction বদল), `TAX_RATE_NOT_FOUND`, `TAX_RATE_OVERLAP`, `TAX_RATE_RETROACTIVE`, `TAX_RATE_IMMUTABLE`, `TAX_CONFLICT` |
| Audit | `accounting.tax_code.created`, `.updated`, `.deactivated`; `accounting.tax_rate.proposed`, `.activated`, `.cancelled` (আগের ও নতুন rate, effective তারিখসহ) |
| Acceptance | AC8.2, AC8.3 |
| Test | `TaxRateEffectiveTest`: (ক) rate 15 থেকে 12, effective ১ জানুয়ারি। ৩১ ডিসেম্বরের তারিখে 15, ১ জানুয়ারি 12। (খ) overlap আটকায়। (গ) ব্যবহৃত rate-এর `rate` UPDATE trigger-এ আটকায়। (ঘ) retroactive আটকায়। (ঙ) প্রস্তাবকারী নিজে সক্রিয় করতে পারে না। (চ) পুরোনো posted tax সারি অপরিবর্তিত |
| Deliverable | Migration, trigger, service, resolver, API, config history |
| Sub-tasks | (১) Migration ও model। (২) Rate trigger (শুধু `effective_to`, `status`, `first_used_at` বদল)। (৩) `TaxRateService` ও `taxrate:` ChangeApplier prefix। (৪) `TaxRateResolver` ও cache (`rate_epoch`)। (৫) Retroactive পরীক্ষা। (৬) History writer। (৭) API। (৮) Test |

**`acc_tax_codes`**

| column | ধরন | null | নোট |
| --- | --- | --- | --- |
| id, company\_id |  | না |  |
| jurisdiction\_id | BIGINT UNSIGNED | না |  |
| code | VARCHAR(30) | না | company-তে unique |
| name | VARCHAR(150) | না |  |
| tax\_type | ENUM(input, output, withholding, reverse\_charge, zero\_rated, exempt, out\_of\_scope) | না | ব্যবহৃত হলে অপরিবর্তনীয় |
| calc\_method | ENUM(percent, fixed\_amount) | না | default percent |
| price\_basis | ENUM(exclusive, inclusive) | না | default `tax.default_price_basis` |
| recoverable\_mode | ENUM(full, none, partial) | না | default full (input-এ অর্থবহ) |
| recoverable\_pct | DECIMAL(9,6) | হ্যাঁ | partial হলে বাধ্যতামূলক |
| withholding\_side | ENUM(deducted, suffered) | হ্যাঁ | withholding হলে বাধ্যতামূলক |
| box\_code\_base, box\_code\_tax | VARCHAR(30) | হ্যাঁ | return box (P8-01) |
| rate\_epoch | INT | না | default 1, cache key |
| is\_active | TINYINT(1) | না |  |
| version, created\_by, created\_at, updated\_at |  | না |  |

- FK `fk_acc_taxcode_jur (company_id, jurisdiction_id) → acc_tax_jurisdictions (company_id, id)`। `uq (company_id, code)`, `uq (company_id, id)`।
- CHECK `ck_taxcode_partial`: `(recoverable_mode = 'partial') = (recoverable_pct IS NOT NULL)` এবং pct 0.000001 থেকে 99.999999। CHECK `ck_taxcode_wh`: `(tax_type = 'withholding') = (withholding_side IS NOT NULL)`।
- Index `(company_id, jurisdiction_id, tax_type, is_active)`।

**`acc_tax_rates`**

| column | ধরন | null | নোট |
| --- | --- | --- | --- |
| id, company\_id |  | না |  |
| tax\_code\_id | BIGINT UNSIGNED | না |  |
| rate | DECIMAL(20,10) | হ্যাঁ | শতাংশ (15 মানে 15%) |
| fixed\_amount | DECIMAL(20,4) | হ্যাঁ | calc\_method fixed হলে |
| effective\_from | DATE | না |  |
| effective\_to | DATE | হ্যাঁ | NULL মানে খোলা |
| status | ENUM(pending, active, superseded, rejected) | না |  |
| change\_request\_id | BIGINT UNSIGNED | হ্যাঁ | P1-04 |
| first\_used\_at | DATETIME | হ্যাঁ | প্রথম posted ব্যবহারে বসে |
| proposed\_by, approved\_by, approved\_at |  | হ্যাঁ | approved\_by ≠ proposed\_by |
| created\_at | DATETIME | না |  |

- FK `fk_acc_taxrate_code (company_id, tax_code_id) → acc_tax_codes (company_id, id)`। `uq (company_id, tax_code_id, effective_from)`, `uq (company_id, id)`।
- CHECK: `(rate IS NULL) <> (fixed_amount IS NULL)`; `rate BETWEEN 0 AND 100`; `effective_to IS NULL OR effective_to >= effective_from`।
- Index `(company_id, tax_code_id, status, effective_from, effective_to)`।
- Trigger `trg_acc_tax_rates_bu`: `rate`, `fixed_amount`, `effective_from`, `tax_code_id` UPDATE নিষিদ্ধ যখন `status != 'pending'`। অনুমোদিত বদল: `effective_to` (NULL থেকে তারিখ), `status`, `first_used_at` (NULL থেকে), অনুমোদন column। DELETE নিষিদ্ধ।

**`acc_tax_config_history`** (`Schema::appendOnly()`)

| column | ধরন | null | নোট |
| --- | --- | --- | --- |
| id, company\_id |  | না |  |
| entity | ENUM(jurisdiction, registration, box, code, rate, group, account\_map, rule) | না |  |
| entity\_id | BIGINT UNSIGNED | না |  |
| action | ENUM(create, update, activate, deactivate, supersede, cancel) | না |  |
| old\_value, new\_value | JSON | হ্যাঁ |  |
| reason | VARCHAR(500) | হ্যাঁ |  |
| changed\_by | BIGINT UNSIGNED | না |  |
| changed\_at | DATETIME | না |  |

- Index `(company_id, entity, entity_id, changed_at)`। UPDATE ও DELETE trigger-এ নিষিদ্ধ।

### P8-03: Tax group ও component

| Field | Specification |
| --- | --- |
| ধরন | `schema` ও service |
| নির্ভর করে | P8-02 |
| অবরুদ্ধ করে | P8-05, P8-06 |
| OD | OD-44b (reverse charge group), OD-39b (partial recovery) |
| উদ্দেশ্য | একাধিক component ও compound ক্রম। Group নিজে rate রাখে না, member code-এর rate ব্যবহার হয় |
| Schema | নিচের `acc_tax_groups`, `acc_tax_group_members` |
| DB flow | Group তৈরি বা বদল এক transaction: group `FOR UPDATE` (নিজের table), member code-গুলো একই jurisdiction ও active কি পড়া (lock নেই), সব member সারি delete-insert (draft group), history, `rate_epoch` কোনো member code-এ নয় বরং group `version` +1। ব্যবহৃত group-এর member বদলালে নতুন group `version` সারি নয়, `TAX_GROUP_IN_USE`: নতুন group বানাও। ব্যর্থ হলে rollback |
| নিয়ম | R8.8, R8.9, R8.10 |
| Contract | `TaxGroupService::create/update/deactivate`। API: `GET/POST /tax/groups`, `GET/PUT /tax/groups/{id}`, `POST /tax/groups/{id}/deactivate` |
| Permission | `accounting.tax.view`, `accounting.tax.manage` |
| Error | `TAX_GROUP_INVALID`, `TAX_GROUP_JURISDICTION_MIX`, `TAX_GROUP_IN_USE`, `TAX_CODE_INACTIVE` |
| Audit | `accounting.tax_group.created`, `.updated`, `.deactivated` |
| Acceptance | AC8.4 |
| Test | `TaxGroupTest`: (ক) দুই component compound। (খ) ভিন্ন jurisdiction-এর member আটকায়। (গ) একই code দুইবার আটকায়। (ঘ) ব্যবহৃত group-এর member বদল আটকায় |
| Deliverable | Migration, service, API |
| Sub-tasks | (১) Migration। (২) Service ও validation। (৩) API। (৪) Test |

**`acc_tax_groups`**

| column | ধরন | null | নোট |
| --- | --- | --- | --- |
| id, company\_id |  | না |  |
| jurisdiction\_id | BIGINT UNSIGNED | না |  |
| code, name | VARCHAR(30), VARCHAR(150) | না |  |
| rounding\_mode | ENUM(line, document) | হ্যাঁ | NULL হলে jurisdiction/setting |
| is\_active | TINYINT(1) | না |  |
| first\_used\_at | DATETIME | হ্যাঁ |  |
| version, created\_by, created\_at, updated\_at |  | না |  |

- FK `(company_id, jurisdiction_id)`। `uq (company_id, code)`, `uq (company_id, id)`।

**`acc_tax_group_members`**

| column | ধরন | null | নোট |
| --- | --- | --- | --- |
| id, company\_id |  | না |  |
| group\_id, tax\_code\_id | BIGINT UNSIGNED | না |  |
| seq\_no | SMALLINT | না | 1 থেকে |
| apply\_on | ENUM(base, base\_plus\_previous) | না | seq 1-এ অবশ্যই base |

- FK `(company_id, group_id) → acc_tax_groups (company_id, id)`, `(company_id, tax_code_id) → acc_tax_codes (company_id, id)`। `uq (company_id, group_id, seq_no)`, `uq (company_id, group_id, tax_code_id)`, `uq (company_id, id)`। CHECK `seq_no >= 1`।

### P8-04: Tax account map

| Field | Specification |
| --- | --- |
| ধরন | `schema` ও service |
| নির্ভর করে | P8-01, P8-02, P1-04, বিদ্যমান CoA (`acc_accounts`) |
| অবরুদ্ধ করে | P8-05, P8-13, P8-16 |
| OD | OD-39a (non-recoverable posting) |
| উদ্দেশ্য | কোন tax কোন account-এ যাবে তা তারিখ-ভিত্তিক configuration। কোডে কোনো account বাঁধা নেই। Map বদলালে পুরোনো লেনদেনে কিছু বদলায় না, কারণ posted line-এ account\_id সরাসরি লেখা |
| Schema | নিচের `acc_tax_account_map` |
| DB flow | বদলের অনুরোধ এক transaction: jurisdiction সারি `FOR UPDATE` (শুধু নিজের table), `version` মেলানো, account যাচাই (সাধারণ read, ledger lock নেই): active, group নয়, posting অনুমোদিত, type role-এর সাথে মেলে। নতুন সারি `pending`, change request `taxmap:<id>` (P1-04), history। অনুমোদনে এক transaction: jurisdiction `FOR UPDATE`, আবার যাচাই, আগের open সারির `effective_to` বন্ধ, নতুন সারি `active`, jurisdiction-এর config epoch +1 (cache বাতিল), audit। Account পরে নিষ্ক্রিয় বা type বদল হলে CoA service map পড়ে আটকায় (`TAX_ACCOUNT_IN_USE`)। ব্যর্থ হলে সব rollback |
| নিয়ম | R8.18, R8.41 |
| Contract | `TaxAccountMapService::propose/activate`, `::resolve(company, jurisdictionId, taxCodeId, role, date): accountId`। API `GET/PUT /tax/account-map` (পুরো তালিকা, `version` সহ) |
| Permission | view, manage, approve (দ্বিতীয় ব্যক্তি) |
| Error | `TAX_ACCOUNT_MAP_MISSING`, `TAX_ACCOUNT_TYPE_MISMATCH`, `TAX_ACCOUNT_IN_USE`, `TAX_CONFLICT` |
| Audit | `accounting.tax_account_map.proposed`, `.activated` (আগের ও নতুন account, effective তারিখ) |
| Acceptance | AC8.5 |
| Test | `TaxAccountMapTest`: (ক) payable role-এ asset account আটকায়। (খ) role ফাঁকা থাকলে calculate `TAX_ACCOUNT_MAP_MISSING`। (গ) map বদলের পরেও পুরোনো posted line একই account। (ঘ) mapped account নিষ্ক্রিয় করা আটকায়। (ঙ) নিজের প্রস্তাব নিজে অনুমোদন আটকায় |
| Deliverable | Migration, service, resolver, CoA hook, API |
| Sub-tasks | (১) Migration। (২) Account যাচাই। (৩) `taxmap:` ChangeApplier। (৪) Resolver ও cache। (৫) CoA service hook। (৬) API। (৭) Test |

**`acc_tax_account_map`**

| column | ধরন | null | নোট |
| --- | --- | --- | --- |
| id, company\_id |  | না |  |
| jurisdiction\_id | BIGINT UNSIGNED | না |  |
| tax\_code\_id | BIGINT UNSIGNED | হ্যাঁ | jurisdiction-স্তরের role-এ NULL |
| tax\_code\_key | BIGINT UNSIGNED | না | generated `COALESCE(tax_code_id, 0)`, unique-এর জন্য |
| role | ENUM(tax\_payable, tax\_receivable, non\_recoverable\_expense, withholding\_payable, withholding\_receivable, net\_tax\_payable, tax\_refundable, rounding) | না |  |
| account\_id | BIGINT UNSIGNED | না |  |
| effective\_from | DATE | না |  |
| effective\_to | DATE | হ্যাঁ |  |
| status | ENUM(pending, active, superseded, rejected) | না |  |
| change\_request\_id | BIGINT UNSIGNED | হ্যাঁ |  |
| version, created\_by, created\_at |  | না |  |

- FK `(company_id, jurisdiction_id)` → jurisdictions, `(company_id, tax_code_id)` → codes, `(company_id, account_id) → acc_accounts (company_id, id)`। `uq (company_id, id)`, `uq (company_id, jurisdiction_id, role, tax_code_key, effective_from)`।
- CHECK `ck_taxmap_scope`: role `net_tax_payable`, `tax_refundable`, `rounding` হলে `tax_code_id IS NULL`, অন্য role-এ NOT NULL। CHECK `effective_to IS NULL OR effective_to >= effective_from`।
- Index `(company_id, tax_code_id, role, status, effective_from)`, `(company_id, account_id)` (CoA hook-এর জন্য)।
- Trigger: `account_id`, `role`, `effective_from` শুধু `pending` অবস্থায় বদলায়। পরে শুধু `effective_to`, `status`।

### P8-05: TaxService, গণনা engine ও rounding

| Field | Specification |
| --- | --- |
| ধরন | `service` (নতুন table নেই) |
| নির্ভর করে | P8-02, P8-03, P8-04, P7 (`FxService::convert`, base amount আনতে) |
| অবরুদ্ধ করে | P8-06 থেকে P8-09, P8-17 |
| OD | OD-40 (rounding), OD-39a, OD-39b, OD-42 (foreign currency), OD-44b |
| উদ্দেশ্য | সব tax হিসাবের একটাই জায়গা। Pure function: একই input ও config epoch হলে একই ফল। কিছু লেখে না, তাই preview, journal, Phase 14, সবাই একই ফল পায় |
| Schema | নতুন table নেই। Cache: process-এর ভেতর, key `(company, code_id, rate_epoch, date)` |
| DB flow | শুধু read: `acc_tax_codes`, `acc_tax_rates`, `acc_tax_groups`, `acc_tax_group_members`, `acc_tax_account_map`, `acc_tax_determination_rules`। কোনো lock নেই, কোনো write নেই। Transaction লাগে না (repeatable-read snapshot caller-এর transaction থেকে)। ব্যর্থ হলে exception, কিছু লেখা হয়নি |
| নিয়ম | R8.6, R8.9 থেকে R8.17, R8.40 |
| Contract | `TaxService::calculate(company, TaxInput): TaxResult`। `TaxInput = {tax_date, side (sale, purchase, expense, payment_out, receipt_in), lines[{ref, amount (base currency), price_basis?, tax_code_id? বা tax_group_id? বা determination{txn_type, party_tax_status, item_category, service_category, account_id}, quantity?}]}`। `TaxResult = {lines[{ref, components[{tax_code_id, tax_rate_id, rate_snapshot, component_seq, base_amount, tax_amount, direction, is_recoverable, posting_mode, account_id, box_code_tax, box_code_base}]}], base_total, tax_total, gross_total, rounding_notes[], warnings[], calculation_hash}`। API `POST /tax/calculate` (শুধু preview) |
| গণনা ধাপ | (১) প্রতি line-এর code বা group ঠিক (সরাসরি, নইলে P8-06)। (২) প্রতি component-এর rate `TaxRateResolver` থেকে তারিখ ধরে। (৩) Exclusive: component ক্রমে `tax = (base + আগের কর, apply_on অনুযায়ী) × rate ÷ 100`। (৪) Inclusive: `base = gross ÷ f`, যেখানে `f` = base 1 ধরে exclusive গণনার `1 + Σ tax`। এতে additive ও compound দুটোই সামলায়। (৫) Rounding: `line` মোডে প্রতি component পূর্ণসংখ্যায় (precision, method)। `document` মোডে code ধরে মোট একবার round, ফারাক line-এ largest remainder পদ্ধতিতে, tie-এ কম line\_no। Inclusive-এ `base = gross − Σ tax` যাতে মোট ঠিক gross হয়। (৬) Recoverable ভাগ: full সব, none শূন্য, partial `round(tax × pct ÷ 100)`, বাকিটা non-recoverable। (৭) Reverse charge: সমান অঙ্কের output ও input জোড়া। (৮) Zero-rated, exempt, out of scope: `tax_amount = 0` সারি, base সহ। (৯) Withholding: `withholding_side` অনুযায়ী role। (১০) Account map থেকে account। (১১) invariant: tax\_amount >= 0, `Σ components + base = gross` (inclusive), hash |
| Permission | preview-এর জন্য `accounting.tax.view` |
| Error | `TAX_RATE_NOT_FOUND`, `TAX_CODE_INACTIVE`, `TAX_BASIS_INVALID`, `TAX_ROUNDING_CONFLICT`, `TAX_ACCOUNT_MAP_MISSING`, `TAX_RULE_NO_MATCH`, `TAX_RULE_AMBIGUOUS` |
| Audit | নেই (pure)। Preview-র ব্যবহার সাধারণ request log-এ |
| Acceptance | AC8.6, AC8.7, AC8.8 |
| Test | `TaxCalculationTest`: golden G8.1 থেকে G8.5, G8.8। Property test: inclusive-এর পর `base + tax = gross` সবসময়। Document মোডে line-এর যোগফল = document-এর মোট। একই input দুইবার একই hash। Half\_up বনাম half\_even ফারাক |
| Deliverable | Service, `TaxRounding`, `TaxRateResolver` cache, API, golden fixture ফাইল |
| Sub-tasks | (১) `TaxInput`/`TaxResult` DTO। (২) Component engine (exclusive, inclusive factor)। (৩) `TaxRounding` (line, document, largest remainder)। (৪) Recoverable ভাগ। (৫) Reverse charge ও withholding। (৬) Account resolution। (৭) Hash। (৮) API preview। (৯) Golden ও property test |

**P8-06 থেকে P8-18, Registry যোগ ও Build ক্রমের সারি:** Pack tab-এর block সীমা (২০,০০০) পূর্ণ হওয়ায় আলাদা উপ-tab-এ: Phase 8 task cards: P8-06 থেকে P8-18

## ১৪. Phase 9 task cards: Subledgers (AR ও AP) ও Party

মূল tab-এর block সীমা পূর্ণ হওয়ায় Phase 9-এর সব card আলাদা tab-এ আছে: Phase 9 task cards: Subledgers (AR ও AP) ও Party

## ১৫. Phase 10 task cards: Bank ও Cash Management

মূল tab-এর block সীমা শেষ, তই ১৫টা card (P10-01 থেকে P10-15), Registry যোগ ও Build ক্রমের সারি আলাদা tab-এ: Phase 10 task cards (P10-01 থেকে P10-15)

## ১৬. Phase 11 task cards: Fixed Asset Accounting

Phase 11-এ P11-01 থেকে P11-15, পিছনে Registry যোগ ও Build ক্রমের সারি। Block সীমার কারণে সব card আলাদা tab-এ: Phase 11 task cards (Fixed Asset Accounting)

## ১৭. Phase 12 task cards: Period-End, Recurring ও Year-End

Phase 12-এর card P12-01 থেকে P12-14, Registry যোগ ও Build ক্রমের সারি আলাদা tab-এ আছে (এই tab-এর block limit-এর কারণে): Phase 12 (task cards)

## ১৮. Phase 13 task cards: Budgeting, Forecast ও Management Reporting

Tab-এর আকারের সীমা (20,000 block) থাকায় P13-01 থেকে P13-21 card, Registry যোগ ও Build ক্রমের সারি আলাদা tab-এ লেখা: Phase 13 task cards (Budgeting, Forecast, Management Reporting)

## ১৯. Phase 14 task cards: Posting Rules Engine ও Source Integrations

Card P14-01 থেকে P14-24, Registry যোগ আর Build ক্রমের সারি আলাদা tab-এ (এই doc-এর block সীমা পূর্ণ হওয়ায়): Phase 14 task cards (Posting Rules, Source Integrations)

## ২০. Phase 15 task cards: Multi-Entity, Intercompany ও Consolidation

Phase 15-এর card P15-01 থেকে P15-14, Registry যোগ ও Build ক্রমের সারি আলাদা tab-এ লেখা, কারণ এই tab-এর block সীমা শেষের দিকে: Phase 15 task cards (Multi-Entity, Consolidation)

## ২১. Phase 16 task cards: Compliance, Audit, Data Lifecycle, Migration ও API

১৮টা card (P16-01 থেকে P16-18), ১৭টা নতুন table। Card, সব Schema, “Registry যোগ” ও “Build ক্রমের সারি” আলাদা tab-এ: ২১. Phase 16 cards (P16-01 থেকে P16-18), Registry, Build ক্রম

## ২২. Phase 17 task cards: Enterprise UAT, Parallel Run ও Full Release

Phase 17-এ কোনো নতুন table নেই। Card দুই ধরনের। এক, test ও অপারেশন: UAT, migration মহড়া, parallel run, performance, DR, penetration test, accessibility, training, support, handover। দুই, release control: go or no-go, wave rollout, hypercare, rollback, governance। এই দ্বিতীয় দল বিদ্যমান P5-04, P5-06, P5-08 বাড়ায়, বদলায় না। Card ID P17-01 থেকে P17-20। Scenario ID ও traceability table Blueprint ২৭.৯-এ।

### P17-01: UAT কৌশল, পরিবেশ ও scenario catalog

| Field | Specification |
| --- | --- |
| ধরন | `ops` + `test` |
| নির্ভর করে | P5-01, P5-02, আর Phase 6 থেকে 16-এর প্রতিটা exit gate (সব P6 থেকে P16 card সবুজ) |
| অবরুদ্ধ করে | P17-02 থেকে P17-12, P17-15 |
| OD | OD-105a, OD-105b, OD-108 |
| উদ্দেশ্য | সব capability একই নিয়মে, একই পরিবেশে, একই প্রমাণের ছাঁচে পরীক্ষা করার কাঠামো বানানো |
| Schema | নতুন table নেই |
| DB flow | UAT পরিবেশ = production-copy, মাস্ক করা। পড়া: সব `acc_` table। লেখা: শুধু সাধারণ API দিয়ে, `PostingService` হয়ে। সরাসরি SQL insert নেই। প্রমাণ জমে `acc_reconciliation_runs` (RC1 থেকে RC18) ও `acc_report_exports`-এ। প্রতিটা UAT cycle শুরুতে snapshot restore। ব্যর্থ হলে cycle থামে, snapshot-এ ফেরা হয়, ledger-এ আংশিক অবস্থা থাকে না |
| নিয়ম | R17.1 (UAT ও production আলাদা), R17.2 (expected result আগে লেখা), R17.3 (প্রমাণ ছাড়া pass নেই) |
| Contract | Scenario script ছাঁচ: id, capability, precondition, ধাপ, expected ledger line (account, debit, credit), RC check তালিকা, প্রমাণের লিংক, দায়ী persona। Catalog-এর ID: `UAT-{phase}.{n}`, `UAT-E{n}`, `UAT-N{n}`, `MIG-{n}`, `PF-{n}`, `DR-{n}`, `PT-{n}` |
| Permission | `accounting.release.view` (দেখা), `accounting.release.manage` (cycle চালানো)। প্রতি persona-র জন্য seed করা role: accountant, approver, AP clerk, AR clerk, treasury, auditor, admin, read-only |
| Error | `REL_PRECONDITION_FAILED` (পরিবেশ বা data প্রস্তুত নয়) |
| Audit | `accounting.release.uat_cycle_started`, `accounting.release.uat_cycle_signed` |
| Acceptance | AC17.1: catalog-এ প্রতিটা capability-র অন্তত একটা scenario। AC17.2: প্রতিটা scenario-র expected result লেখা আছে run-এর আগে |
| Test | Catalog completeness check: P6 থেকে P16-এর প্রতিটা AC কোনো না কোনো scenario-তে ঢাকা (traceability table খালি ঘর শূন্য) |
| Deliverable | UAT কৌশল doc, পরিবেশ বিবরণ, persona ও role তালিকা, scenario ছাঁচ, catalog, defect severity সংজ্ঞা (sev1 থেকে sev4) |
| Sub-tasks | 1) পরিবেশ ও data masking নিয়ম। 2) Persona ও role seed। 3) Scenario ছাঁচ ও ID নিয়ম। 4) Defect severity ও blocking নিয়ম। 5) Cycle calendar (কতটা cycle, কখন)। 6) Traceability table ভরা |

### P17-02: Capability UAT script, দল ক (journal, FX, tax, subledger, bank)

| Field | Specification |
| --- | --- |
| ধরন | `test` |
| নির্ভর করে | P17-01, সব P6, P7, P8, P9, P10 card |
| অবরুদ্ধ করে | P17-05, P17-15 |
| OD | - |
| উদ্দেশ্য | Journal management ও dimensions, multi-currency, tax, subledger, bank, প্রতিটার scripted scenario লেখা ও চালানো, expected ledger result ও RC সহ |
| Schema | নতুন table নেই |
| DB flow | প্রতিটা scenario: precondition data API দিয়ে বসানো, ক্রিয়া চালানো, তারপর পড়া: `acc_journals`, `acc_journal_lines`, `acc_journal_line_dimensions`, `acc_journal_line_taxes`, `acc_period_balances`। তুলনা: লিখিত expected line বনাম ledger। RC10, RC12, RC13, RC14 চালানো। কোনো লেখা সরাসরি নয়। ব্যর্থ হলে defect রেকর্ড, ledger হাত দিয়ে ঠিক করা নয়, reversal বা adjustment দিয়ে |
| নিয়ম | R17.2, R17.3, R17.4 (প্রতিটা scenario-তে debit সমান credit নিশ্চিত) |
| Contract | Blueprint ২৭.৯ক-এর scenario তালিকা (UAT-6.x থেকে UAT-10.x) |
| Permission | persona role অনুযায়ী। Scenario নিজেই permission বাদ দেয় না |
| Error | সংশ্লিষ্ট capability-র error code (JTY\_, DIM\_, FX\_, TAX\_, PTY\_, OPI\_, BNK\_) প্রত্যাশিত negative case-এ |
| Audit | `accounting.release.uat_scenario_result` |
| Acceptance | AC17.3: দলের সব scenario pass বা অনুমোদিত exception। কোনো sev1 বা sev2 খোলা নেই |
| Test | নিজেই test। প্রতিটা চালানোর ফল আলাদা সংখ্যা সহ লগ |
| Deliverable | চালানো scenario-র ফল, defect তালিকা, accountant-এর সই |
| Sub-tasks | 1) Phase 6 scenario। 2) Phase 7। 3) Phase 8। 4) Phase 9। 5) Phase 10। 6) Defect triage ও পুনরায় চালানো |

### P17-03: Capability UAT script, দল খ (fixed asset, period-end ও year-end, budgeting ও reporting)

| Field | Specification |
| --- | --- |
| ধরন | `test` |
| নির্ভর করে | P17-01, সব P11, P12, P13 card |
| অবরুদ্ধ করে | P17-05, P17-15 |
| OD | কোনোটি নয় |
| উদ্দেশ্য | Fixed asset, recurring ও deferral ও allocation, close checklist, year-end, budget, forecast, report designer, KPI, সবকটার scripted scenario ও expected result |
| Schema | নতুন table নেই |
| DB flow | পড়া: `acc_journals`, `acc_journal_lines`, `acc_period_balances`, `acc_periods`, `acc_period_events`। চালানো সব API ও job দিয়ে (`AccountingJob`)। Year-end scenario আলাদা snapshot-এ, কারণ ফেরানো যায় না। RC11, RC16, RC17 চালানো। ব্যর্থ হলে snapshot restore, ledger হাতে ঠিক নয় |
| নিয়ম | R17.2, R17.4, R17.5 (year-end scenario সবসময় snapshot-এ) |
| Contract | Blueprint ২৭.৯ক-এর UAT-11.x থেকে UAT-13.x |
| Permission | persona role অনুযায়ী |
| Error | FA\_, PER\_, REC\_, DEF\_, ALC\_, YER\_, BUD\_, RPD\_ প্রত্যাশিত negative case-এ |
| Audit | `accounting.release.uat_scenario_result` |
| Acceptance | AC17.3: দলের সব scenario pass বা অনুমোদিত exception। Year-end carry-forward-এর পর opening balance = আগের closing balance (RC1) |
| Test | নিজেই test। Year-end scenario দুইবার চালানো (একবার snapshot থেকে, ফল একই কিনা) |
| Deliverable | ফল, defect তালিকা, accountant ও controller-এর সই |
| Sub-tasks | 1) Phase 11 scenario। 2) Phase 12 scenario, year-end আলাদা। 3) Phase 13 scenario। 4) Defect triage ও পুনরায় চালানো |

### P17-04: Capability UAT script, দল গ (posting rules, integration, consolidation, compliance, migration)

| Field | Specification |
| --- | --- |
| ধরন | `test` |
| নির্ভর করে | P17-01, সব P14, P15, P16 card |
| অবরুদ্ধ করে | P17-05, P17-07, P17-15 |
| OD | OD-105b |
| উদ্দেশ্য | Posting rule engine ও source integration (Billing, Procurement, Payroll, Inventory, Project, Commissioning, Asset event), consolidation, auditor access, audit chain, import, archive, API ও webhook, সবকটার scripted scenario |
| Schema | নতুন table নেই |
| DB flow | পড়া: `acc_finance_event_inbox`, `acc_finance_event_journal_links`, `acc_journals`, `acc_journal_lines`। প্রতিটা source module থেকে নমুনা event inbox-এ আসে, Draft journal তৈরি হয়, মানুষ approve করে, post হয়। Event ফেরত চালানো (replay) স্বাভাবিক সংখ্যায় একই ফল দেয় কিনা দেখা (idempotency)। RC15, RC18 চালানো |
| নিয়ম | R17.2, R17.4, R17.6 (event replay-এ নতুন journal নয়, শুধু লিংক পুনরুদ্ধার) |
| Contract | Blueprint ২৭.৯ক-এর UAT-14.x থেকে UAT-16.x |
| Permission | persona role, সাথে auditor ও API client role |
| Error | RUL\_, EVC\_, GRP\_, ICO\_, CON\_, AUD\_, IMP\_, ARC\_, API\_ প্রত্যাশিত negative case-এ |
| Audit | `accounting.release.uat_scenario_result` |
| Acceptance | AC17.3: সব scenario pass বা অনুমোদিত exception। কোনো source event সরাসরি ledger-এ লেখে না (G9 ধরে) |
| Test | নিজেই test। প্রতিটা source module-এর অন্তত একটা সুস্থ ও একটা ভুল event |
| Deliverable | ফল, defect তালিকা, tech lead ও accountant-এর সই |
| Sub-tasks | 1) Phase 14 scenario, প্রতি source আলাদা। 2) Phase 15 scenario। 3) Phase 16 scenario। 4) Defect triage ও পুনরায় চালানো |

### P17-05: Cross-module end-to-end scenario ও golden test

| Field | Specification |
| --- | --- |
| ধরন | `test` |
| নির্ভর করে | P17-02, P17-03, P17-04 |
| অবরুদ্ধ করে | P17-07, P17-08, P17-15 |
| OD | OD-105b (data volume) |
| উদ্দেশ্য | চারটা পথ শুরু থেকে শেষ চালানো, প্রতি ধাপে ledger ও RC যাচাই। একটা capability আলাদাভাবে ঠিক হলেও জোড়ায় ভাঙতে পারে, এটা ধরা |
| Schema | নতুন table নেই |
| DB flow | পথ E1: Project event, `acc_finance_event_inbox`, Draft journal, post, open item (`acc_` receivable), bank statement line match, bank reconciliation, tax return, period close (`acc_periods`, `acc_period_events`), year-end journal, consolidation journal। পথ E2: Payroll event, payable, bank payment, bank match। পথ E3: Asset acquisition event, capitalization, প্রতি মাসে depreciation run, disposal। পথ E4: বিদেশি currency invoice, revaluation journal, settlement, realized gain। প্রতি ধাপের পর RC1 থেকে RC18-এর প্রযোজ্যগুলো চলে। প্রতিটা পথ একটা snapshot থেকে শুরু, ব্যর্থ হলে ওই ধাপ থেকে আবার নয়, snapshot থেকে শুরু |
| নিয়ম | R17.2, R17.4, R17.7 (E2E-তে পরের ধাপ আগের ধাপের posted ফলের উপর, কখনো test data হাতে বসিয়ে নয়) |
| Contract | Blueprint ২৭.৯গ-এর UAT-E1 থেকে E4 ও ২৭.৯ঙ-এর G17.1 থেকে G17.4। প্রতিটার সংখ্যা সহ expected result সেখানে |
| Permission | সব persona আলাদা login, এক ব্যবহারকারী পুরো পথ নয় (SoD সত্যিকারের মতো) |
| Error | কোনোটি নতুন নয়। যেখানে ব্যর্থ, সেই capability-র prefix |
| Audit | `accounting.release.uat_scenario_result` |
| Acceptance | AC17.4: চার পথ pass। AC17.5: প্রতি ধাপে debit = credit, শেষে RC1 থেকে RC18 সবুজ |
| Test | G17.1, G17.2, G17.3, G17.4 স্বয়ংক্রিয় ও MySQL-এ। E1 হাতে চালানো UAT-ও (accountant দেখে) |
| Deliverable | চার পথের ফল প্রতিবেদন, প্রতি ধাপের ledger screenshot বা export, accountant ও finance owner-এর সই |
| Sub-tasks | 1) E1 পথ। 2) E2 পথ। 3) E3 পথ। 4) E4 পথ। 5) Golden test স্বয়ংক্রিয় করা। 6) প্রতি ধাপের RC প্রমাণ |

### P17-06: Negative, security ও isolation scenario

| Field | Specification |
| --- | --- |
| ধরন | `test` |
| নির্ভর করে | P17-02, P17-03, P17-04 |
| অবরুদ্ধ করে | P17-11, P17-15 |
| OD | OD-107 |
| উদ্দেশ্য | সিস্টেম ভুল কাজ ঠিকভাবে ফিরিয়ে দেয় কিনা, আর এক company-র তথ্য অন্যের কাছে যায় কিনা, প্রমাণ করা |
| Schema | নতুন table নেই |
| DB flow | প্রতিটা negative scenario-তে প্রত্যাশা: ledger-এ কোনো বদল নেই, `acc_idempotency_keys`-এ প্রবেশ বা না-প্রবেশ নিয়ম মতো, audit-এ ব্যর্থ চেষ্টার রেকর্ড। Isolation: দুই company-তে একই code ও id, এক company-র token দিয়ে অন্যের id ডাকলে 404। পড়া: `activity_logs`, `acc_journals` গণনা আগে ও পরে |
| নিয়ম | R17.8 (negative scenario-র পাস মানে ফল অপরিবর্তিত ও error আকার সঠিক), R17.9 |
| Contract | Blueprint ২৭.৯ঘ-এর UAT-N1 থেকে N14 |
| Permission | অনুমতিহীন persona বানানো হয় ইচ্ছা করে |
| Error | প্রত্যাশিত `AccountingError` আকার `{code, message, field, details, correlation_id}`, আর কখনো stack trace বা অন্য company-র তথ্য নয় |
| Audit | `accounting.release.uat_scenario_result`, আর প্রত্যাখ্যাত চেষ্টার নিজস্ব audit |
| Acceptance | AC17.6: সব negative pass। AC17.7: company isolation scenario শূন্য ফাঁস |
| Test | `ReleaseNegativeTest`, `CompanyIsolationTest` (MySQL)। সবগুলো Cycle 3-এ হাতে ও স্বয়ংক্রিয়ে |
| Deliverable | ফল, ব্যর্থ চেষ্টার log নমুনা, security সই |
| Sub-tasks | 1) অনুমতি ও SoD। 2) Company isolation। 3) Duplicate, locked period, ভুল mapping। 4) Malformed webhook ও API token। 5) Audit chain ছেঁড়া। 6) ব্যর্থ posting ও retry |

### P17-07: Data migration মহড়া, বাস্তব volume-এ

| Field | Specification |
| --- | --- |
| ধরন | `ops` + `test` |
| নির্ভর করে | P17-04, সব P16 import card, P5-02 |
| অবরুদ্ধ করে | P17-08, P17-15, P17-16 |
| OD | OD-105b (volume), OD-119 |
| উদ্দেশ্য | প্রতি company-র বাস্তব data দিয়ে import তিনবার চালিয়ে সময়, মিল ও ত্রুটির হার মাপা, যাতে production cutover-এ কোনো আশ্চর্য না থাকে |
| Schema | নতুন table নেই |
| DB flow | মহড়ার পরিবেশে: snapshot, তারপর Phase 16 `ImportService` দিয়ে dry-run (কিছু লেখে না, শুধু ত্রুটি দেখায়), copy (opening journal ও master data mirror লেখে, `PostingService` দিয়ে), তারপর RC1 থেকে RC18-এর প্রযোজ্যগুলো ও legacy trial balance-এর সাথে মিল। Rollback মহড়া = snapshot restore। সময়, মেমরি, lock অপেক্ষা মাপা। ব্যর্থ হলে snapshot restore, আর সমস্যার বিবরণ সহ আবার |
| নিয়ম | R17.1, R17.3, R17.14 (production-এ migration rollback নেই, শুধু মহড়ায় restore) |
| Contract | MIG-1 থেকে MIG-6 (Blueprint ২৭.৯চ) |
| Permission | `accounting.import.manage` (Phase 16), `accounting.release.manage` |
| Error | `IMP_` (Phase 16), `REL_PRECONDITION_FAILED` |
| Audit | `accounting.release.migration_rehearsal_done` |
| Acceptance | AC17.8: তিনবারের শেষবার সময় বরাদ্দ cutover জানালার ভেতরে। Legacy trial balance = নতুন opening trial balance (control total শূন্য পার্থক্য)। ত্রুটির হার লিখিত সীমার ভেতরে |
| Test | Dry-run দুইবার একই ফল (idempotency)। Copy দুইবার নতুন journal বানায় না। Restore-এর পর সবুজ RC |
| Deliverable | প্রতি company-র মহড়া প্রতিবেদন (সময়, volume, ত্রুটি, মিল), cutover ঘড়ি, cutover runbook |
| Sub-tasks | 1) Volume নিশ্চিত (OD-105b)। 2) মহড়া 1 dry-run। 3) মহড়া 2 copy ও মিল। 4) মহড়া 3 সময়-মাপা ও rollback। 5) Opening balance মিল। 6) Cutover ঘড়ি ও runbook |

### P17-08: Parallel run ও তুলনা

| Field | Specification |
| --- | --- |
| ধরন | `ops` + `api` (OD-105a: script বা API) |
| নির্ভর করে | P17-05, P17-07 |
| অবরুদ্ধ করে | P17-15 |
| OD | OD-105, OD-105a, OD-108b |
| উদ্দেশ্য | নির্ধারিত সংখ্যক period পুরোনো ও নতুন পদ্ধতি একসাথে চালানো, প্রতিদিন ও period-end মেলানো |
| Schema | নতুন table নেই |
| DB flow | পড়া: `acc_period_balances`, `acc_journals`, `acc_journal_lines` (নতুন পক্ষ)। Legacy পক্ষ ফাইলে আসে (account code, debit, credit), Phase 16 import mapping-এ নতুন account-এ ম্যাপ। লেখা: কেবল `acc_report_exports`-এ তুলনা report (প্রতি account: legacy, নতুন, পার্থক্য, অবস্থা) ও `acc_enablement_log`-এ এন্ট্রি। Ledger-এ কোনো লেখা নেই। ব্যর্থ হলে ওই দিনের তুলনা আবার চালানো যায় (idempotent, period key + দিন) |
| নিয়ম | R17.10, R17.11 |
| Contract | `POST /accounting/release/parallel-runs/{period_key}/compare` (দৈনিক ও period-end)। Discrepancy শ্রেণি: timing, rounding, mapping, rule পার্থক্য, legacy ভুল, নতুন system defect। অবস্থা: open, explained, corrected, accepted (অনুমোদন সহ) |
| Permission | `accounting.release.manage` (চালানো), `accounting.release.approve` (accepted অবস্থা) |
| Error | `REL_PARALLEL_RUN_OPEN` (exit শর্ত অপূর্ণ) |
| Audit | `accounting.release.parallel_run_compared`, `accounting.release.discrepancy_resolved` |
| Acceptance | AC17.9: সব period-এ অব্যাখ্যাত discrepancy শূন্য, control total পার্থক্য শূন্য, প্রতি accepted exception-এ finance owner-এর সই |
| Test | বানানো ভুল (mapping, সময়, rounding) ঢোকালে তুলনা সঠিক শ্রেণিতে ধরে কিনা। Tolerance সীমায় ও সীমার বাইরে দুই মান |
| Deliverable | তুলনা report, discrepancy log, সমাধানের সারাংশ, exit সই |
| Sub-tasks | 1) Legacy ফাইল ছাঁচ ও mapping। 2) দৈনিক তুলনা। 3) Period-end তুলনা (trial balance, aging, bank, tax, depreciation)। 4) Discrepancy workflow। 5) Exit criteria পরীক্ষা |

### P17-09: Scale ও performance test

| Field | Specification |
| --- | --- |
| ধরন | `test` + `ops` |
| নির্ভর করে | P17-05, P17-07, সব P16 partitioning/archive card |
| অবরুদ্ধ করে | P17-15 |
| OD | OD-105b, OD-119 |
| উদ্দেশ্য | নির্ধারিত volume-এ সিস্টেম লক্ষ্য মেলায় কিনা প্রমাণ করা, আর কোথায় ভাঙে জানা |
| Schema | নতুন table নেই |
| DB flow | আলাদা scale পরিবেশে সিনথেটিক data দিয়ে volume বানানো (API দিয়ে বা Phase 16 import দিয়ে)। পড়া ও মাপা: slow query log, `EXPLAIN`, lock wait, deadlock, queue দৈর্ঘ্য। Lock ক্রম (journal, approval, account, period, voucher counter) বজায় থাকে কিনা দেখা। ব্যর্থ হলে সংশোধন সম্পর্কিত phase-এর card-এ (expand-only migration দিয়ে index), এই card-এ নয় |
| নিয়ম | R17.1, R17.16 (sev1 বা sev2 performance defect gate আটকায়) |
| Contract | PF-1 সমান্তরাল posting, PF-2 report, PF-3 bulk import, PF-4 depreciation ও revaluation, PF-5 consolidation, PF-6 event intake burst, PF-7 8 ঘণ্টা soak (মিশ্র load), PF-8 period close চলাকালীন posting (lock contention) |
| Permission | `accounting.release.manage` |
| Error | কোনোটি নতুন নয়। Timeout হলে বিদ্যমান error |
| Audit | `accounting.release.scale_test_done` |
| Acceptance | AC17.10: প্রতিটা PF-এ p95 লক্ষ্যের ভেতরে, deadlock শূন্য বা স্বয়ংক্রিয় retry-তে সমাধান, আর PF-7-এ সব RC সবুজ |
| Test | PF-1 থেকে PF-8। প্রতিটার পর ফল সংখ্যায় লেখা |
| Deliverable | Benchmark প্রতিবেদন, bottleneck তালিকা, capacity পরিকল্পনা (কত বাড়লে কী বাড়াতে হবে) |
| Sub-tasks | 1) Volume বানানোর tool। 2) PF-1, PF-2। 3) PF-3, PF-4, PF-5। 4) PF-6, PF-8। 5) PF-7 soak। 6) Bottleneck ও পুনরায় মাপা |

### P17-10: Failover ও DR drill

| Field | Specification |
| --- | --- |
| ধরন | `ops` + `test` |
| নির্ভর করে | P17-09, সব P16 DR card, P5-04 |
| অবরুদ্ধ করে | P17-15, P17-19 |
| OD | OD-114, OD-115 |
| উদ্দেশ্য | RPO ও RTO সত্যিকারে মেলে কিনা প্রমাণ করা, আর drill-এর পর ledger সঠিক কিনা দেখা |
| Schema | নতুন table নেই |
| DB flow | Drill-এর আগে স্থির অবস্থায় সব `acc_journals` গণনা ও posted hash নেওয়া। লোড চলাকালীন primary বন্ধ। Failover অথবা point-in-time restore। পরে গণনায় কত গেছে (RPO মাপ) ও কতক্ষণ লাগল (RTO মাপ)। `acc_finance_event_inbox` ও outbox কোনো event হারায় নি ও দুর্বার journal বানায় নি (inbox identity)। RC1 থেকে RC18 ও audit chain যাচাই (Phase 16)। ব্যর্থ হলে কারণ লিখে আবার drill |
| নিয়ম | R17.18 |
| Contract | DR-1 DB primary বন্ধ, DR-2 application node বন্ধ, DR-3 backup থেকে point-in-time restore, DR-4 queue ও inbox পুনরুদ্ধার, DR-5 hosting এলাকা হারানো (OD-115 থাকলে), DR-6 drill-এর পর RC ও audit chain |
| Permission | `accounting.release.manage`, ops role |
| Error | কোনোটি নতুন নয় |
| Audit | `accounting.release.dr_drill_done` |
| Acceptance | AC17.11: RPO ও RTO লক্ষ্যের ভেতরে (OD-114 নিশ্চিত না হলে প্রস্তাব 15 মিনিট ও 4 ঘণ্টা), কোনো event হারায় নি, কোনো দুর্বার journal নেই, RC সবুজ |
| Test | DR-1 থেকে DR-6, সময় ঘড়ি ধরে, মানুষ runbook অনুসরণ করে (স্বয়ংক্রিয় script নয়) |
| Deliverable | Drill প্রতিবেদন (সময়রেখা, মাপা RPO ও RTO), সংশোধিত runbook, পরের drill-এর তারিখ |
| Sub-tasks | 1) Drill পরিকল্পনা ও অনুমতি। 2) DR-1, DR-2। 3) DR-3। 4) DR-4। 5) DR-5 (যদি প্রযোজ্য)। 6) DR-6 ও প্রতিবেদন |

**P17-11 থেকে P17-20, Registry যোগ, Build ক্রমের সারি:** block সীমার কারণে আলাদা tab-এ: Phase 17 task cards P17-11 থেকে P17-20

## ২৩. Enterprise Registry

Phase 6-17-এর table, permission, error code, setting, audit, queue ও RC check: ২৩. Enterprise Registry

## ২৪. Enterprise Master Build Sequence

Phase 6-17-এর একটি build ক্রম, dependency ও wave gate: ২৪. Enterprise Master Build Sequence

## ২৫. WBS Mapping ও Corrections

WBS mapping, আগের card-এ পরিবর্তন ও known inconsistencies: ২৫. WBS Mapping ও Corrections
