# Accounting Module — All Phase Tasks (Combined)

> সব phase-এর task এক জায়গায়। ভাষা চলিত বাংলায়।
> **৩ Dev distribution:** দেখো `ACC-3Dev-Task-Distribution.md` — প্রতিটা task-এ `Owner` + `Estimate Hours` আছে।
> ক্রম: 0 → 1 → 2 → 3 → 4 → 5 → 6 → 8 → 17।
> Schema bundle: `ACC-SCHEMA-01-MVP-All-Schemas.md` (Dev A)।

## Index

| Phase | File | Tasks | Focus |
| --- | --- | --- | --- |
| 0 | ACC-Phase-0-Tasks.md | 5 | Governance ও Discovery |
| 1 | ACC-Phase-1-Tasks.md | 14 | Foundation ও Configuration |
| 2 | ACC-Phase-2-Tasks.md | 18 | Journal Engine ও Approvals |
| 3 | ACC-Phase-3-Tasks.md | 15 | Ledger, Period ও Reports |
| 4 | ACC-Phase-4-Tasks.md | 13 | Finance Event Intake ও Asset Pilot |
| 5 | ACC-Phase-5-Tasks.md | 8 | UAT ও Controlled Release |
| 6 | ACC-Phase-6-Tasks.md | 15 | Journal Management ও Dimensions |
| 8 | ACC-Phase-8-Tasks.md | 5 | Tax Management (partial) |
| 17 | ACC-Phase-17-Tasks.md | 10 | Enterprise UAT ও Full Release |

**মোট: ~১০৩টা task (+ SCHEMA-01)।**

### Dev lanes (সংক্ষেপ)

| Dev | Lane | Hours (approx) |
| --- | --- | --- |
| **A** | Core / Schema / Posting / Ledger | ~153 |
| **B** | Workflow / Approval / Rules | ~99 |
| **C** | UI / Reports / Ops / UAT | ~87 |
| Shared | P0-01, P4-01 | ~6 |

---


<!-- ===== BEGIN: ACC-Phase-0-Tasks.md ===== -->

# Accounting Module — Phase 0 Tasks (Governance ও Discovery)

> এখানকার প্রতিটা task আলাদা করে একজন কাজ করতে পারবে।
> Phase 0-এ কোনো product code নেই। মানে journal বা report বানানোর কিছু নেই। কাজ হলো সবাই একমত হওয়া, জিনিসপত্র নিশ্চিত করা, আর design প্রমাণ করা। কিন্তু পরের সব কাজ এগুলোর ওপর দাঁড়ায়, তাই আলাদা task হিসেবে রাখা।
> Estimate, Owner, Sprint ইচ্ছে করে খালি রাখা হয়েছে — Discovery শেষ হলে ভর্তি হবে।

**Phase 0 কে কার ওপর ঝুলে আছে:**

```
P0-01 ──┬─► P0-02 ──► P0-05 ──► (P1-09, P1-10)
        ├─► P0-03 ──► P1-01
        └─► P0-04 ──► P1-03
```

---

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

Project
ERPFlow
Module
Accounting Module
Type
Governance (`gov`)
Title
Blueprint ও ADR অনুমোদন
Branch Name
কোনো branch নেই (নথি-ভিত্তিক কাজ)
View Order
1
Description
**Summary**
আগে সবাই লিখিতভাবে একমত হয়ে নাও: Blueprint, ADR (A1 থেকে A10), Phase ক্রম, আর Open Decision register। না হলে পরে Accounting module এদিক-ওদিক বানানো শুরু হবে।

**Task serial**
`ACC-P0-01 — Blueprint ও ADR অনুমোদন` · Governance

**Depends on**
- Cursor-এর Phase 0 audit report (শেষ হতে হবে)
- Accounting Module Blueprint
- Task Specification Pack (এই doc)

**Relation: আগের task**
এটাই প্রথম কাজ। এর আগে শুধু Cursor-এর read-only audit report লাগে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `ACC-P0-02` (Repo convention চেক): এই অনুমোদন ছাড়া শুরু করা যাবে না।
- `ACC-P0-03` (Branch ও release নিয়ম) আর `ACC-P0-04` (Client decision list): দুটোই এটার ওপর।
- পুরো Phase 1: সই না হলে Phase 1-এর কোনো কাজ শুরু করা যাবে না।

**Related tables / schema**
লাগে না। কোনো DB কাজ নেই।

**Business rules**
- Architecture decision A1 থেকে A10 কাগজে লিখে মানতে হবে।
- Audit যে তিনটা ফাঁক ধরেছে, সেগুলো নথিতে "স্বীকৃত" লিখে রাখতে হবে: (1) ApprovalEngine-এ amount threshold নেই, (2) posted journal DB-level-এ অপরিবর্তনীয় নয়, (3) company-তে currency ও fiscal year field নেই।
- প্রতিটা Open Decision (OD)-র একজন করে owner থাকতে হবে।

**Task scope**
1. Tech lead, Product owner আর Finance contact মিলে Blueprint-এর section ১ থেকে ১২ পড়ে যাওয়া।
2. ADR (A1 থেকে A10) পড়ে হ্যাঁ বলা, না হলে আপত্তি লেখা।
3. Audit-এর ফাঁকগুলো "স্বীকৃত" হিসেবে নথিতে তোলা।
4. প্রতিটা OD-র জন্য কে দায়িত্ব নেবে সেটা ঠিক করা।
5. পর্যালোচনায় বদল লাগলে Blueprint আপডেট করা।
6. অনুমোদন নথিতে সই নেওয়া।

**Out of scope**
Code, migration, client-এর কাছে কিছু পাঠানো (সেটা `ACC-P0-04`)।

**Deliverable**
- সই করা অনুমোদন নথি
- সংশোধিত Blueprint (যদি দরকার হয়)

**Definition of done**
- লিখিত অনুমোদন আছে।
- প্রতিটা OD-র একজন owner আছে।
- Audit-এর তিন ফাঁক নথিতে "স্বীকৃত" লেখা আছে।
- পরের task (`P0-02`, `P0-03`, `P0-04`) শুরু করার রাস্তা খুলে গেছে।

**Architecture constraints**
- Blueprint-এর বাইরে কোনো architecture সিদ্ধান্ত মুখে মুখে নেওয়া যাবে না — সব লিখিত।

Owner
All (TL+PO+Finance)
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Governance (`gov`), read-only investigation
Title
Repo-র convention নিশ্চিত করা (spike)
Branch Name
কোনো branch নেই (read-only, কোনো code বদলাবে না)
View Order
2
Description
**Summary**
Pack-এ যেখানে "অনুমান" লেখা, সেগুলো আসল কোড খুলে মিলিয়ে দেখো। না হলে Phase 1-এ ভুল ধরে নিয়ে কাজ শুরু হবে, পরে ঘুরে আসতে হবে।

**Task serial**
`ACC-P0-02 — Repo-র convention নিশ্চিত করা` · Spike

**Depends on**
- `ACC-P0-01`

**Relation: আগের task**
`ACC-P0-01` বলে দিয়েছে Blueprint ও Pack ঠিক পথে। এখন সেই Pack-এর অনুমানগুলো কোডে মিলিয়ে দেখতে হবে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `ACC-P0-05` (DB guard spike): trigger বানানো যায় কি না — এই task-এর ১২ নম্বর উত্তরের ওপর।
- `ACC-P1-01` (Module scaffold) আর Phase 1-এর সব schema task: PK ধরন (bigint না ULID) এখান থেকে আসবে।
- প্রায় সব Phase 1 task কোনো না কোনো উত্তর ব্যবহার করবে (নিচের তালিকা দেখো)।

**Related tables / schema**
নতুন কিছু বানাতে হবে না, শুধু পড়া। যেগুলোর নাম ও key নিশ্চিত করতে হবে: `companies`, `users`, `settings`, `activity_logs`, RBAC table।

**যা নিশ্চিত করতে হবে (১২টা প্রশ্ন, প্রতিটার উত্তর file path ও line সহ)**

| # | প্রশ্ন | কোন task এটা ব্যবহার করবে |
| --- | --- | --- |
| 1 | বিদ্যমান table-এর primary key ধরন: bigint unsigned নাকি ULID? | সব schema task (সব `*_id` column) |
| 2 | `companies` ও `users` table-এর নাম ও key | সব FK |
| 3 | `TenantContext` queue আর console job-এ কীভাবে বসে? | `ACC-P1-12` (base job) |
| 4 | `Setting` model-এর API (group, key, value, cache) | `ACC-P1-01` (flag), `ACC-P1-03` |
| 5 | `ActivityLogService`-এর method signature | `ACC-P1-12` (audit) |
| 6 | `ApprovalEngine`-এর contract ও executor registry | Phase 2 approval (OD-6) |
| 7 | permission কীভাবে `config/actions.php`-এ যোগ হয় ও role-এ বসে | `ACC-P1-02` |
| 8 | module scaffold কীভাবে হয় (কোন command, কোন module নকল করা হয়) | `ACC-P1-01` |
| 9 | queue driver ও scheduler কেমন | `ACC-P1-08` (job), Phase 3+ |
| 10 | file storage ও attachment এখন কেমন | `ACC-P1-09`, `P2-02` |
| 11 | test framework ও factory-র নিয়ম | `ACC-P1-13` |
| 12 | DB user trigger বানাতে পারে কি না (binary log থাকলে `log_bin_trust_function_creators`) | `ACC-P0-05`, `ACC-P1-10` |

**Business rules**
- যা নিশ্চিত না, সেটা স্পষ্ট করে "অনিশ্চিত" লিখো। অনুমানকে নিশ্চিত বলে চালিয়ে দিও না।
- কোনো code, migration বা config বদলানো যাবে না।
- PK ধরন বদলালে Pack-এর সব `*_id` column-এর ধরন বদলায় — আর কিছু নয়।

**Task scope**
১২টা প্রশ্নের প্রতিটার জন্য repo-তে খুঁজে file ও line সহ উত্তর লেখো। এক পাতার "Convention Sheet" বানাও।

**Out of scope**
কোনো code লেখা, কোনো migration লেখা।

**Deliverable**
"Convention Sheet": এক পাতা, প্রতিটা প্রশ্নের উত্তর file path ও line সহ।

**Definition of done**
- ১২টা প্রশ্নের সবকটার উত্তর আছে।
- অনিশ্চিতগুলোতে স্পষ্ট "অনিশ্চিত" লেখা।
- Convention Sheet tech lead দেখেছে।
- PK ধরন (প্রশ্ন ১) সবার জানা আছে।

**Architecture constraints**
- Read-only। Repo-তে কোনো commit নেই।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Governance (`gov`)
Title
Feature branch ও release নিয়ম
Branch Name
`feature/accounting-governance-release-rules` (শুধু CI check ও PR template-এর জন্য)
View Order
3
Description
**Summary**
ERPFlow-এ `development`-এ push মানেই production deploy। তাই অসম্পূর্ণ Accounting কাজ যেন সেখানে না যায় — সেই নিয়ম আগে বেঁধে নাও।

**Task serial**
`ACC-P0-03 — Feature branch ও release নিয়ম` · Governance

**Depends on**
- `ACC-P0-01`

**Relation: আগের task**
`ACC-P0-01` অনুমোদন দিয়েছে। এখন সেই architecture নিরাপদে merge করার নিয়ম বানাতে হবে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `ACC-P1-01` (Module scaffold): প্রথম code merge হওয়ার আগে এই নিয়ম আর CI check থাকতে হবে।
- `ACC-P1-14`, `P2-17`, `P4-13` (merge gate): এই task-এর PR template আর CI check ব্যবহার করবে।

**Related tables / schema**
লাগে না।

**Business rules (পরের সব task এগুলো মানতে বাধ্য)**
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` নয়।

**Task scope**
1. Branching ও merge নিয়মের এক পাতা লেখো।
2. PR template বানাও (flag-বন্ধ চেকবক্স, migration সামঞ্জস্য প্রমাণ, অন্য module অপরিবর্তিত প্রমাণ)।
3. CI check-এর খসড়া: Accounting-এর flag Default `true` হলে build ব্যর্থ হবে।
4. ইচ্ছে করে একটা ভুল PR দিয়ে CI check পরীক্ষা করো।

**Out of scope**
Accounting-এর কোনো feature code।

**Deliverable**
Branching ও merge নিয়মের এক পাতা, PR template, CI check-এর খসড়া।

**Definition of done**
- নিয়ম দলের সবাই মেনেছে (লিখিত স্বীকৃতি বা review-এ সম্মতি)।
- CI check ইচ্ছে করে ভুল PR-এ ব্যর্থ হয় (প্রমাণ সংযুক্ত)।
- PR template repo-তে আছে।

**Architecture constraints**
- Accounting-এর কোনো flag-এর Default `true` হওয়া যাবে না।
- বিদ্যমান ERPFlow CI job (বিশেষ করে `backend-mysql-concurrency`) ভাঙা যাবে না।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Governance (`gov`)
Title
Client-এর Setup Checklist ও Decision list পাঠানো
Branch Name
কোনো branch নেই (নথি-ভিত্তিক কাজ)
View Order
4
Description
**Summary**
Client-কে স্পষ্ট করে বলো — কোন সিদ্ধান্ত কখন নিতে হবে। না হলে সিদ্ধান্তের অভাবে dev কাজ আটকে যাবে।

**Task serial**
`ACC-P0-04 — Client-এর Setup Checklist ও Decision list পাঠানো` · Governance

**Depends on**
- `ACC-P0-01`

**Relation: আগের task**
`ACC-P0-01` OD register-এর owner ঠিক করেছে। এখন সেই OD-গুলো client-এর ভাষায় ছোট করে পাঠাতে হবে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `ACC-P1-03` (Settings store): setting-এর কাঠামো আটকায় না, কিন্তু OD-3 (currency), OD-4 (fiscal year) ছাড়া setting-এর **মান** বসানো যাবে না।
- `ACC-P4-01` (Asset pilot change request): এই task-এর আলাদা পাতায় OD-18-এর খসড়া থাকে।

**Related tables / schema**
লাগে না। Source: Pack-এর Section ২.৪ (Settings registry) আর Blueprint section ১১ (OD register)।

**Business rules**
- Client-কে প্রতিটা সিদ্ধান্ত নাম ধরে বলতে হবে: কোন সিদ্ধান্ত, কে নেবে, কোন Phase শুরুর আগে লাগবে।
- Asset pilot-এর change request (OD-18) আলাদা পাতায় রাখো — এটা মূল scope-এর বাইরের জিনিস।

**Task scope**
1. Settings registry আর OD register থেকে client-এর জন্য ছোট নথি বানাও।
2. প্রতিটা সিদ্ধান্তের পাশে লেখো: কে নেবে, কোন Phase-এর আগে।
3. OD-18-এর change request খসড়া আলাদা পাতায় রাখো।
4. Client-কে পাঠাও আর প্রাপ্তি নিশ্চিত করো।
5. প্রতিটা OD-র নাম ধরে একজন owner লিখে নাও।

**Out of scope**
Setting-এর মান নিজে বসানো। সিদ্ধান্ত নিজে নেওয়া।

**Deliverable**
Client-কে পাঠানো নথি, OD owner-এর তালিকা।

**Definition of done**
- Client নথি পেয়েছে (প্রমাণ সহ)।
- প্রতিটা OD-র নাম ধরে একজন owner নির্ধারিত।

**Architecture constraints**
লাগে না (নথি-কাজ)।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Governance (`gov`), design spike (prototype merge হবে না)
Title
DB guard ও ledger-উৎস design spike
Branch Name
`spike/accounting-db-guard-prototype` (কখনো `development`-এ merge হবে না)
View Order
5
Description
**Summary**
Posted journal DB স্তরে কীভাবে আটকে থাকবে, আর ledger কোথা থেকে পড়া হবে — এটা প্রমাণসহ ঠিক করো। না হলে Phase 1-এর schema ভুল ভিত্তিতে বানানো শুরু হবে।

**Task serial**
`ACC-P0-05 — DB guard ও ledger-উৎস design spike` · Spike

**Depends on**
- `ACC-P0-02` (বিশেষ করে ১২ নম্বর উত্তর: DB user trigger বানাতে পারে কি না)

**OD**
OD-13 (DB guard), OD-14 (ledger-উৎস)

**Relation: আগের task**
`ACC-P0-02`-এর Convention Sheet বলে দেবে CI-র MySQL আর deployment-এ trigger বানানোর অনুমতি আছে কি না। সেই উত্তর ছাড়া এই spike অর্থহীন।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `ACC-P1-09` (Journal table): `acc_journals`-এর generated column আর constraint এই spike-এর সিদ্ধান্ত অনুযায়ী।
- `ACC-P1-10` (DB guard বাস্তবায়ন): এই spike-এর প্রমাণিত trigger নকশা হুবহু বাস্তবায়ন হবে।
- `ACC-P3-01` (Ledger query): OD-14-এর ফল (posted line নিজেই ledger নাকি আলাদা projection) এখান থেকে।

**Related tables / schema**
Prototype-এর জন্য `acc_journals`, `acc_journal_lines` (শুধু prototype, production migration নয়)।

**OD-13-এর বিকল্প**
- (ক) `BEFORE UPDATE` ও `BEFORE DELETE` trigger যা `acc_journals.status = 'posted'` থাকলে `SIGNAL` দিয়ে ব্যর্থ করে; `acc_journal_lines`-এ parent-এর status দেখে একই কাজ। **(Pack-এ ধরে নেওয়া)**
- (খ) Application-এর DB user-এর অনুমতি কমানো।
- (গ) দুটোই।

**OD-14**
Posted `acc_journal_lines` সরাসরি ledger (Option A, ধরে নেওয়া) নাকি আলাদা projection (Option B)।

**Business rules (prototype-এ যা প্রমাণ করতে হবে)**
- Posted row-এ সরাসরি `UPDATE` ও `DELETE` ব্যর্থ হয়।
- `INSERT` আর Draft-এ `UPDATE` চলে।
- Post-এর transaction-এর ভেতরে অবস্থা বদল চলে: `approved` থেকে `posted` ঠিক আছে, `posted` থেকে অন্য কিছু নিষিদ্ধ।
- `reversed` চিহ্ন বসানো চলে, কিন্তু শুধু নির্দিষ্ট column বদলে (`status`, `reversed_at`, `reversed_by_journal_id`, `updated_at`, `updated_by`, `version`)।

**Task scope**
1. CI-র MySQL-এ trigger-এর prototype বানিয়ে উপরের চারটা আচরণ test করো।
2. বড় data-য় (১০ লক্ষ line) GL query-র গতি মেপে Option A আর Option B তুলনা করো।
3. Trigger-এর `down()` নিরাপদ কি না লেখো।
4. Design note লেখো: কোন উপায় বেছে নেওয়া হলো আর কেন।

**Out of scope**
Production migration (সেটা `ACC-P1-09`, `ACC-P1-10`)। Prototype merge করা।

**Deliverable**
Design note (কী বেছে নিলাম, কেন), prototype migration ও test, trigger `down()`-এর নিরাপত্তা বিশ্লেষণ।

**Definition of done**
- Prototype CI-র MySQL-এ চলে।
- `JRN_IMMUTABLE` আচরণ প্রমাণিত।
- OD-13 ও OD-14 লিখিতভাবে বন্ধ (tech lead ও client জানে; বিশেষ করে trigger না বসানো গেলে বিকল্প দুর্বল — সেটাও স্পষ্ট)।
- Prototype branch `development`-এ merge হয়নি।

**Architecture constraints**
- Prototype production code-এ যাবে না।
- Trigger বানানো না গেলে বিকল্প: application guard + `posted_hash` দিয়ে রাতের integrity check। এটা দুর্বল, তাই OD-13-এ client ও tech lead-কে স্পষ্ট জানাতে হবে।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

<!-- ===== END: ACC-Phase-0-Tasks.md ===== -->


<!-- ===== BEGIN: ACC-Phase-1-Tasks.md ===== -->

# Accounting Module — Phase 1 Tasks (Foundation ও Configuration)

> এখানকার প্রতিটা task আলাদা করে একজন কাজ করতে পারবে।
> Phase 1 শেষ মানে একটা company accounting চালু করার মতো **তৈরি** করা যায় — কিন্তু এখনও কোনো লেনদেন post হবে না।
> কাজের ক্রম: module → permission → settings → checklist → CoA → period → journal ভিত্তি → DB guard → opening balance → tenancy/audit → test।
> Estimate, Owner, Sprint ইচ্ছা করে ফাঁকা — পরে ভর্তি হবে।

## Phase 1-এ সব task-এ মেনে চলতে হবে (প্রতিটা task-এর DoD-তেও এগুলো ধরে নেওয়া হয়)

- **Database:** MySQL, InnoDB, `utf8mb4`। Table prefix `acc_`।
- **Primary key:** `ACC-P0-02`-এর Convention Sheet অনুযায়ী (অনুমান: bigint unsigned বা ULID)। সব `*_id` FK একই ধরন।
- **Tenant:** প্রতিটা business table-এ `company_id NOT NULL`, `companies(id)`-এ FK; composite index-এ `company_id` সবার আগে।
- **টাকা:** `DECIMAL(20,4)`। Enum হিসেবে `VARCHAR(32)` + `CHECK` — MySQL-এর built-in enum নয়।
- **Optimistic lock:** বদলানো যায় এমন row-এ `version INT UNSIGNED NOT NULL DEFAULT 1`; update-এ `WHERE version = ?` আর `version = version + 1`।
- **লেজার:** Posted বা ledger row কখনো মোছা হয় না। ডিফল্ট `ON DELETE RESTRICT`; ledger table-এ `CASCADE` নয়।
- **Migration:** table প্রতি আলাদা migration; একবার apply হওয়া migration এডিট নয়; পিছনে সামঞ্জস্যপূর্ণ (add → alter → drop); `down()` নিরাপদ।
- **Composite FK:** `account_id` বা `journal_id` যেখানে আছে, সেখানে `(company_id, account_id)` / `(company_id, journal_id)` — একা `account_id`-র FK নয় (অন্য company-র data জোড়া লাগানো DB-তেই আটকাতে)।
- **সাধারণ DoD (প্রতিটা task-এ):** Service–Repository (interface সহ), Form Request, API Resource; `company_id` tenant scope + IDOR test; Feature test (happy path, validation, unauthorized/forbidden); audit `AccountingAudit` দিয়ে; error `AccountingError` enum code দিয়ে; flag বন্ধ রেখে merge নিরাপদ; বিদ্যমান ERPFlow regression সবুজ; Project/Payroll/Asset/Commissioning code untouched।

**Phase 1 task-গুলো কার ওপর ঝুলে (নির্ভরতা):**

```
P1-01 ─┬─► P1-12 ──────────────────────────────┐
       ├─► P1-02 ──┐                            │
       └─► P1-03 ─┬─► P1-04 ─► P1-05           │
                  ├─► P1-06 ─► P1-07 ─┐         │
                  └─► P1-08 ──┐       │         │
        P1-06+P1-08+P0-05 ─► P1-09 ─► P1-10 ─► P1-11 (P1-07,P1-12ও লাগে)
        সব ─► P1-13 ─► P1-14 ─► Phase 2
```

---

## ACC-P1-01 — Module scaffold ও flag

Project
ERPFlow
Module
Accounting Module
Type
Feature (service)
Title
Module scaffold ও flag — Backend ও Frontend
Branch Name
`feature/accounting-module-scaffold`
View Order
6
Description
**Summary**
এই কাজটায় `Accounting` module-এর খালি কাঠামো বানাতে হবে — flag বন্ধ থাকলে কিছুই চালু হবে না। বাকি Phase 1 task-এর নিরাপদ ভিত্তি এটাই।

**Task serial**
`ACC-P1-01 — Module scaffold ও flag` · Feature

**Depends on**
- `ACC-P0-02` (module scaffold পদ্ধতি, `Setting` API নিশ্চিত)
- `ACC-P0-03` (branch ও release নিয়ম, CI check)

**Relation: আগের task**
`P0-02` বলে দিয়েছে কোন বিদ্যমান module নকল করে scaffold বানাতে হয় আর `Setting` model কীভাবে কাজ করে। `P0-03` merge নিয়ম ঠিক করেছে। দুটো না থাকলে এখানে আন্দাজে কাজ শুরু হতো, পরে ঘুরে আসতে হতো।

**Relation: পরের task (কে অপেক্ষা করছে)**
**সব Phase 1 task** এটার ওপর। বিশেষ করে `P1-02` (permission), `P1-03` (settings), `P1-12` (tenancy, audit, error)। Flag ছাড়া কোনো accounting route চালু করা যাবে না।

**Related tables / schema**
কোনো নতুন table নেই। শুধু বিদ্যমান `Setting` table পড়া:
`SELECT value FROM settings WHERE company_id = ? AND group = 'accounting' AND key = 'enabled'`

**Business rules**
- Flag `accounting.enabled` company ধরে, `Setting` model-এ (group `accounting`, key `enabled`)।
- তিনটা flag (`accounting.enabled`, `event_intake.enabled`, `event_intake.sources.*`) `acc_settings`-এ নয়, `Setting`-এ থাকে, যাতে `acc_settings` table তৈরির আগেই flag কাজ করে।
- **Default সবসময় `false`।**
- Flag বন্ধ হলে middleware `ACC_MODULE_DISABLED` (403) দেয়।
- Module চালু থাকলে migration চলে, কিন্তু সব ব্যবহার flag-এ আটকানো।

**Task scope**
1. `Modules/Accounting` তৈরি (Convention Sheet-এর পদ্ধতিতে): service provider, routes, config, migrations ফোল্ডার।
2. `modules_statuses.json`-এ `Accounting` যোগ।
3. `AccountingFeatureService` (`TimeBillingFeatureService`-এর ধাঁচে): `enabled(companyId)`, `intakeEnabled(companyId)`, `sourceEnabled(companyId, source)`। সব default `false`।
4. Middleware `accounting.enabled`।
5. Frontend: `registerModules.ts`-এ module নিবন্ধন; navigation-এ আইটেম যা flag বন্ধ থাকলে দেখা যায় না।

**Out of scope**
Flag চালু করার UI/API (`P5-06`, `P4-11`) — এখানে শুধু infrastructure। Setup Checklist (`P1-05`) আলাদা task।

**Contract**
`AccountingFeatureService::enabled(companyId): bool`, `::intakeEnabled(companyId)`, `::sourceEnabled(companyId, source)`।

**Error**
`ACC_MODULE_DISABLED` (403)

**Audit**
`accounting.flag.enabled`, `accounting.flag.disabled` (flag বদলানোর card `P5-06` আর `P4-11`-এ লেখা হবে, এই task শুধু event নাম নিবন্ধন করে)

**Acceptance**
AC1.10

**Test**
Flag বন্ধ থাকলে কোনো route সাড়া দেয় না; navigation-এ আইটেম নেই; বিদ্যমান module-এর কোনো test ভাঙে না।

**Deliverable**
খালি কিন্তু বুট হওয়া module, flag service, middleware।

**Sub-tasks**
module কাঠামো, status, flag service, middleware, frontend নিবন্ধন, test

**Definition of done**
- Phase 1-এর সাধারণ DoD (উপরের তালিকা) পূরণ।
- Flag বন্ধ রেখে `development`-এ merge নিরাপদ; PR-এ "flag Default বন্ধ প্রমাণিত" টিক।
- বিদ্যমান ERPFlow test একটাও ভাঙেনি।

**Architecture constraints**
- Flag default কখনো `true` নয় — merge gate-এ প্রমাণ লাগবে।
- Project/Payroll/Asset/Commissioning-এর code ছুঁয়ে ফেলা যাবে না।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Feature (service, cross-cutting)
Title
Tenancy, audit আর error-এর সাধারণ ব্যবস্থা
Branch Name
`feature/accounting-tenancy-audit-error`
View Order
7
Description
**Summary**
এই কাজটায় company context, audit লেখা আর error response একবারেই গুছিয়ে রাখতে হবে — যাতে বাকি task-গুলো একই জিনিস ব্যবহার করে, কেউ আলাদা নিজের মতো বানাবে না।

**Task serial**
`ACC-P1-12 — Tenancy, audit আর error-এর সাধারণ ব্যবস্থা` · Feature

**Depends on**
- `ACC-P1-01`
- `ACC-P0-02` (প্রশ্ন ৩ `TenantContext`, ৫ `ActivityLogService`)

**Relation: আগের task**
`P1-01` module আর flag দিয়েছে। `P0-02` Convention Sheet-এ `TenantContext` job-এ কীভাবে বসে আর `ActivityLogService`-এর signature কী — সেটা এখানে ব্যবহার।

**Relation: পরের task (কে অপেক্ষা করছে)**
ক্রমে `P1-01`-এর ঠিক পরে রাখা হয়েছে — **error, audit বা job যেখানে আছে প্রায় সব task** এটা ব্যবহার করে (`P1-03`, `P1-08`, `P1-11`, `P2-*`, `P3-*`, `P4-*`)। আগে না থাকলে প্রতিটা task নিজের মতো error/audit বানিয়ে ফেলত।

**Related tables / schema**
কোনো নতুন table নেই। বিদ্যমান `activity_logs`-এ লেখা।

**Business rules**
- `companyId` ছাড়া job ডাকা যাবে না (exception, job আরম্ভই হয় না)।
- Job শেষে (`finally`) `TenantContext` পরিষ্কার, যাতে পরের job আগের company দেখে না।
- ব্যর্থ বা অস্বীকৃত চেষ্টাও audit-এ লেখা হয়, শুধু সফল কাজ নয়।
- অপ্রত্যাশিত exception 500 আর `correlation_id`, ভেতরের তথ্য বাইরে যাবে না।

**Task scope**
1. **Company context:** `RunsInCompanyContext` trait বা base class `AccountingJob`: constructor-এ `companyId` বাধ্যতামূলক, `handle()`-এর শুরুতে `TenantContext` বসানো, `finally`-তে পরিষ্কার। `CompanyScope` console-এ enforce না হলে সেটা test দিয়ে ধরা।
2. **Audit:** `AccountingAudit::log(event, subject, before, after, reason = null)`: সাধারণ field (`company_id`, `actor_id`, `subject_type`, `subject_id`, `before`, `after`, `reason`, `correlation_id`) বসিয়ে `ActivityLogService` ডাকে। Permission বা company লঙ্ঘনে middleware `accounting.guard.denied` লেখে।
3. **Error:** `AccountingException` ও সন্তান class; একটা `AccountingError` enum (code ও HTTP status বাঁধা, কোডে একটাই উৎস)। Handler সব Accounting API-তে `{code, message, field, details, correlation_id}` আকারে উত্তর দেয়।
4. **Correlation:** Middleware `X-Correlation-Id` পড়ে বা বানায়, response-এও ফেরত দেয়।

**Out of scope**
নির্দিষ্ট business error-এর ব্যবহার (প্রতিটা task নিজে enum থেকে নেবে)।

**Error**
`ACC_FORBIDDEN` (403), `ACC_COMPANY_FORBIDDEN` (403), `ACC_MODULE_DISABLED` (403)

**Acceptance**
AC1.8

**Test**
- Job context ছাড়া ব্যর্থ।
- Context পরিষ্কার হয় (পরের job আগের company দেখে না)।
- একটা contract test `AccountingError`-এর প্রতিটা case ঘুরে HTTP status আর response আকার যাচাই করে।
- Audit-এ সব সাধারণ field আছে।

**Deliverable**
Base job, audit service, error enum ও handler, correlation middleware।

**Sub-tasks**
base job, audit service, error enum, handler, correlation, test

**Definition of done**
- Phase 1-এর সাধারণ DoD পূরণ।
- AC1.8 সবুজ।
- Pack-এর Section ২.৩-এর সব error code enum-এ আছে।

**Architecture constraints**
- HTTP status আর error code-এর একমাত্র উৎস `AccountingError` enum — handler/API-তে আলাদা করে লেখা যাবে না।
- পরে নতুন code (যেমন `COA_CODE_INVALID`) শুধু enum-এ যোগ; string ছড়ানো নয়।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P1-02 — Permission slug ও role seed

Project
ERPFlow
Module
Accounting Module
Type
Feature (service)
Title
Permission slug ও role seed
Branch Name
`feature/accounting-permissions-seed`
View Order
8
Description
**Summary**
এই কাজটায় Accounting-এর সব permission নিবন্ধিত থাকবে আর ডিফল্ট role-এ বসানো থাকবে — যাতে কোনো API route permission ছাড়া খোলা না থাকে।

**Task serial**
`ACC-P1-02 — Permission slug ও role seed` · Feature

**Depends on**
- `ACC-P1-01`

**Relation: আগের task**
`P1-01` module আর flag দিয়েছে। Module বুট হওয়ার পর permission middleware যোগ করা যায়।

**Relation: পরের task (কে অপেক্ষা করছে)**
**সব API task** (`P1-04`, `P1-07`, `P1-11`, Phase 2–5 API)। Route permission middleware ছাড়া বানানো যাবে না।

**Related tables / schema**
RBAC-এর বিদ্যমান table-এ row যোগ (নতুন table নয়)। Seeder idempotent: আবার চালালে duplicate হয় না।

**Business rules**
- Permission company ধরে দেওয়া হয়। এক company-তে পাওয়া permission অন্য company-তে কাজ করে না (বিদ্যমান RBAC এটা কীভাবে করে তা Convention Sheet-এ)।
- ডিফল্ট role: Accountant, Finance Manager, Head of Finance, Auditor, Admin। প্রতিটার ডিফল্ট permission Blueprint ৪.৫ ও Phase-ভিত্তিক role table অনুযায়ী।

**Phase 1-এর slug (মোট ২৪টা slug Pack-এর Section ২.৩-এ, Phase অনুযায়ী পরে আরও আসবে)**
`accounting.settings.view/.manage/.approve`, `accounting.enable.manage`, `accounting.coa.view/.manage`, `accounting.journal.view/.create`, `accounting.period.view`, `accounting.audit.view`। বাকিগুলো (journal.approve/post/reverse/override, approval.manage, period.manage/approve, report.*, cashflow.manage, reconcile.run, mapping.manage, intake.*) config-এ একসাথে নিবন্ধিত, কিন্তু ব্যবহার হবে তাদের নিজের Phase-এ।

**Task scope**
1. `config/actions.php`-এ `accounting` module-এর অধীনে সব slug।
2. ডিফল্ট role-এ বসানোর seeder বা migration (Convention Sheet অনুযায়ী)।
3. প্রতিটা API route-এ permission middleware।

**Error**
`ACC_FORBIDDEN`, `ACC_COMPANY_FORBIDDEN`

**Audit**
`accounting.guard.denied`

**Acceptance**
AC1.6

**Test**
প্রতিটা role-slug জোড়ার matrix test; company পার হওয়ার চেষ্টা; seeder দুইবার চালানো।

**Deliverable**
Slug তালিকা, seeder, middleware।

**Definition of done**
- Phase 1-এর সাধারণ DoD পূরণ।
- AC1.6 (permission matrix) সবুজ।
- Seeder দুইবার চালালে কোনো duplicate নেই।

**Architecture constraints**
- বিদ্যমান RBAC ব্যবস্থা নতুন করে বানানো যাবে না, ব্যবহার করতে হবে।
- নতুন approval engine নয়।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P1-03 — Settings store, registry, history ও lock

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service)
Title
Settings store, registry, history ও lock — Backend
Branch Name
`feature/accounting-settings-store-backend`
View Order
9
Description
**Summary**
এই কাজটায় company-ভিত্তিক typed setting, তার ইতিহাস আর post-এর পর lock-এর নিয়ম থাকবে — যাতে নীতি কোডে বাঁধা না থেকে Admin Configuration থেকে বদলানো যায়।

**Task serial**
`ACC-P1-03 — Settings store, registry, history ও lock` · Feature

**Depends on**
- `ACC-P1-01`
- `ACC-P0-04` (OD-3 currency, OD-4 fiscal year: কাঠামো আটকায় না, শুধু **মান** বসাতে লাগে)

**Relation: আগের task**
`P1-01` flag service আর module দিয়েছে। `P0-04` client decision list-এ OD-3 (currency), OD-4 (fiscal year) — কাঠামো আটকায় না, **মান** বসাতে লাগে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P1-04` (Settings UI ও দ্বিতীয় অনুমোদন): `set()`-এর step 5 change request তৈরি করে এই task-এর registry দেখে।
- `P1-05` (Checklist): ধাপ S01-S03, S09 `acc_settings` পড়ে।
- `P1-06` (CoA): code-এর দৈর্ঘ্য ও পরিসর setting-এ।
- `P1-08` (Period): `fiscal_year_start` পড়ে।
- `P2-03` (Validator): সব setting এখান থেকে পড়ে।

**Related tables / 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 | হ্যাঁ |  |

Unique: `uq_acc_settings_company_key (company_id, key)`।

**`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)`।

**Business rules**
- Registry (`SettingRegistry`) কোডে একটাই উৎস: প্রতিটা key-র ধরন, যাচাই, default, lock, sensitive, phase, OD। Pack-এর Section ২.৪ ঠিক যা। UI আর API এটাই পড়ে।
- "বাধ্যতামূলক" default মানে কোনো default নেই; Admin না বসানো পর্যন্ত Setup Checklist সবুজ হয় না, `get()` তখন `null` দেয়।
- Lock-করা key (যেমন `fiscal_year_start`, `base_currency`, `currency_precision`, `accounting_basis`, `system_accounts`, `opening_cutover_date`) কোনো journal post হওয়ার পর বদলানো যায় না (`SET_LOCKED`)।
- Flag তিনটা (`accounting.enabled`, `event_intake.enabled`, `event_intake.sources.*`) এখানে নয়, `P1-01`-এর `Setting` model-এ।

**`set()`-এর DB flow (এক transaction)**
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।** এই task-এ এর stub implementation থাকে যা সবসময় `false` দেয় (কারণ `acc_journals` table এখনো নেই)। **আসল implementation `P1-09`-এ বসে:** `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।

**Task scope**
দুটো migration, registry, `SettingsService` (`get`, `set`, `all`, `history`), probe interface ও stub, API (`GET`, `PUT`, history), test।

**Out of scope**
Settings UI, দ্বিতীয় অনুমোদনের পূর্ণ workflow (`P1-04`), `PostedJournalProbe`-এর আসল implementation (`P1-09`)।

**Contract**
`SettingsService::get(companyId, key)`, `::set(...)`, `::all(companyId)`, `::history(companyId, key)`। API: `GET`, `PUT`, history API।

**Error**
`SET_INVALID` (422), `SET_LOCKED` (409), `JRN_CONFLICT` (409)

**Audit**
`accounting.setting.changed`

**Acceptance**
AC1.4

**Test**
টাইপ ও পরিসরের যাচাই; lock; দুজন একসাথে বদলানো; history যোগ হওয়া; default ফেরত।

**Deliverable**
দুটো migration, registry, service, probe interface ও stub, API, history API।

**Definition of done**
- Phase 1-এর সাধারণ DoD পূরণ।
- AC1.4 সবুজ।
- `acc_settings_history`-এ `UPDATE`/`DELETE` করা যায় না (পরে `P1-10` trigger আটকাবে; এই task-এ application-এ কখনো update/delete নেই)।

**Architecture constraints**
- `SettingRegistry`-ই key-র type/default/lock-এর একমাত্র উৎস — UI/API-তে duplicate definition নয়।
- `PostedJournalProbe` stub এখানে; আসল query `P1-09`-এ (`acc_journals` তৈরি হলে)।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service + ui)
Title
Settings UI ও দ্বিতীয় অনুমোদন (Maker–Checker)
Branch Name
`feature/accounting-settings-ui-approval`
View Order
10
Description
**Summary**
এই কাজটায় settings screen আর সংবেদনশীল setting-এ দ্বিতীয় অনুমোদন (Maker–Checker) থাকবে — `base_currency` বা `accounting_basis`-এর মতো জিনিস একজন একা বদলাতে পারবে না।

**Task serial**
`ACC-P1-04 — Settings UI ও দ্বিতীয় অনুমোদন` · Feature

**Depends on**
- `ACC-P1-03`
- `ACC-P1-02`

**Relation: আগের task**
`P1-03`-এর `set()` step 5 বলে sensitive হলে change request — সেই request এই task বানায়। `P1-02` `accounting.settings.manage` / `.approve` slug দিয়েছে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P1-05` (Checklist UI): এই task-এর settings পাতায় "ঠিক করুন" লিংক যায়।
- `P3-07` (নগদ-সমতুল্য account বদল): `cash_equivalent_accounts` সংবেদনশীল, তাই দ্বিতীয় অনুমোদনের পথে।
- `P4-05` (mapping), `P4-11` (event_intake flag, প্রথমবার), `P5-06` (`accounting.enabled`), `P6-02`/`P6-06`/`P6-03`: সবাই এই task-এর **`ChangeApplier` রেজিস্ট্রি** ব্যবহার করে prefix (`mapping:`, `flag:`, `jtype:`, `jtemplate:`, `series:`) নিবন্ধন করে। তাই `ChangeApplier` এমনভাবে বানাও যাতে পরে prefix যোগ করা যায়।

**Related tables / 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` হলে থাকবে।

**Business rules**
- **অনুরোধকারী নিজে অনুমোদন করতে পারে না (সবসময়, setting বদলে বন্ধ করা যায় না)।**
- একটা key-র জন্য একসাথে একটাই `pending` অনুরোধ; pending থাকা অবস্থায় সরাসরি `set()` সেই key-তে আটকে।
- `settings_require_second_approval` false হলে অনুরোধ তৈরিই হয় না, সরাসরি লেখা।
- অনুমোদনের সময় `acc_settings.version` ≠ `base_version` হলে অনুরোধ বাতিল (`cancelled`, নতুন code `SET_STALE_REQUEST` 409), নতুন করে অনুরোধ করতে হবে।

**DB flow: অনুমোদন (এক transaction)**
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` না হলে অনুরোধ বাতিল (`SET_STALE_REQUEST`)।
7. `SettingsService`-এর ভেতরের লেখার অংশ (দ্বিতীয় অনুমোদন এড়িয়ে) দিয়ে মান লেখা, history-তে `change_request_id` সহ।
8. request `approved`।
9. commit।

**Task scope**
Migration, request service, approve/reject service, API, settings পাতা, history ড্রয়ার, অপেক্ষমাণ tab, test।

**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, `ChangeApplier` রেজিস্ট্রি।

**Definition of done**
- Phase 1-এর সাধারণ DoD পূরণ।
- AC1.4 ও AC1.6 সবুজ।
- `ChangeApplier` prefix-ভিত্তিক নিবন্ধন সমর্থন করে (পরের task-গুলো prefix যোগ করবে)।

**Architecture constraints**
- পুরো ApprovalEngine এখানে নয় — settings-এর জন্য হালকা Maker–Checker। Phase 2 journal approval আলাদা।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P1-05 — Setup Checklist

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service + ui)
Title
Setup Checklist
Branch Name
`feature/accounting-setup-checklist`
View Order
11
Description
**Summary**
এই কাজটায় company accounting চালু করার আগে সব দরকারি জিনিস ঠিক আছে কি না যাচাই হবে — অসম্পূর্ণ setup-এ `accounting.enabled` চালু করা যাবে না।

**Task serial**
`ACC-P1-05 — Setup Checklist` · Feature

**Depends on**
- `ACC-P1-03`
- `ACC-P1-04`

**Relation: আগের task**
`P1-03` setting মান দেয় (checklist S01–S03, S09 এখান থেকে পড়ে)। `P1-04` settings UI দেয় — লাল ধাপে "ঠিক করুন" লিংক ওখানে যায়।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P5-06` (`accounting.enabled` চালু): `canEnable()` এখান থেকে।
- `P2-14` (opening post): post হলে `ChecklistService::evaluate` চালায়, S10 সবুজ হয়।
- কিছু ধাপ (S04, S05) `P1-06` (CoA)-এর পর পূর্ণ কাজ করবে; তার আগে সেই checker `pending` দেবে। S06-S08 Phase 2 চালু না হওয়া পর্যন্ত `not_applicable`।

**Related tables / schema**

**`acc_setup_status`**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | FK | না |  |
| `step_code` | VARCHAR(40) | না | `S01_fiscal_year` ইত্যাদি |
| `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)`।

**Checklist-এর ধাপ**

| ধাপ | কী যাচাই | কখন বাধ্যতামূলক |
| --- | --- | --- |
| `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` |
| `I01_mappings` | প্রয়োজনীয় সব mapping বৈধ | `event_intake.enabled` চালু করতে |
| `I02_currency_policy` | `event_currency_policy` আছে | `event_intake.enabled` চালু করতে |
| `I03_source_flags` | অন্তত একটা source চালু | Asset intake চালু করতে |

**Business rules**
- **`S01` থেকে `S10` সবুজ না হলে `accounting.enabled` চালু করা যায় না (`SET_INCOMPLETE`)।** `I01`-`I03` আলাদা group, শুধু intake চালুর সময়।
- প্রতিটা ধাপ আলাদা checker class, যে **নিজে মূল DB পড়ে** ফল বের করে (সংরক্ষিত অবস্থার ওপর নির্ভর করে না)।
- `canEnable()` true যদি S01-S10-এর সবকটা `done`, `not_applicable` বা `skipped`।
- শুধু S10 `skipped` হতে পারে, Admin কারণ সহ।

**কখন চলে**
যেকোনো setting, CoA বা permission বদলের পর (event listener); checklist পাতা খোলার সময়; `accounting.enabled` চালুর অনুরোধের সময় (সবসময় নতুন করে)।

**Task scope**
Migration, checker class সব ধাপের (১৪টা), `ChecklistService::evaluate`, listener, API, UI, test। ফল একটা transaction-এ upsert।

**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।

**Definition of done**
- Phase 1-এর সাধারণ DoD পূরণ।
- AC1.5 সবুজ।
- যে ধাপের উৎস table এখনো নেই (S04, S05, S10), সেটা ভাঙে না, `pending` দেয়।

**Architecture constraints**
- Checklist step-এর `state` DB-তে cache, কিন্তু `canEnable()` বা enable চেষ্টায় **সবসময়** checker দিয়ে live data থেকে পুনর্মূল্যায়ন — পুরোনো সবুজ "বিশ্বাস" করা যাবে না।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service)
Title
Chart of Accounts schema ও service — Backend
Branch Name
`feature/accounting-coa-backend`
View Order
12
Description
**Summary**
এই কাজটায় প্রতি company-র আলাদা Chart of Accounts (CoA) থাকবে — অন্য company-র account parent বা line-এ জোড়া লাগানো DB composite FK দিয়েই আটকাবে।

**Task serial**
`ACC-P1-06 — Chart of Accounts: schema ও service` · Feature

**Depends on**
- `ACC-P1-03` (code-এর দৈর্ঘ্য ও পরিসর setting-এ; OD-7 আংশিক, কাঠামো আটকায় না)

**Relation: আগের task**
`P1-03` settings store থেকে `coa_code_min_length`, `coa_code_max_length`, `coa_type_ranges`, `coa_inactive_nonzero_policy` পড়ে — account create/update-এ validation এগুলো দিয়ে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P1-07` (CoA API ও UI): এই service-এর ওপর।
- `P1-09` (Journal table): `acc_journal_lines`-এর composite FK `(company_id, account_id)` → এই task-এর `acc_accounts (company_id, id)` unique-কে লক্ষ্য করে। **তাই `uq_acc_accounts_company_id` বাদ দেওয়া যাবে না।**
- `P1-11` (Opening balance): account leaf ও active কি যাচাই।
- **Probe stub:** এই task `PostedLineProbe`-এর stub রাখে (সবসময় `false`); আসল implementation `P1-09`-এ বসে।

**Related tables / 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 FK-র লক্ষ্য)
- `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`, `ck_acc_accounts_cf`
- `idx_acc_accounts_company_parent (company_id, parent_id)`, `idx_acc_accounts_company_type_active (company_id, type, is_active)`

**Business rules (Blueprint ৬.৫, R1.1 থেকে R1.6)**
- Group account-এ line নেই; line আছে এমন account-এ `is_group = true` করা যায় না।
- Parent অবশ্যই `is_group = true` এবং একই type।
- Line আছে এমন account-এর type বদল আটকানো (`COA_IN_USE`)।
- System account মোছা, inactive করা, type বদল সব আটকে (`COA_SYSTEM_PROTECTED`)।
- মোছা শুধু যদি line নেই আর অন্য account-এর parent নয়।
- Parent বদলালে চক্র দেখা (সর্বোচ্চ গভীরতা 10)।
- Inactive করার সময় closing balance শূন্য না হলে `coa_inactive_nonzero_policy` অনুযায়ী `warn` বা `block` (balance হিসাব `P1-09`-এর পরে; stub শূন্য ফেরত)।

**DB flow: তৈরি (এক transaction)**
1. transaction।
2. `parent_id` থাকলে parent row `SELECT ... FOR UPDATE`।
3. যাচাই: parent `is_group = true`; parent-এর type = নতুন account-এর type; code-এর দৈর্ঘ্য ও পরিসর settings অনুযায়ী। ভাঙলে নতুন code `COA_CODE_INVALID` (422)।
4. `INSERT`। Unique ভাঙলে `COA_CODE_DUPLICATE`।
5. Audit।
6. commit।

**Task scope**
Migration, model, `AccountService` (`create/update/deactivate/reactivate/delete`, প্রতিটায় `version` মেলানো), চক্র যাচাই, probe stub, test।

**Out of scope**
API ও UI (`P1-07`)।

**Error**
`COA_CODE_DUPLICATE`, `COA_CODE_INVALID` (নতুন), `COA_IN_USE`, `COA_SYSTEM_PROTECTED`, `JRN_CONFLICT`

**Audit**
`accounting.coa.created/.updated/.deactivated/.reactivated/.deleted`

**Acceptance**
AC1.2

**Test**
একই code দুই company-তে; অন্য company-র parent; type বদল line সহ ও ছাড়া; চক্র; system account; দুজন একসাথে বদলানো।

**Deliverable**
Migration, model, service।

**Definition of done**
- Phase 1-এর সাধারণ DoD পূরণ।
- AC1.2 সবুজ।
- সরাসরি SQL দিয়ে অন্য company-র parent বসাতে গেলে DB FK আটকায় (test প্রমাণ)।

**Architecture constraints**
- Composite FK নিয়ম ভাঙা যাবে না।
- `PostedLineProbe`-এর stub `P1-09`-এ আসল implementation দিয়ে বদলাতে হবে (সেই task-এর দায়িত্ব)।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P1-07 — CoA API ও UI

Project
ERPFlow
Module
Accounting Module
Type
Feature (api + ui)
Title
CoA API ও UI
Branch Name
`feature/accounting-coa-api-ui`
View Order
13
Description
**Summary**
এই কাজটায় accountant বা finance manager CoA গাছের মতো দেখে account তৈরি/সম্পাদনা/নিষ্ক্রিয় করতে পারবে — API আর UI একসাথে।

**Task serial**
`ACC-P1-07 — CoA API ও UI` · Feature

**Depends on**
- `ACC-P1-06`
- `ACC-P1-02`

**Relation: আগের task**
`P1-06` service আর business rule দিয়েছে। এখানে শুধু API ও UI — নতুন rule বানানো যাবে না, server-ই আসল বাধা। `P1-02` permission slug দিয়েছে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P1-11` (Opening balance গ্রিড): account picker এই API থেকে leaf ও active account আনে।
- `P2-02` (journal editor): account picker একই।
- `P1-05`-এর S04, S05: এই task-এর "system account তৈরি" endpoint দিয়ে সবুজ হয়।

**Related tables / schema**
নতুন table নেই। `acc_accounts` (`P1-06`)।

**Business rules**
- Bulk import নেই, বাটনও নেই (OD-19)।
- Type বদলানোর ঘর line থাকলে নিষ্ক্রিয়, কারণ লিখে।
- System account চিহ্নিত ও সুরক্ষিত।

**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 চিহ্ন ও সুরক্ষা।

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

**Definition of done**
- Phase 1-এর সাধারণ DoD পূরণ।
- সব error API-র `field` ধরে ফর্মের জায়গায় দেখায়।

**Architecture constraints**
- CoA UI-তে business rule duplicate নয় — type বদল disable ইত্যাদি server error দেখায়, client শুধু UX।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P1-08 — Accounting period কাঠামো

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service + job)
Title
Accounting period কাঠামো — Backend
Branch Name
`feature/accounting-periods-backend`
View Order
14
Description
**Summary**
এই কাজটায় fiscal year আর মাস অনুযায়ী accounting period তৈরি হবে — পরে Phase 3-এ সেগুলো বন্ধ করা যাবে।

**Task serial**
`ACC-P1-08 — Accounting period কাঠামো` · Feature

**Depends on**
- `ACC-P1-03` (`fiscal_year_start` setting)
- OD-4 (আর্থিক বছরের নামকরণ: শুরুর বছর ধরে, Blueprint-এর ধরে নেওয়া)

**Relation: আগের task**
`P1-03` settings store দিয়েছে। `fiscal_year_start` না থাকলে period generator `SET_INCOMPLETE` দেয় — checklist S01-ও লাল থাকে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P1-09` (Journal table): `acc_journals.period_id` composite FK `(company_id, period_id)` → `acc_periods (company_id, id)`। **তাই `uq_acc_periods_company_id` বাদ দেওয়া যাবে না।**
- `P2-03` (Validator): `PeriodResolver::forDate`।
- `P2-12` (Post), `P3-11` (Period বন্ধ), `P3-12` (PeriodGuard)।

**Related tables / 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)`**, `idx_acc_periods_company_dates (company_id, start_date, end_date)`, `ck_acc_periods_dates` (`end_date >= start_date`), `ck_acc_periods_status`।

**Business rules**
- Period-এর তারিখ ফাঁকহীন ও overlap-হীন।
- যে বছরে post হওয়া লেনদেন আছে, তার period-এর তারিখ বদলানো যাবে না (`SET_LOCKED`-এর মতো)।
- FY শুরু মাসের ১ তারিখ না হলে (যেমন ১৫ জুলাই) period-ও সেই তারিখে শুরু ও তার আগের দিন শেষ।
- Edge: leap year; ২৯ ফেব্রুয়ারিতে শুরু হওয়া FY (setting-এ যাচাই করে `SET_INVALID`); বছরের মাঝে setting বদল (lock আটকায়)।

**DB flow**
- **তৈরি:** `PeriodGenerator::ensureYear(companyId, fiscalYear)`: (1) `fiscal_year_start` পড়া (না থাকলে `SET_INCOMPLETE`); (2) ১২টা মাসের সীমা হিসাব; (3) transaction-এ `INSERT ... ON DUPLICATE KEY` (বা `INSERT IGNORE`) দিয়ে সব period; আবার চালালে একই ফল।
- **খোঁজা:** `PeriodResolver::forDate(companyId, date)`: `SELECT ... WHERE company_id = ? AND start_date <= ? AND end_date >= ?`; না পেলে `JRN_DATE_INVALID`।

**Job**
মাসে একবার চলে, পরের আর্থিক বছরের period আগে থেকে তৈরি করে রাখে (idempotent, `AccountingJob` base দিয়ে company context সহ; `P1-12` লাগে)।

**Task scope**
Migration, generator, resolver, scheduler job, test।

**Error**
`SET_INCOMPLETE`, `JRN_DATE_INVALID`

**Audit**
`accounting.period.generated`

**Acceptance**
নতুন AC1.12: period ফাঁকহীন ও overlap-হীন

**Test**
বিভিন্ন FY শুরু (জানুয়ারি, জুলাই, ১৫ জুলাই); আবার চালানো; leap year; তারিখ খোঁজা।

**Deliverable**
Migration, generator, resolver, job।

**Definition of done**
- Phase 1-এর সাধারণ DoD পূরণ।
- AC1.12 সবুজ (`PeriodGeneratorTest`)।

**Architecture constraints**
- Period scheduler job অবশ্যই `P1-12`-এর `AccountingJob` base — `companyId` ছাড়া queue-তে যাবে না।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service)
Title
Journal-এর ভিত্তি table ও idempotency — Backend
Branch Name
`feature/accounting-journal-foundation-backend`
View Order
15
Description
**Summary**
এই কাজটায় সব journal-এর জন্য header, line, attachment table আর API idempotency বসবে — Phase 1-এ মূলত opening-এর জন্য, কিন্তু নকশা Phase 2 ও 4-এর পুরো journal flow-এর জন্য।

**Task serial**
`ACC-P1-09 — Journal-এর ভিত্তি table ও idempotency` · Feature

**Depends on**
- `ACC-P1-06` (CoA: `uq_acc_accounts_company_id`)
- `ACC-P1-08` (Period: `uq_acc_periods_company_id`)
- `ACC-P0-05` (DB guard ও ledger-উৎস সিদ্ধান্ত)

**Relation: আগের task**
`P1-06` (`uq_acc_accounts_company_id`) আর `P1-08` (`uq_acc_periods_company_id`) composite FK-র লক্ষ্য unique key দিয়েছে। `P0-05` spike-এ generated column আর constraint নকশা ঠিক হয়েছে — সেটা এখানে বাস্তবায়ন।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P1-10` (DB guard): এই task-এর table-এ trigger বসায়।
- `P1-11` (Opening balance): `acc_journals`, `acc_journal_lines` ব্যবহার।
- পুরো Phase 2। Phase 4-এর `uq_acc_journals_source_active` এই task-এ বসে।
- **Probe-এর আসল implementation:** `P1-03`-এর `PostedJournalProbe` আর `P1-06`-এর `PostedLineProbe`-এর stub এই task বদলে আসল query দেয়।

**Related tables / 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-এর সময় বসে |
| `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-এর জন্য |
| `reverses_journal_id`, `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` |  | হ্যাঁ |  |
| `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 অনেক চলে)
- **Source unique (সংশোধিত, `P4-06` থেকে):** সরাসরি তিন 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 বাতিল হলে নতুন 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`, `posted_at`, `period_id` সব 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`
- Index: `idx_acc_journals_company_status_date`, `idx_acc_journals_company_ref`, `idx_acc_journals_company_type_status`

**`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: `uq_acc_jl_journal_line (journal_id, line_no)`; `fk_acc_jl_journal (company_id, journal_id)` → `acc_journals` `ON DELETE RESTRICT`; `fk_acc_jl_account (company_id, account_id)` → `acc_accounts`; `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)`।

**`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)`।

**`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)`।

**Business rules**
- Posted হওয়া journal বা line বদলানো এই task-এ শুধু schema-এ ঠিক করা; **প্রয়োগ `P1-10`-এ।**

**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 দিন।

**Task scope**
চারটা migration, model, idempotency middleware ও cleanup job, দুটো probe-এর আসল implementation, constraint test, idempotency test।

**Error**
`ACC_IDEMPOTENCY_CONFLICT` (409)

**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।

**Definition of done**
- Phase 1-এর সাধারণ DoD পূরণ।
- AC1.1 সবুজ; `ConstraintTest` সবুজ।
- `P1-03`-এর `SET_LOCKED` এখন আসল probe দিয়ে কাজ করে (posted journal থাকলে)।
- MySQL-এ test (SQLite-এ generated column ও composite FK আলাদা আচরণ করে)।

**Architecture constraints**
- Journal status transition API এই task-এ নয় — Phase 2 `PostingService`/state machine।
- Posted row delete/`CASCADE` নিষিদ্ধ (guard `P1-10`-এ)।

Owner
Dev A
Estimate Hours
6
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema, trigger)
Title
DB স্তরের অপরিবর্তনীয়তা (guard) — Backend
Branch Name
`feature/accounting-db-guard-triggers`
View Order
16
Description
**Summary**
এই কাজটায় posted journal আর line যেন application ছাড়াও সরাসরি SQL দিয়ে বদলানো বা মোছা না যায় — DB trigger দিয়ে guard।

**Task serial**
`ACC-P1-10 — DB স্তরের অপরিবর্তনীয়তা (guard)` · Feature

**Depends on**
- `ACC-P1-09`
- `ACC-P0-05` (OD-13: trigger-ভিত্তিক বিকল্প (ক) ধরে লেখা)

**Relation: আগের task**
`P1-09` journal table দিয়েছে। `P0-05` spike-এ যে prototype প্রমাণিত, trigger guard-এর নকশা **হুবহু সেটাই** — নতুন করে ভাবার সুযোগ নেই।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P2-01` (state machine): trigger-এর অনুমোদিত পথ (`approved` থেকে `posted`) ধরে চলে।
- `P2-12` (Post): `posted_hash` এই task-এর column-এ বসায়।
- `P2-04`, `P2-05`, `P3-11`, `P4-09`, `P3-13`, `P5-06` এই task-এর **`Schema::appendOnly('table')` helper** ব্যবহার করে append-only table বানায়।
- `P3-13` (RC9): `posted_hash` মিলায়।

**Related tables / schema**

**নিয়ম যা 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:** এই Phase-এ `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
```

**Business rules**
- নতুন column `acc_journals.posted_hash CHAR(64) NULL` (expand migration)। `P2-12` post-এর সময় journal আর তার line-এর canonical SHA-256 এখানে বসায়।
- Application `SQLSTATE 45000` আর বার্তা `JRN_IMMUTABLE` ধরে error `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)।

**Task scope**
journals trigger, lines trigger, attachments trigger, append-only helper, `posted_hash` column, error mapping, test, fallback ও সতর্কতার নথি।

**Error**
`JRN_IMMUTABLE` (409), `ACC_APPEND_ONLY` (নতুন, 409)

**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।

**Definition of done**
- Phase 1-এর সাধারণ DoD পূরণ।
- AC1.9 সবুজ (`GuardTriggerTest`, MySQL-এ)।
- Runbook-এর জন্য `down()` সতর্কতা লেখা আছে (`P1-14` নেবে)।

**Architecture constraints**
- Trigger deploy না গেলে চুপচাপ fallback নয় — OD-13-এ client/tech lead লিখিত স্বীকৃতি।
- Posted data ঠিক করার পথ reversal/adjustment; production-এ trigger `down()` নয়।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P1-11 — Opening balance Draft

Project
ERPFlow
Module
Accounting Module
Type
Feature (service + api + ui)
Title
Opening balance Draft — Backend ও Frontend
Branch Name
`feature/accounting-opening-balance`
View Order
17
Description
**Summary**
এই কাজটায় opening balance হাতে ঢোকানোর গ্রিড থাকবে — Debit/Credit না মিললে সংরক্ষণই হবে না (transaction rollback)।

**Task serial**
`ACC-P1-11 — Opening balance Draft` · Feature

**Depends on**
- `ACC-P1-07` (CoA API, account picker)
- `ACC-P1-09` (journal table)
- `ACC-P1-10` (DB guard)
- `ACC-P1-12` (error, audit, tenancy)
- OD-9 (cutover তারিখ setting-এ থাকে; বসানো Admin-এর কাজ)

**Relation: আগের task**
`P1-07` leaf/active account picker; `P1-09` `acc_journals`/`acc_journal_lines` (এক company-তে এক opening — `uq_acc_journals_opening`); `P1-10` Draft-এ line লেখা/`DELETE` অনুমোদন; `P1-12` error, audit, tenancy।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P2-14` (Opening post): এই Draft সাধারণ Submit, approve, post পথে যায়।
- `P1-05`-এর S10: opening post হলে সবুজ।
- `P5-02` (Cutover মহড়া): এই গ্রিডে ঢোকানো হয়।
- **খোলা প্রশ্ন OD-23:** opening-এ Revenue বা Expense account থাকতে পারবে কি। Pack ধরে নিয়েছে (ক) "বিশেষ আচরণ নেই", কিন্তু Accountant-কে এই task শুরুর আগে সিদ্ধান্ত দিতে হবে। (খ) হলে এই task-এর validator বাড়বে।

**Related tables / schema**
কোনো নতুন table নেই। `acc_journals` (`journal_type = 'opening'`) ও `acc_journal_lines`। একটা company-র একটাই সক্রিয় opening journal, `uq_acc_journals_opening` (`P1-09`) DB স্তরে নিশ্চিত করে।

**Business rules (Blueprint ৬.৫, R1.7 থেকে R1.11)**
- **কোনো আপনা-আপনি মেলানোর line (যেমন "Opening Balance Equity" দিয়ে পার্থক্য ঢাকা) তৈরি হয় না।** না-মেলা balance চুপচাপ গ্রহণ নয়।
- প্রতিটা account leaf ও active ও এই company-র।
- Debit বা Credit একটাই; শূন্য amount-এর line বাদ।
- Draft ছাড়া অন্য অবস্থায় (posted হলে) সংশোধন Phase 2-এর Adjustment-এ।

**DB flow: `PUT` (এক transaction)**
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`।
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 বানানো যায়।

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

**Definition of done**
- Phase 1-এর সাধারণ DoD পূরণ।
- AC1.3 সবুজ (`OpeningBalanceTest`)।
- না-মেলা balance কখনো DB-তে থাকে না (rollback প্রমাণিত)।

**Architecture constraints**
- Opening journal শুধু service layer — raw SQL insert নয়। Post workflow Phase 2 (`P2-14`)।
- System কখনো "Opening Balance Equity" দিয়ে পার্থক্য auto-fill করবে না (R1.7)।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Test
Title
Phase 1-এর test suite ও regression
Branch Name
`feature/accounting-phase1-tests`
View Order
18
Description
**Summary**
এই কাজটায় Phase 1-এর সব Acceptance Criteria (AC1.1–AC1.12) স্বয়ংক্রিয় test দিয়ে প্রমাণিত হবে — বিদ্যমান ERPFlow suite-ও সবুজ থাকতে হবে।

**Task serial**
`ACC-P1-13 — Phase 1-এর test suite ও regression` · Test

**Depends on**
- `ACC-P1-01` থেকে `ACC-P1-12` সব

**Relation: আগের task**
প্রতিটা Phase 1 task নিজের unit/feature test লিখেছে। এখানে AC1.1–AC1.12 ধরে suite এক জায়গায় জোড়া, ফাঁক ধরা, আর পুরো ERPFlow regression সবুজ রাখা।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `ACC-P1-14` (Runbook ও merge gate)
- Phase 2 শুরু: **Phase 1 test সবুজ না হলে `P2-01` শুরু করা যাবে না।**

**Related tables / schema**
নতুন table নেই।

**AC থেকে test**

| 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।

**Business rules**
- **CI-তে MySQL-ভিত্তিক job চালাতে হবে।** SQLite-এ trigger, generated column, composite FK আলাদা আচরণ করে, তাই এই test গুলো MySQL ছাড়া মূল্যহীন।
- Golden suite (Blueprint ১২.২)-এর প্রথম ৪টা সবুজ হতে হবে।

**Task scope**
প্রতিটা test class আলাদা, factory, CI job, test report।

**Deliverable**
সব test class, factory, CI configuration, test report।

**Definition of done**
- AC1.1 থেকে AC1.12 সবুজ।
- Golden suite-এর প্রথম ৪টা সবুজ।
- বিদ্যমান ERPFlow suite সবুজ।
- MySQL CI job ব্যবহার হয়েছে।

**Architecture constraints**
- Guard trigger, generated column, composite FK test **MySQL CI-তে** — SQLite-এ মূল্যহীন।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P1-14 — Runbook ও merge gate

Project
ERPFlow
Module
Accounting Module
Type
Operations (`ops`)
Title
Runbook ও merge gate
Branch Name
`feature/accounting-phase1-runbook`
View Order
19
Description
**Summary**
এই কাজটায় Phase 1 নিরাপদে `development`-এ merge করার runbook আর gate থাকবে (flag বন্ধ), Phase 2 শুরুর আগে accountant-এর লিখিত মতামত লাগবে।

**Task serial**
`ACC-P1-14 — Runbook ও merge gate` · Ops

**Depends on**
- `ACC-P1-13`

**Relation: আগের task**
`P1-13` AC সবুজ প্রমাণ করেছে। এখন runbook, merge gate checklist আর accountant demo/মতামত — তারপর Phase 2।

**Relation: পরের task (কে অপেক্ষা করছে)**
Phase 2 শুরু (`P2-01`)। `P2-17` এই runbook হালনাগাদ করে।

**Business rules**
- Phase 1 exit criteria (Blueprint ৬.১০) পূরণ।
- `development`-এ push মানে production deploy, তাই merge gate কঠোর।

**Task scope**
1. **Runbook-এর প্রথম খসড়া:** module কী, flag, Setup Checklist, trigger-এর সতর্কতা (`P1-10`), `down()` চালানো নিষেধ, সাধারণ সমস্যা।
2. **Merge gate checklist:** সব flag Default বন্ধ প্রমাণ; প্রতিটা migration পিছনে সামঞ্জস্যপূর্ণ প্রমাণ; বিদ্যমান suite সবুজ; Project/Payroll/Asset/Commissioning-এর code অপরিবর্তিত (diff দিয়ে প্রমাণ)।
3. **Accountant demo:** নমুনা company-তে CoA, settings আর opening Draft দেখানো; মতামত লিখে রাখা।

**Deliverable**
Runbook খসড়া, merge gate-এর সই করা checklist, accountant-এর মতামত নথি।

**Definition of done**
- সই করা merge checklist আছে।
- Accountant-এর লিখিত মতামত আছে।
- Phase 1 exit criteria (Blueprint ৬.১০) পূরণ।

**Architecture constraints**
- Phase 1 merge শুধু সব accounting flag default `false` রেখে — `P0-03` CI check সবুজ।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

<!-- ===== END: ACC-Phase-1-Tasks.md ===== -->


<!-- ===== BEGIN: ACC-Phase-2-Tasks.md ===== -->

# Accounting Module — Phase 2 Tasks (Journal Engine ও Approvals)

> এখানকার প্রতিটা task আলাদা করে একজন কাজ করতে পারবে।
> Phase 2 শেষ হলে হাতে লেখা journal-এর পুরো চক্র চলবে: Draft → Submit → Approve → Post → Reversal।
> বানানোর ক্রম (সংক্ষেপে): state machine → validator → SoD → status history → approval (schema, rule, eligibility, resolver, request) → voucher → Draft UI → posting → delegation/override → reversal → opening post → queue/register → test → runbook।
> Estimate, Owner, Sprint ইচ্ছা করে খালি রাখা হয়েছে — পরে ভর্তি হবে।
> View Order 20 থেকে 37, **build ক্রম অনুযায়ী** (সংখ্যার ক্রমে নয়)। Master build sequence (Section ১০.১) অনুযায়ী `P2-09` দুই ভাগ: `P2-09a` (শুধু `ApproverEligibility`) আর `P2-09b` (delegation CRUD, override, পর্যালোচনা, UI)। তাই এই ফাইলে মোট ১৮টা card।

## সব Phase 2 task-এ প্রযোজ্য সাধারণ নিয়ম (প্রতিটা task নিজের DoD-তে এগুলো ধরে নেয়)

Phase 1-এর সাধারণ নিয়ম (`ACC-Phase-1-Tasks.md`-এর শুরুর block) এখানেও একই রকম — সংক্ষেপে:

- Database: MySQL, InnoDB, `utf8mb4`। Table prefix `acc_`।
- প্রতিটা business table-এ `company_id NOT NULL` + `companies(id)` FK; প্রতিটা composite index-এ `company_id` সবার আগে।
- টাকা `DECIMAL(20,4)`। Enum `VARCHAR` + `CHECK` (MySQL enum নয়)।
- বদলানো যায় এমন row-এ `version INT UNSIGNED NOT NULL DEFAULT 1` (`WHERE version = ?`, `version = version + 1`)।
- `ON DELETE RESTRICT` ডিফল্ট; লেজারের table-এ `CASCADE` নয়।
- Migration: প্রতি table-এ আলাদা, প্রয়োগ হওয়া migration সম্পাদনা নয়, পিছনে সামঞ্জস্যপূর্ণ (আগে যোগ, পরে বদল, শেষে মোছা), `down()` নিরাপদ।
- **`account_id` বা `journal_id` রাখা table-এ composite FK** `(company_id, account_id)` / `(company_id, journal_id)`।

Phase 2-এর অতিরিক্ত (না মানলে journal/approval ভেঙে যাবে):

- **Journal-এর `status` বদল শুধু `JournalTransitionService` (`P2-01`) দিয়ে।** অন্য কোনো code সরাসরি `acc_journals.status` set করবে না (code review + static test)। `P2-08`, `P2-09b`, `P2-12`, `P2-13` সবাই এই service-এর ভেতরের অংশ ডাকে।
- **Lock-এর ক্রম (Section ৯.১, সব Phase-এর জন্য):** `acc_journals` (`FOR UPDATE`) → `acc_approval_requests` (`FOR UPDATE`) → `acc_accounts` (`FOR SHARE`, `id` ছোট থেকে বড়) → `acc_periods` (Post-এ `FOR SHARE`) → `acc_voucher_counters` (`ON DUPLICATE KEY UPDATE`) → দ্বিতীয় journal (reversal-এর মূল journal, `FOR UPDATE`, শেষে)। ক্রম ভাঙলে deadlock — code review-তে আটকাবে। Blueprint-এর পুরোনো তিন-ধাপ আর Pack-এর পাঁচ-ধাপের পার্থক্য `P2-08`-এর "Open point"-এ দেখো।
- **Idempotency:** `POST` / `PUT` write endpoint-এ `Idempotency-Key` header (`P1-09`-এর middleware)। Post endpoint-এ বাধ্যতামূলক। একই key + একই request = আগের ফল; একই key + ভিন্ন request = `ACC_IDEMPOTENCY_CONFLICT` (409)।
- **Maker / Checker:** Maker = যে journal বানিয়েছে (`created_by`); `event_generated` journal-এ Maker = `submitted_by` (system তৈরি করে, `created_by` null, `P4-06`)। SoD-র সব যাচাই `SodGuard` (`P2-10`) দিয়ে — প্রতিটা card নিজের মতো if-else লিখবে না।
- **Error response আকার:** `{ code, message, field, details, correlation_id }`; একাধিক ভুল হলে প্রধান code প্রথমটা, বাকিগুলো `details.errors`-এ।
- **Test:** concurrency, atomicity, trigger, composite FK, generated column — সব MySQL-এ, একাধিক connection দিয়ে (SQLite-এ আচরণ আলাদা, তাই মূল্যহীন)। Factory-তে দুই company, প্রতিটায় পাঁচ role-এর user।
- **DoD-র সাধারণ অংশ:** Service–Repository architecture (interface সহ), Form Request, API Resource; `company_id` tenant scope + IDOR test; Feature test (happy path, validation, unauthorized/forbidden); audit event `AccountingAudit` দিয়ে (ব্যর্থ বা অস্বীকৃত চেষ্টাও); error `AccountingError` enum-এর code দিয়ে; flag (`accounting.enabled`) বন্ধ রেখে merge নিরাপদ; বিদ্যমান ERPFlow regression সবুজ; Project/Payroll/Asset/Commissioning-এর code অপরিবর্তিত।
- **"Open point"** মানে spec-এ অস্পষ্টতা বা অসঙ্গতি; dev নিজে অনুমান করে বসাবে না — tech lead / product owner থেকে লিখিত উত্তর নাও (card `Blocked` নয়, কিন্তু PR-এ উত্তর লিংক করতে হবে)।

**Phase 2 কে কার ওপর ঝুলে আছে:**

```
P1-13 ─► P2-01 ─┬─► P2-04 ─────────────────────────────┐
P1-10 ──┘       ├─► P2-05 ─┬─► P2-06 ─► P2-07 ─┐         │
                │          └─► P2-09a ─────────┤         │
P1-03 ─► P2-10 ─┼──────────────────────────────┤         │
P1-09,P1-08,P1-03 ─► P2-03 ─┬─► P2-02          ▼         ▼
                            │            P2-08 (P2-07,P2-10,P2-04,P2-09a)
P1-08,P1-09,P1-03 ─► P2-11 ─┤                │
                            ▼                ├─► P2-09b
              P2-12 (P2-08,P2-10,P2-11,P1-10,P2-03) ─┬─► P2-13
                                                     ├─► P2-14 (P1-11ও লাগে)
                                  P2-02,P2-08 ───────┴─► P2-15
        P2-01 থেকে P2-15 (P2-09a/b সহ) ─► P2-16 ─► P2-17 ─► Phase 3 (P3-01)
```

> গ্রুপ (Master sequence ১৩ থেকে ২১): ১৩ = P2-01, P2-03, P2-10 · ১৪ = P2-04, P2-05, P2-11 · ১৫ = P2-06, P2-09a, P2-02 · ১৬ = P2-07 · ১৭ = P2-08 · ১৮ = P2-12, P2-09b · ১৯ = P2-13, P2-14, P2-15 · ২০ = P2-16 · ২১ = P2-17। একই গ্রুপের card একসাথে (সমান্তরাল) চলতে পারে।

---

## ACC-P2-01 — Journal state machine

Project
ERPFlow
Module
Accounting Module
Type
Feature (service)
Title
Journal state machine ও transition service — Backend
Branch Name
`feature/accounting-journal-state-machine`
View Order
20
Description
**Summary**
Journal-এর সব অবস্থা বদল একটাই service (`JournalTransitionService`) দিয়ে হতে হবে — কোথাও সরাসরি `status` set করার পথ থাকলে চলবে না, না হলে নিয়ম ফাঁকি হবে।

**Task serial**
`ACC-P2-01 — Journal state machine` · Feature (service)

**Depends on**
- `ACC-P1-13` (Phase 1 exit; test সবুজ না হলে Phase 2 শুরু করা যায় না)
- `ACC-P1-10` (DB guard trigger: কোন status বদল DB অনুমোদন করে)

**Relation: আগের task**
- `P1-13` প্রমাণ করেছে Phase 1-এর সব AC (AC1.1 থেকে AC1.12) সবুজ আর বিদ্যমান ERPFlow অক্ষত। সেই প্রমাণ ছাড়া এই task শুরু হয় না। `P1-13`-এর factory (দুই company, পাঁচ role-এর user) আর MySQL CI job এই task-এর test ব্যবহার করে।
- `P1-09` বানিয়েছে `acc_journals` (সব status, `submission_no`, `submitted_at/by`, `approved_at`, `posted_at/by`, `cancelled_at`, `reversed_at`, `reversed_by_journal_id`, `version`, `ck_acc_journals_status`, `ck_acc_journals_posted`) আর idempotency middleware। এই task শুধু এই column-গুলো লেখে, নতুন column বানায় না।
- `P1-10` DB স্তরে নিশ্চিত করেছে: `approved → posted` transaction-এর ভেতরে চলে; `posted → reversed` শুধু `status`, `reversed_at`, `reversed_by_journal_id`, `updated_at`, `updated_by`, `version` বদলে চলে; `posted`/`reversed` অবস্থা থেকে আর কিছু বদলালে `JRN_IMMUTABLE`; line ও attachment শুধু `draft`/`rejected` অবস্থায় বদলানো যায়। এই task-এর transition table এই trigger-এর সাথে হুবহু মিলতে হবে (নইলে application অনুমোদন করবে অথচ DB আটকাবে)।
- `P1-12` দিয়েছে `AccountingError` enum, `AccountingException`, `AccountingAudit::log(...)`, correlation id। এই task এগুলোই ব্যবহার করে।
- `P1-02` slug নিবন্ধন করেছে (`accounting.journal.create/approve/post/reverse/override`); এই task শুধু permission যাচাই করে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P2-04` (status history): এই service-এর step 8 থেকেই history row লেখা হয়; `P2-04`-এর writer এই task-এর hook-এ বসে।
- `P2-05` (approval table): `acc_approval_requests.status`-এর সাথে journal status মেলে।
- `P2-02` (Draft editor): `can(journal, action, actor)` দিয়ে বাটন দেখায়, `cancel` এই service দিয়ে।
- `P2-08` (approval request): submit/approve/reject/withdraw/return সব এই service-এর ভেতরের অংশ ডাকে।
- `P2-09b` (override): `approve` পথ ধরে journal `approved` করে।
- `P2-12` (Post): `approved → posted` এবং reversal-এর ক্ষেত্রে মূল journal-এর `mark_reversed`।
- `P2-13` (reversal): `posted` কি না যাচাইয়ে, আর `P2-12`-এর মাধ্যমে `mark_reversed`।
- `P2-16` (test): `StateMachineMatrixTest` এই table থেকে।
- Source spec-এ `P2-01` "P2-02 থেকে সব Phase 2 card"-কে অবরুদ্ধ করে বলা আছে, কিন্তু Master sequence-এ `P2-03` আর `P2-10` একই গ্রুপে (১৩) সমান্তরাল। Open point-এ দেখো।

**Related tables / schema**
কোনো নতুন table নেই। পড়ে ও লেখে `acc_journals` (`P1-09`):

| Column (এই task যা লেখে) | কখন |
| --- | --- |
| `status`, `version`, `updated_by`, `updated_at` | প্রতিটা transition |
| `submission_no` (+1), `submitted_at`, `submitted_by` | `submit` (draft→submitted এবং rejected→submitted) |
| `approved_at` | শেষ ধাপে `approve` |
| `cancelled_at` | `cancel` |
| `voucher_no`, `period_id`, `posted_at`, `posted_by`, `posted_hash` | `post` (মান বসায় `P2-12`, এই service শুধু অনুমোদন দেয় আর `ck_acc_journals_posted` মেনে লেখার জায়গা দেয়) |
| `reversed_at`, `reversed_by_journal_id` | `mark_reversed` |

আরও পড়ে: `acc_approval_requests` (`P2-05`; `submit`-এ তৈরি, `withdraw`/`reject`/`return`-এ status বদলায়, কিন্তু সেই row-র লেখা `P2-08`-এর দায়িত্ব)।

**বৈধ পরিবর্তন (পুরো table, এর বাইরে সব `JRN_STATE_INVALID`):**

| থেকে | কাজ | যায় | কে | বিশেষ শর্ত | 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` |

`cancelled` আর `reversed` টার্মিনাল অবস্থা: এখান থেকে কোনো কাজ নেই।

**Business rules**
- সব অবস্থা বদল শুধু এই service দিয়ে। অন্য কোনো code সরাসরি `status` update করবে না। প্রমাণ: code review + একটা static test (`acc_journals`-এ `status` set করা শুধু এই class-এ)।
- `return` কাজটা Blueprint-এ ছিল না (সেখানে journal `approved`-এই থেকে যেত), কিন্তু তাহলে সমস্যাযুক্ত journal-এর সংশোধনের পথ থাকে না, তাই এই Pack যোগ করেছে (`approved → rejected`)। **Client-কে জানাতে হবে।** নিয়ম বদলাতে চাইলে এই table-ই একমাত্র জায়গা।
- `mark_reversed` কোনো user চালাতে পারে না; শুধু `P2-12`-এর ধাপ 14 থেকে, একই transaction-এ।
- `withdraw` শুধু তখন যখন request-এ কোনো `acc_approval_actions` row নেই (`P2-08` যাচাই করে, এই service শর্তটা ctx দিয়ে পায়)।
- `reject`, `return`-এ মন্তব্য বাধ্যতামূলক (R2.11)।
- `rejected` journal আবার `submit` হলে নতুন `submission_no`, নতুন request; আগের request-এর ইতিহাস থাকে।

**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. (`status`, `action`) জোড়া ওপরের table-এ বৈধ কি না।
5. কর্তা, SoD (`SodGuard`, `P2-10`) ও permission যাচাই।
6. কাজ-নির্দিষ্ট শর্ত (validator, approval)।
7. `UPDATE acc_journals SET status = ?, ..., version = version + 1 WHERE id = ? AND status = ? AND version = ?`। Affected rows 0 হলে কেউ মাঝে বদলে দিয়েছে: আবার পড়ে `JRN_CONFLICT` (version বদলেছে কিন্তু status একই) বা `JRN_STATE_INVALID` (status বদলেছে)।
8. `acc_journal_status_history`-এ row (`P2-04`)।
9. Audit (`AccountingAudit`)।
10. commit।

বাস্তবায়ন নোট (spec-এ নাম নেই, তবে `P2-08` ও `P2-12` "ভেতরের অংশ" বলেছে): caller (`P2-08`/`P2-12`/`P2-13`) আগে থেকেই transaction ও journal lock ধরে রাখে, তাই service-এ দুটো প্রবেশদ্বার লাগবে: (ক) পূর্ণ `apply` (নিজে transaction + lock নেয়, ধাপ 1-10), (খ) "ভেতরের অংশ" (ধাপ 4-9, caller-এর transaction ও lock ধরে নেয়)। দুটোর যাচাই একই কোড। Lock-ক্রম ভাঙা যাবে না।

**Task scope**
1. `JournalStateMachine`: ওপরের transition table কোডে data হিসেবে (একটাই উৎস); UI আর test এটা থেকেই পড়ে।
2. `JournalTransitionService::apply` (পূর্ণ) আর ভেতরের অংশ (উপরের নোট)।
3. `JournalTransitionService::can(journal, action, actor)`: transaction ছাড়া, lock ছাড়া, শুধু পড়া; UI-র বাটন দেখানোর জন্য (state ঠিক কি না, actor Maker/Approver/Poster কি না, SoD, permission)। `can()` true মানে নিশ্চয়তা নয়, `apply` আবার যাচাই করে।
4. Action প্রতি audit event (নিচের তালিকা)।
5. Static test: `acc_journals`-এ `status` set করা শুধু এই class-এ (repo scan)।
6. Matrix test: সব অবস্থা × সব কাজ।
7. Concurrency test: দুজন একসাথে একই journal-এ।

**Out of scope**
- Approval request-এর তৈরি/বদল (`P2-08`), history table (`P2-04`), voucher নম্বর (`P2-11`), Post-এর ধাপ (`P2-12`), reversal (`P2-13`)। এই task শুধু "এই জোড়া বৈধ কি না" আর "লেখার জায়গা" দেয়।
- UI (বাটন দেখানো `P2-02`, `P2-08`)।

**Contract**
- `JournalTransitionService::apply(journalId, action, actor, ctx)`
- `JournalTransitionService::can(journal, action, actor)`
- Action নাম: `submit`, `cancel`, `withdraw`, `approve`, `reject`, `return`, `post`, `mark_reversed` (`P2-04`-এর `action` enum-এ `create` ও আছে, সেটা journal তৈরির সময় `P2-02`/`P2-13` history-তে লেখে)।

**Permission**
- `accounting.journal.create` (submit, cancel, withdraw: Maker হিসেবে)
- `accounting.journal.approve` (approve, reject, return)
- `accounting.journal.post` (post, return)
- `mark_reversed`: কোনো user permission নয়, শুধু system context।

**Error**
`JRN_STATE_INVALID` (409), `JRN_CONFLICT` (409), `JRN_SOD_VIOLATION` (403), `ACC_FORBIDDEN` (403)

**Audit**
অবস্থা বদলের প্রতিটার নিজের event (Section ২.৫): `accounting.journal.submitted`, `.withdrawn`, `.approved`, `.rejected`, `.posted`, `.cancelled`, `.marked_reversed`। প্রতিটায় সাধারণ field (`company_id`, `actor_id`, `subject_type`, `subject_id`, `before`, `after`, `reason`, `correlation_id`)। ব্যর্থ বা অস্বীকৃত চেষ্টাও (SoD/permission) `accounting.guard.denied` হিসেবে।

**Acceptance**
AC2.4, AC2.10

**Test**
- প্রতিটা বৈধ জোড়া সফল।
- প্রতিটা অবৈধ জোড়া `JRN_STATE_INVALID` (matrix test: সব অবস্থা × সব কাজ; `StateMachineMatrixTest`)।
- দুজন একসাথে একই journal-এ (MySQL, দুই connection): একজন সফল, অন্যজন `JRN_CONFLICT` বা `JRN_STATE_INVALID`।
- `return` (`approved → rejected`), আর return-এর পর আবার `submit`।
- `withdraw` সিদ্ধান্ত হয়ে যাওয়ার পর আটকায়।
- Static test: অন্য কোনো class `status` set করে না।
- Transition table আর `P1-10` trigger মেলে: trigger-এ আটকানো কোনো জোড়া service অনুমোদন করে না।

**Deliverable**
State machine class, transition service, matrix test।

**Sub-tasks**
transition table, service, `can()`, static test, matrix test, concurrency test

**Definition of done**
- সাধারণ DoD (উপরে) পূরণ।
- AC2.4 ও AC2.10-এর এই অংশ সবুজ; `StateMachineMatrixTest` সবুজ MySQL-এ।
- Static test CI-তে চলে।
- `can()` আর `apply()` একই নিয়ম ব্যবহার করে (কোডে নকল নেই)।

**Architecture constraints**
- `status` বদলের দ্বিতীয় কোনো পথ নয়।
- Lock-ক্রম (journal → request → ...) ভাঙা যাবে না।
- Transition table-এর বাইরে "বিশেষ" কোনো শাখা নয়; নিয়ম বদলাতে হলে শুধু table বদলাবে।

**Open point**
1. Source-এ `P2-01` "P2-02 থেকে সব Phase 2 card"-কে অবরুদ্ধ করে লেখা, কিন্তু Master sequence-এ `P2-03` (নির্ভর করে `P1-09`, `P1-08`, `P1-03`) আর `P2-10` (`P1-03`) এই task-এর জন্য অপেক্ষা করে না এবং একই গ্রুপে চলে। এই ফাইলে Master sequence অনুসরণ করা হয়েছে (সমান্তরাল)।
2. `return` কাজের জন্য Audit registry (Section ২.৫)-এ আলাদা event নাম নেই। প্রস্তাব: `accounting.journal.rejected` ব্যবহার করে `after`-এ `action = return` রাখা, নয়তো নতুন event `accounting.journal.returned` registry-তে যোগ। লিখিত সিদ্ধান্ত লাগবে।
3. `approve` যখন `submitted → submitted` (মাঝের ধাপ), তখন step 8-এর history row লিখতে হবে কি না। Step 8 প্রতিটা `apply`-তে বলা, তাই এই ফাইলে ধরা হয়েছে লিখতে হবে (`from_status = to_status = submitted`, `action = approve`)। `P2-04`-এর test "প্রতিটা অবস্থা বদলে ঠিক একটা row" এই ব্যাখ্যার সাথে মিলিয়ে নিতে হবে।
4. `P2-12`-এর ধাপ 13 সরাসরি `UPDATE acc_journals SET status = 'posted' ...` লেখে, অথচ এই task-এর নিয়ম "শুধু এই class-এ status set"। প্রস্তাবিত সমাধান: `PostingService` ধাপ 13-র UPDATE-টা এই service-এর "ভেতরের অংশ" দিয়ে করে (যেমন `P2-08` approve-এ করে)। সিদ্ধান্ত tech lead/product-এর কাছে নিশ্চিত করো; static test-এর অনুমোদিত তালিকা সেই অনুযায়ী।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P2-03 — Journal validator

Project
ERPFlow
Module
Accounting Module
Type
Feature (service)
Title
Journal validator (SaveValidator ও SubmitValidator) — Backend
Branch Name
`feature/accounting-journal-validator`
View Order
21
Description
**Summary**
Journal হাতে লেখা হোক, reversal হোক, পরে event থেকে আসুক — Save আর Submit/Post-এ একই validator-এর নিয়ম লাগবে, যাতে ভুল journal কোনো shortcut দিয়ে ঢুকতে না পারে।

**Task serial**
`ACC-P2-03 — Journal validator` · Feature (service)

**Depends on**
- `ACC-P1-09` (journal table, line, composite FK)
- `ACC-P1-08` (`PeriodResolver::forDate`)
- `ACC-P1-03` (settings store ও registry)

**Relation: আগের task**
- `P1-09` দিয়েছে `acc_journals`, `acc_journal_lines` (`ck_acc_jl_amounts`: `debit >= 0 AND credit >= 0` এবং একটাই পাশ > 0), `acc_journal_attachments`। Validator এই table-গুলো পড়ে; DB check আছে বলেও application-এ আলাদা যাচাই লাগে, কারণ DB-র error বার্তা user-কে দেখানো যায় না আর `details.errors` তালিকা বানানো যায় না।
- `P1-08` দিয়েছে `PeriodResolver::forDate(companyId, date)` (না পেলে `JRN_DATE_INVALID`)। "তারিখ কোনো period-এ পড়ে" নিয়ম এটা ডাকে।
- `P1-03` দিয়েছে `SettingsService::get` আর registry-র key: `base_currency`, `currency_precision`, `opening_cutover_date`, `backdate_limit_days`, `attachment_required`, `duplicate_reference_mode`। সব এখান থেকে পড়া হয়।
- `P1-06` দিয়েছে `acc_accounts` (`is_active`, `is_group`, `company_id`); `P1-11` Opening balance-এ যে account-যাচাই (leaf, active, এই company) আছে এটা তারই সাধারণ রূপ।
- `P1-12` দিয়েছে `AccountingError` code ও `details.errors` কাঠামো।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P2-02` (Draft editor): সংরক্ষণে `SaveValidator`।
- `P2-08` (Submit): `SubmitValidator` Submit-এর ধাপ 4-এ, আর Re-submit-এ আবার।
- `P2-12` (Post): `SubmitValidator` "Post-মোডে" (নিচের table-এর "Post (আবার)" কলাম) ধাপ 8-এ।
- `P2-13` (Reversal/Adjustment): তৈরি হওয়া Draft Submit-এ একই validator-এ যায় (inactive account হলে `JRN_ACCOUNT_INVALID`)।
- `P4-06` (Event থেকে Draft) এই validator ডাকে। এ ছাড়া পরের Phase (Phase 6) এই task-এ hook যোগ করবে, তবে সেটা এই task-এর scope নয়।

**Related tables / schema**
কোনো নতুন table নেই। শুধু পড়া:
- `acc_journals` (header: `journal_date`, `reference_no`, `currency`, `journal_type`, `id`, `status`, `deleted_at`)
- `acc_journal_lines` (`account_id`, `debit`, `credit`, `line_no`)
- `acc_accounts` (`id`, `company_id`, `is_active`, `is_group`)
- `acc_journal_attachments` (শুধু আছে কি না / সংখ্যা)
- `acc_periods` (`PeriodResolver` দিয়ে)
- `acc_settings` (cache-সহ)

**Business rules**
দুটো 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` |

- Duplicate reference: `duplicate_reference_mode` `warn` হলে error নয়, `details.warnings`-এ; `block` হলে error (`JRN_DUPLICATE_REF`, HTTP 409 বা 422 setting অনুযায়ী)।
- Post-মোডে `backdate_limit_days`, attachment আর duplicate-reference আবার দেখা হয় না (Submit-এর সময়ই দেখা হয়েছে); কিন্তু account, balance, cutover, period আবার দেখা হয়, কারণ Submit থেকে Post-এর মাঝে account inactive হতে পারে বা setting বদলাতে পারে।
- একাধিক ভুল হলে প্রধান `code` প্রথমটা, বাকিগুলো `details.errors`-এ।
- Validator শুধু পড়ে, কিছু লেখে না।

**DB flow** (কোনো লেখা নেই, শুধু পড়া)
1. সব line-এর `account_id` একটাই query-তে (`WHERE company_id = ? AND id IN (...)`)। প্রতি line-এ আলাদা query নয়।
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. কোনো লেখা নেই, শুধু পড়া।

**Task scope**
1. `SaveValidator` ও `SubmitValidator`, প্রতিটা নিয়ম আলাদা class বা method, দুই validator একই নিয়ম-object ব্যবহার করে (নকল নয়)।
2. Aggregation: সব ভুল একসাথে `details.errors`-এ; প্রধান code প্রথমটা।
3. "Post-মোড": `SubmitValidator`-কে mode parameter দিয়ে ডাকা যায় (`submit` বা `post`), যাতে `P2-12` একই class আবার ব্যবহার করে।
4. Warning channel: `details.warnings` (duplicate reference `warn`)।

**Out of scope**
Approval rule-এর খোঁজা (`P2-07`), period `closed`/`strict` যাচাই (`PeriodGuard`, `P2-12`; Phase 3 এ পূর্ণ রূপ), DB constraint (`P1-09`)।

**Contract**
- `SaveValidator::validate(Journal $journal, array $lines): ValidationResult`
- `SubmitValidator::validate(Journal $journal, array $lines, string $mode = 'submit'|'post'): ValidationResult`
- `ValidationResult`: `errors[]` (code, field, details), `warnings[]`, `primaryCode()`।

**Permission**
নেই (সাধারণ service; ডাকা card নিজের permission যাচাই করে)।

**Error**
`JRN_ACCOUNT_INVALID` (422), `JRN_LINE_INVALID` (422), `JRN_DATE_INVALID` (422), `JRN_UNBALANCED` (422), `JRN_ATTACHMENT_REQUIRED` (422), `JRN_DUPLICATE_REF` (409 বা 422 setting অনুযায়ী)

**Audit**
নেই (পড়া-মাত্র; ডাকা card audit লেখে)।

**Acceptance**
AC2.1

**Test**
- প্রতিটা নিয়মের ইতিবাচক ও নেতিবাচক (table-driven)।
- একাধিক ভুল একসাথে (`details.errors` সব দেখায়, প্রথমটায় থামে না)।
- `warn` বনাম `block` (`duplicate_reference_mode`)।
- অন্য company-র account, inactive account, group account।
- একই journal নিজের reference-এ duplicate ধরে না (`id <> ?`), `cancelled` ও soft-deleted journal বাদ।
- Cutover-এর আগের তারিখ, `backdate_limit_days` সীমা (খালি = সীমাহীন), period-হীন তারিখ।
- Post-মোডে `backdate_limit_days`, attachment, duplicate আবার দেখা হয় না।
- Query সংখ্যা: ৫০০ line-এও account query একটাই।

**Deliverable**
দুটো validator class, rule-ভিত্তিক test।

**Sub-tasks**
প্রতিটা নিয়ম আলাদা class বা method, aggregation, post-mode, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC2.1 সবুজ (`JournalValidationTest`: Unbalanced journal Submit ও Post দুই জায়গাতেই আটকায়, এই task-এর অংশ)।
- Validator কোনো DB write করে না (test প্রমাণ)।

**Architecture constraints**
- নিয়মের কোড একটাই জায়গায়; `P2-02`, `P2-08`, `P2-12`, `P2-13`, `P4-06` কেউ নিজে নকল যাচাই লিখবে না।
- Settings প্রতি request-এ একবারই পড়া।

**Open point**
1. `attachment_required` এর "amount-এর সীমা" মান `acc_settings`-এ কোন আকারে থাকবে (সংখ্যা, নাকি object) source-এ নেই; `P1-03` registry-র সংজ্ঞা দেখে নিতে হবে।
2. Post-মোডের table-এ "তারিখ `backdate_limit_days`-এর মধ্যে" = না: নিশ্চিত যে Submit-এর পর Post-এ দেরি হলে backdate সীমায় আটকাবে না (ইচ্ছাকৃত, source তাই বলছে)।
3. `JRN_DUPLICATE_REF`-এর HTTP status (409 বা 422) "setting অনুযায়ী" বলা আছে কিন্তু কোন setting মান কোন status দেয় তা লেখা নেই। প্রস্তাব: `block` মোডে 409; লিখিত সিদ্ধান্ত লাগবে।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P2-10 — Segregation of Duties (SoD) service

Project
ERPFlow
Module
Accounting Module
Type
Feature (service)
Title
Segregation of Duties (SoD) service — Backend
Branch Name
`feature/accounting-sod-guard`
View Order
22
Description
**Summary**
Maker যেন নিজের journal approve বা post করতে না পারে — SoD নিয়ম এক জায়গায় (`SodGuard`) থাকবে; সব card সেখান থেকে ডাকবে, নিজে নিজে if-else লিখবে না।

**Task serial**
`ACC-P2-10 — Segregation of Duties (SoD) service` · Feature (service)

**Depends on**
- `ACC-P1-03` (settings: `sod_*` key-গুলো registry-তে)

**Relation: আগের task**
- `P1-03` registry-তে ইতিমধ্যে আছে `sod_maker_cannot_approve` (default true), `sod_maker_cannot_post` (true), `sod_rule_editor_cannot_approve` (true), `sod_mapping_editor_cannot_approve` (true, Phase 4), `sod_poster_cannot_sole_close` (true, Phase 3)। এই task নতুন setting বানায় না, `SettingsService::get` দিয়ে পড়ে।
- `P1-12` দিয়েছে `AccountingError::JRN_SOD_VIOLATION` (403) আর `AccountingAudit` (`accounting.guard.denied`)।
- `P1-04` ইতিমধ্যে নিজের একটা SoD যাচাই করে (অনুরোধকারী নিজে approve করতে পারে না, `decided_by <> requested_by`), কিন্তু সেটা সেই card-এর ভেতরে। এই task সেই নিয়মটা `SodGuard`-এর তালিকায় নাম দিয়ে রাখে যাতে একই জায়গা থেকে দেখা যায়।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P2-08` (approve): `SodGuard::assert('approve', ...)` ডাকে (`sod_maker_cannot_approve`, `sod_rule_editor_cannot_approve`)।
- `P2-09b` (override ও পর্যালোচনা): "Maker নিজের journal override করতে পারে না" আর "Override-কারী নিজের override পর্যালোচনা করতে পারে না" (দুটোই সবসময় চালু)।
- `P2-12` (Post): `SodGuard::assert('post', ...)` (`sod_maker_cannot_post`)।
- `P1-04`-এর settings approve নিয়ম (Phase 1-এর card), `P3-11` (period বন্ধে `sod_poster_cannot_sole_close`), `P4-05` (mapping), `P4-08` (event-journal approve, `sod_mapping_editor_cannot_approve`) পরে এই guard ব্যবহার করে।

**Related tables / schema**
কোনো নতুন table নেই, কোনো লেখা নেই। শুধু পড়া:
- `acc_journals.created_by`, `acc_journals.submitted_by`, `acc_journals.source_type`
- `acc_approval_requests.rule_id`, `acc_approval_requests.rule_snapshot` (যাতে `rule_version` ও `updated_by` থাকবে, `P2-08` বসায়)
- `acc_approval_rules.updated_by` (rule-এর শেষ বদলকারী)
- `acc_account_mappings.updated_by` (Phase 4)
- `acc_settings_history.changed_by`

**Business rules**
`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-09b` |
| Override-কারী নিজের override পর্যালোচনা করতে পারে না | সবসময় | — | `P2-09b` |

- "সবসময়" নিয়ম setting-এ `false` বসালেও আটকায় (setting আদৌ পড়াই হয় না)।
- "Maker" মানে `created_by`; `journal_type = 'event_generated'` journal-এ (system তৈরি, `created_by` null) Maker = `submitted_by` (`P4-06`, `P4-08`)।
- "যে rule বদলেছে" নির্ণয়: journal-এর `acc_approval_requests.rule_id`-র rule-এর `updated_by`। কিন্তু rule পরে আবার বদলাতে পারে, তাই `rule_snapshot`-এ `rule_version` ও `updated_by` রাখা হয় (`P2-08`-এর snapshot-এ এই দুটো field থাকবে), যাতে পরে rule আরও বদলালেও আগের তথ্য ঠিক থাকে। `SodGuard` snapshot থেকে পড়ে।
- Delegate-এর জন্যও SoD খাটে (সে Maker হলে সে approve করতে পারে না, `P2-09a`)।

**DB flow**
কোনো লেখা নেই। প্রয়োজনীয় তথ্য পড়া (উপরে "Related tables"-এ তালিকা)।

**Task scope**
1. `SodGuard` class: action নাম ধরে নিয়ম-তালিকা (বদ্ধ, কোডে একটাই উৎস)।
2. প্রতিটা নিয়ম একটা ছোট class বা method (rule-object), setting key সহ।
3. লঙ্ঘনে `AccountingException(JRN_SOD_VIOLATION)` আর `accounting.guard.denied` audit (নিয়মের নাম `details`-এ)।
4. "বন্ধ করা যায় না" নিয়মগুলো setting-নিরপেক্ষ।
5. Table-driven test।

**Out of scope**
- Permission যাচাই (`accounting.journal.approve` ইত্যাদি) এটা `ACC_FORBIDDEN`, এই guard শুধু SoD।
- `sod_mapping_editor_cannot_approve`-এর পূর্ণ রূপ (`P4-08` এই guard-এ নতুন শাখা যোগ করবে: `journal.source_type` থাকলে লিঙ্কের `mapping_snapshot.updated_by` পড়া)।
- `sod_poster_cannot_sole_close`-এর call (`P3-11`)।

**Contract**
`SodGuard::assert(string $action, User $actor, mixed $subject): void` (লঙ্ঘনে exception, নইলে কিছু না)।
Action নাম (প্রস্তাব, spec-এ "কোন নিয়ম কোন card ডাকে" তাই দেখে): `approve`, `post`, `override`, `override_review`, `settings_approve`, `period_close`।

**Permission**
নেই (ভেতরের service)।

**Error**
`JRN_SOD_VIOLATION` (403)

**Audit**
`accounting.guard.denied` (কোন নিয়ম ভাঙল সহ)

**Acceptance**
AC2.3

**Test**
- প্রতিটা নিয়ম চালু অবস্থায় আটকায়।
- বন্ধ করলে চলে (শুধু যেগুলো setting দিয়ে বন্ধ করা যায়)।
- বন্ধ করা যায় না এমনগুলো (settings-requester, Maker-override, নিজের override পর্যালোচনা) setting `false` করলেও আটকায়।
- `event_generated` journal-এ Maker = `submitted_by`।
- Rule পরে আবার বদলালেও snapshot-এর `updated_by` ঠিক থাকে (rule editor যে snapshot-এর সময়ের, সে-ই আটকায়)।
- Audit-এ কোন নিয়ম ভাঙল লেখা থাকে।

**Deliverable**
`SodGuard`, নিয়মের table-driven test।

**Sub-tasks**
guard class, প্রতিটা নিয়ম, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC2.3-এর SoD অংশ সবুজ (পরে `ApprovalSodTest`, `OverrideTest` এই guard ভিত্তি করে)।
- অন্য কোনো card-এ SoD-র if-else নকল নেই (code review)।

**Architecture constraints**
- নিয়মের তালিকা বদ্ধ: নতুন নিয়ম যোগ মানে কোডে নতুন rule-object, এলোমেলো if-else নয়।
- "বন্ধ করা যায় না" নিয়মগুলোকে কোনো setting-এর সাথে বাঁধা যাবে না।
- শুধু পড়া; এই service কখনো DB-তে লেখে না (audit বাদে)।

**Open point**
1. Source-এ এই card "অবরুদ্ধ করে" লিখেছে `P1-04-এর ব্যবহার (settings approve)`, অথচ `P1-04` Phase 1-এর card এবং নিজের `decided_by <> requested_by` যাচাই ইতিমধ্যে করে। প্রস্তাব: `P1-04`-এর যাচাই যেমন আছে তেমনই থাকবে, `SodGuard` সেই নিয়মকে তালিকায় নাম দেবে (পরে `P1-04` ইচ্ছা করলে guard ডাকবে)। নিশ্চিত করতে হবে।
2. Action স্ট্রিং-এর সঠিক নাম (`'approve'`, `'post'` source-এ উল্লেখ আছে; বাকিগুলো প্রস্তাব)।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P2-04 — Journal status history

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service + api)
Title
Journal status history — Backend
Branch Name
`feature/accounting-journal-status-history`
View Order
23
Description
**Summary**
প্রতিটা journal-এর পুরো যাত্রা লগে থাকতে হবে — কে কখন কোন status থেকে কোনটায় গেছে; audit-এর জন্য append-only history।

**Task serial**
`ACC-P2-04 — Journal status history` · Feature (schema + service + api)

**Depends on**
- `ACC-P2-01` (transition service, history লেখার hook)
- `ACC-P1-10` (`Schema::appendOnly('table')` helper)

**Relation: আগের task**
- `P2-01` তার ধাপ 8-এ "`acc_journal_status_history`-এ row (P2-04)" বলে রেখেছে। এই task সেই row-র table ও writer দেয়। Writer `JournalTransitionService`-এর একই transaction-এর ভেতরে ডাকে।
- `P1-10` দিয়েছে `Schema::appendOnly('table')` helper, যা `UPDATE`/`DELETE` ব্যর্থ করা trigger বসায় (`ACC_APPEND_ONLY`, 409)। এই table সেই তালিকায় আছে।
- `P1-09` দিয়েছে `acc_journals` ও `uq_acc_journals_company_id (company_id, id)` (composite FK-র লক্ষ্য)।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P2-08` (approval): নির্ভর করে (Master sequence), কারণ submit/approve/reject/withdraw/return প্রতিটায় history লাগে।
- `P2-15` (Queue ও Register): journal-এর timeline দেখানো।
- `P2-02` (Draft তৈরি) তৈরির সময় `from` null, `to` `draft` row লেখে; `P2-13` (reversal/adjustment Draft তৈরি) একইভাবে।
- `P3-03` (General Ledger drill-down): history পাতার লিংক।
- `P2-16`: `AuditHistoryTest` (AC2.10)।

**Related tables / schema**

**`acc_journal_status_history`** (append-only)

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | FK | না |  |
| `journal_id` | FK | না | Composite FK `(company_id, journal_id)` |
| `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)` → `acc_journals (company_id, id)`; `idx_acc_jsh_company_journal_time (company_id, journal_id, created_at)`; `ck` দিয়ে `action` তালিকা। Append-only (`P1-10`-এর helper, `Schema::appendOnly('acc_journal_status_history')`)।

**Business rules**
- শুধু `JournalTransitionService` (`P2-01`) লেখে, একই transaction-এ। অন্য কেউ নয় (code review + test)।
- প্রতিটা অবস্থা বদলে ঠিক একটা row। Transaction rollback হলে row-ও থাকে না।
- `UPDATE`/`DELETE` কখনো না (trigger আটকায়)।
- `comment`: reject, return, override-এর মন্তব্য; `meta`: ঐচ্ছিক অতিরিক্ত তথ্য (যেমন কোন approval level; ধরন source-এ নির্দিষ্ট করা নেই)।
- `actor_id` null = system (যেমন reversal post-এর ভেতরে `mark_reversed`)।
- Reversal Post হলে `P2-12` দুটো row লেখে, প্রতিটা journal-এর জন্য একটা (মূলের `mark_reversed` + reversal-এর `post`)।

**DB flow**
শুধু `JournalTransitionService`-এর ধাপ 8-এ: `INSERT INTO acc_journal_status_history (company_id, journal_id, from_status, to_status, action, actor_id, submission_no, comment, meta, created_at)`। কোনো read-modify-write নেই। পড়ার জন্য `GET`-এ `idx_acc_jsh_company_journal_time` ব্যবহার, `created_at` ক্রমে (সমান সময়ে `id` ক্রম)।

**Task scope**
Migration (composite FK, index, `Schema::appendOnly`), model (শুধু `create`, কোনো `update`/`delete` method নেই), `StatusHistoryWriter` (P2-01 ডাকে), `GET /accounting/journals/{id}/history`, resource, test।

**Out of scope**
Timeline UI (`P2-15` আর journal দেখার পাতা), approval-এর নিজের ইতিহাস (সেটা `acc_approval_actions`, `P2-05`/`P2-08`)।

**Contract**
- `GET /accounting/journals/{id}/history` → তালিকা (`from_status`, `to_status`, `action`, `actor` {id, নাম}, `submission_no`, `comment`, `meta`, `created_at`)।
- `StatusHistoryWriter::record(journal, from, to, action, actor, submissionNo, comment = null, meta = null)`

**Permission**
`accounting.journal.view`

**Error**
`ACC_FORBIDDEN`, `ACC_COMPANY_FORBIDDEN` (অন্য company-র journal), `ACC_APPEND_ONLY` (সরাসরি SQL-এ বদলানোর চেষ্টা)

**Audit**
নতুন event নেই (history নিজেই অবস্থা-বদলের লগ; audit event-এর নিজের নিয়ম `P2-01`-এ)।

**Acceptance**
AC2.10

**Test**
- প্রতিটা অবস্থা বদলে ঠিক একটা row।
- ব্যর্থ transaction-এ row নেই (rollback)।
- `UPDATE` ও `DELETE` ব্যর্থ (সরাসরি SQL, `ACC_APPEND_ONLY`)।
- অন্য company-র journal-এর history পাওয়া যায় না (IDOR)।
- Composite FK: অন্য company-র `journal_id` বসানো যায় না।
- তৈরির row-তে `from_status` null।

**Deliverable**
Migration, writer, API।

**Sub-tasks**
migration, writer, API, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC2.10-এর history অংশ সবুজ।
- `Schema::appendOnly` trigger MySQL-এ প্রমাণিত।

**Architecture constraints**
- Writer-এর একমাত্র caller `JournalTransitionService` (আর journal-তৈরির সময় `P2-02`/`P2-13`/`P4-06` যারা `create` row লেখে)।
- কখনো `CASCADE` নয়।

**Open point**
1. Journal-তৈরির `create` row (`from` null, `to` `draft`) কোন class লিখবে: `P2-02` সরাসরি writer ডাকে, কারণ `P2-01`-এর table-এ "তৈরি" কোনো transition নেই। এই ফাইলে ধরা হয়েছে `P2-02` (এবং `P2-13`, `P4-06`) writer-কে সরাসরি ডাকবে, শুধু `create` action-এর জন্য।
2. `meta` JSON-এর নির্দিষ্ট কাঠামো source-এ নেই।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P2-05 — Approval-এর table (schema)

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema)
Title
Approval-এর table (rule, ধাপ, request, সিদ্ধান্ত, delegation) — Backend
Branch Name
`feature/accounting-approval-schema`
View Order
24
Description
**Summary**
Approval-এর rule, ধাপ, request, সিদ্ধান্ত আর delegation রাখার table আগে বানাতে হবে — পরের service (rule, resolver, request, eligibility) এই schema-র ওপরই বসবে।

**Task serial**
`ACC-P2-05 — Approval-এর table (schema)` · Feature (schema)

**Depends on**
- `ACC-P2-01` (journal status ও transition; request-এর status journal-এর সাথে মেলে)
- `ACC-P1-09` (`acc_journals` ও `uq_acc_journals_company_id`)
- **OD-6 (বন্ধ থাকতে হবে):** এই card বিকল্প (গ) ধরে লেখা

**Relation: আগের task**
- `P2-01` নির্ধারণ করেছে কোন অবস্থায় approval request তৈরি/`cancelled`/`rejected`/`returned` হয়; তাই `acc_approval_requests.status` (`pending`, `approved`, `rejected`, `cancelled`, `returned`) তার সাথে মেলে।
- `P1-09` দিয়েছে `acc_journals (company_id, id)` unique, যার ওপর `acc_approval_requests`-এর composite FK দাঁড়ায়, আর `submission_no` column।
- `P1-10` দিয়েছে `Schema::appendOnly('table')` helper; `acc_approval_actions`-এ এটা বসে।
- OD-6: Platform-এর `ApprovalEngine` আছে কিন্তু audit-এ ধরা পড়েছিল তাতে amount threshold নেই, আর সেটা বাড়ালে অন্য module প্রভাবিত হতে পারে (সেজন্য বিকল্প (গ): Accounting-এর নিজস্ব table)।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P2-06` (rule service ও UI): `acc_approval_rules`, `acc_approval_rule_levels`-এর ওপর।
- `P2-09a` (`ApproverEligibility`): `acc_approval_rule_levels`, `acc_approval_delegations`, `acc_approval_requests`-এর `rule_snapshot` পড়ে।
- `P2-07` (resolver): rule ও ধাপ পড়ে।
- `P2-08` (request ও সিদ্ধান্ত): `acc_approval_requests`, `acc_approval_actions` লেখে।
- `P2-09b` (delegation CRUD, override): `acc_approval_delegations` লেখে, `acc_approval_actions`-এ `decision = 'override'`, আর নিজের table `acc_approval_override_reviews`।
- `P2-15` (pending/stuck/register): সব পড়ে, `JSON_TABLE(rule_snapshot ...)`।
- Source বলে `P2-05` "P2-06 থেকে P2-10"-কে অবরুদ্ধ করে; Master sequence-এ `P2-10` শুধু `P1-03`-এর ওপর নির্ভর করে, তাই এই ফাইলে ধরা হয়েছে `P2-10` আসলে অপেক্ষা করে না (Open point)।

**Related tables / 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 (`created_at`, `updated_at`, `created_by`, `updated_by`) |  |  | `updated_by` `SodGuard`-এর "rule editor" নিয়মে লাগে |

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)`।

**`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)`।

**`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)`।

**`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 স্তরেই আটকায়।

**`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)`।

**Business rules**
- Amount-এর পরিসর `[min_amount, max_amount)` (নিচের সীমা অন্তর্ভুক্ত, ওপরের নয়); `max_amount` null = সীমাহীন।
- `acc_approval_actions` append-only: একবার লেখা সিদ্ধান্ত বদলায় না।
- `uq_acc_aact_request_level (request_id, level_no)`: এক request-এর এক ধাপে একটাই সিদ্ধান্ত (DB স্তরে `APR_ALREADY_DECIDED`-এর ভিত্তি)। ফলে `parallel` mode-এও প্রতিটা ধাপের জন্য একটাই row।
- `rule_snapshot` Submit-এর সময় `P2-08` ভরে; rule বদলালে চলমান request অপরিবর্তিত। Snapshot-এ `rule_version` ও `updated_by` থাকতে হবে (`P2-10`-এর জন্য)।
- Composite FK নিয়ম: `acc_approval_requests (company_id, journal_id)` → `acc_journals (company_id, id)`; `acc_approval_rule_levels (company_id, rule_id)` → `acc_approval_rules (company_id, id)` (`uq_acc_ar_company_id` এর জন্যই)।
- `ON DELETE RESTRICT` সব FK-তে।
- **OD-6 বিকল্প (ক) বা (খ)-তে গেলে:** `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 ডাকবে। এই ফাইলের সব card বিকল্প (গ) ধরে লেখা।

**Task scope**
1. পাঁচটা migration (rules, levels, requests, actions, delegations): প্রতি table-এ আলাদা, expand-only।
2. Model ও relation (rule → levels; request → journal, rule, actions; delegation → users)।
3. `Schema::appendOnly('acc_approval_actions')` (`P1-10`-এর helper)।
4. Constraint test (সরাসরি SQL)।

**Out of scope**
- Rule save-এর যাচাই, overlap query (`P2-06`), resolver (`P2-07`), request লেখার লজিক (`P2-08`), eligibility (`P2-09a`), `acc_approval_override_reviews` table (`P2-09b` বানায়)।

**Contract**
Model: `ApprovalRule`, `ApprovalRuleLevel`, `ApprovalRequest`, `ApprovalAction`, `ApprovalDelegation` (Repository interface সহ)।

**Permission**
নেই (schema)।

**Error**
নেই (DB constraint ভাঙলে application-এ পরের card-গুলো `APR_*` / `SET_INVALID`-এ ম্যাপ করবে)।

**Audit**
নেই (পরের card-এ)।

**Acceptance**
AC2.10, AC2.13-এর ভিত্তি

**Test**
প্রতিটা check ও unique ভাঙার চেষ্টা সরাসরি SQL-এ:
- `ck_acc_ar_range` (max <= min), `ck_acc_ar_mode`, `ck_acc_ar_type`, `ck_acc_ar_self`।
- `ck_acc_arl_approver` (role ও user দুটোই null / দুটোই আছে)।
- `uq_acc_arl_rule_level` ও `uq_acc_areq_journal_sub` ও `uq_acc_aact_request_level` (দ্বিতীয় সিদ্ধান্ত একই ধাপে)।
- Delegation: `delegator_id = delegate_id`, `valid_to < valid_from`।
- Composite FK: অন্য company-র journal / rule জোড়া লাগানো যায় না।
- `acc_approval_actions`-এ `UPDATE`/`DELETE` ব্যর্থ।

**Deliverable**
পাঁচটা migration (rules, levels, requests, actions, delegations), model ও relation, append-only helper `acc_approval_actions`-এ।

**Sub-tasks**
migration ×5, model, constraint test

**Definition of done**
- সাধারণ DoD পূরণ।
- সব migration পিছনে সামঞ্জস্যপূর্ণ, `down()` নিরাপদ।
- Constraint test MySQL-এ সবুজ।
- OD-6 বিকল্প (গ) লিখিতভাবে বন্ধ (নইলে card `Blocked`)।

**Architecture constraints**
- সবকটা table `company_id`-সহ, composite FK।
- Platform `ApprovalEngine`-এর কোনো code বদলানো যাবে না।
- `rule_snapshot` এই schema-র অংশ, আলাদা table নয়।

**Open point**
1. OD-6 এখনও বন্ধ না হলে card `Blocked`; বিকল্প (ক)/(খ)-তে গেলে ওপরের নিয়ম।
2. "অবরুদ্ধ করে: P2-06 থেকে P2-10" বনাম Master sequence (`P2-10` একই গ্রুপ ১৩ এ, কারো ওপর নির্ভর করে না `P1-03` ছাড়া)। Master অনুযায়ী `P2-10` এই task-এর জন্য অপেক্ষা করে না।
3. `approver_role_id` কোন RBAC table-কে লক্ষ্য করে (FK টার্গেট) তা `P0-02`-এর Convention Sheet (প্রশ্ন ৭) থেকে নিতে হবে।
4. `version` column সব table-এ নেই (শুধু rules, requests); actions append-only; delegations-এ `version` লেখা নেই।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P2-11 — Voucher নম্বর

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service)
Title
Voucher নম্বর (ফাঁকহীন counter) — Backend
Branch Name
`feature/accounting-voucher-numbering`
View Order
25
Description
**Summary**
Post-এ voucher নম্বর ফাঁকহীন ও unique হতে হবে — audit-এ গর্ত বা duplicate থাকলে ধারাবাহিকতা প্রমাণ করা যায় না।

**Task serial**
`ACC-P2-11 — Voucher নম্বর` · Feature (schema + service)

**Depends on**
- `ACC-P1-08` (period ও fiscal year)
- `ACC-P1-09` (`acc_journals.voucher_no`, `uq_acc_journals_company_voucher`)
- `ACC-P1-03` (`voucher_format` setting)
- **OD-8:** এই card ধরেছে নম্বর Post-এর সময় বসে, company ও আর্থিক বছর ধরে ধারাবাহিক। Draft-এর সময় বসালে বাতিল হওয়া Draft-এ ফাঁক পড়ত

**Relation: আগের task**
- `P1-03` দিয়েছে `voucher_format` (prefix, company code, বছর, অঙ্ক সংখ্যা, বছরে পুনরায় শুরু; lock = হ্যাঁ, Phase 2, OD-8; default বাধ্যতামূলক) আর `SettingsService::get`। Setup Checklist `S06_voucher` এই setting দেখে (Phase 2 চালু হলে বাধ্যতামূলক)।
- `P1-08` দিয়েছে আর্থিক বছরের ধারণা (`acc_periods.fiscal_year`, শুরুর বছর, OD-4) আর `PeriodResolver`; counter-এর `fiscal_year` এখান থেকে আসে।
- `P1-09` দিয়েছে `acc_journals.voucher_no VARCHAR(40)` (null অনুমোদিত, Post-এর সময় বসে) এবং `uq_acc_journals_company_voucher (company_id, voucher_no)` যা DB স্তরে একই নম্বর দুইবার ঠেকায় (null অনেক চলে), আর `ck_acc_journals_posted` (posted হলে `voucher_no IS NOT NULL`)।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P2-12` (Posting): ধাপ 11-তে `VoucherService::next` ডাকে, Post-এর transaction-এর ভেতরে।
- `P2-13` (reversal): `reference_no = 'REV-' + মূলের voucher নম্বর`।
- `P3-03` (General Ledger) voucher নম্বর ধরে drill-down; `P2-15` Register voucher নম্বর দিয়ে খোঁজে।
- Phase 6 (`P6-03`, `P6-04`) পরে এই service-কে facade বানায় (flag বন্ধ থাকলে এই card-এর কোড হুবহু চলে); এই card-এর scope-এর বাইরে, শুধু জানিয়ে রাখা যে `VoucherService::next` এর signature পরে বদলাতে পারে।

**Related tables / 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)`।

**Business rules**
- নম্বর Post-এর সময় বসে, Draft-এর সময় নয় (OD-8)। Voucher নম্বর শুধু সফল Post-এ খরচ হয়।
- এটা lock-ক্রমের শেষ ধাপ (journal → request → account → period → counter)। Counter আলাদা transaction-এ বাড়ানো **নিষেধ**।
- একই company-র একই বছরের Post গুলো এই row-তে ক্রমে চলে, একটার পর একটা। এটা ইচ্ছাকৃত, নম্বর ফাঁকহীন রাখার দাম।
- Counter row-এর lock Post-এর commit পর্যন্ত থাকে, তাই Post ব্যর্থ হলে increment-ও rollback হয় আর নম্বর খরচ হয় না।
- `voucher_format` setting অনুযায়ী সাজানো: prefix, company code, বছর, নম্বর শূন্য দিয়ে ভরা, যেমন `JV-ACME-2026-000123`।
- `voucher_format` না থাকলে `SET_INCOMPLETE`।
- বাড়তি নিরাপত্তা: `uq_acc_journals_company_voucher` (`P1-09`) DB স্তরে একই নম্বর দুইবার ঠেকায়।
- `series` এখন সবসময় `JV`; ভবিষ্যতের জন্য column আছে।

**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 অনুযায়ী সাজানো (`VoucherService::format`)।

Transaction না থাকলে (caller Post-এর transaction-এর বাইরে) service exception দেয় (guard: `DB::transactionLevel() > 0` যাচাই)।

**Task scope**
1. Migration `acc_voucher_counters` (`uq_acc_vc`)।
2. `VoucherService::next`, `::format(setting, company, year, no)`, `::preview(company)` (Admin-এর জন্য ছাঁচের উদাহরণ)।
3. সমান্তরাল test (৫০টা Post, একাধিক DB connection)।
4. `voucher_format` না থাকলে `SET_INCOMPLETE`।

**Out of scope**
Post-এর বাকি ধাপ (`P2-12`), Admin UI যা `voucher_format` সম্পাদনা করে (`P1-04`), series ধরে আলাদা নম্বর (Phase 6)।

**Contract**
- `VoucherService::next(companyId, fiscalYear): string` (শুধু Post transaction-এর ভেতরে)
- `VoucherService::format(setting, company, year, no): string`
- `VoucherService::preview(company): string` (`GET` endpoint আছে কি না source-এ নেই; service method যথেষ্ট, UI চাইলে `P1-04`-এর পাতা ব্যবহার করবে)

**Permission**
নেই (ভেতরের service)। `preview` Admin ব্যবহার করে (`accounting.settings.view`-এর অধীনে ধরা যায়, tech lead/product-এর কাছে নিশ্চিত করো)।

**Error**
`SET_INCOMPLETE` (`voucher_format` নেই), 422

**Audit**
`accounting.voucher.assigned` (voucher নম্বর বরাদ্দ; Post-এর transaction-এর ভেতরে লেখা হয়)

**Acceptance**
AC2.9

**Test**
- ৫০টা সমান্তরাল Post-এ গর্ত বা পুনরাবৃত্তি নেই (একাধিক DB connection সহ, MySQL)।
- Post ব্যর্থ হলে counter আগের মানে (rollback)।
- বছর বদলে নতুন counter (নতুন `fiscal_year` row, 1 থেকে শুরু)।
- `voucher_format` না থাকলে `SET_INCOMPLETE`।
- Format: prefix/company code/বছর/শূন্য-ভরা অঙ্ক (`JV-ACME-2026-000123`), "বছরে পুনরায় শুরু" অপশন।
- Transaction ছাড়া ডাকলে exception।

**Deliverable**
Migration, service, ছাঁচ preview।

**Sub-tasks**
migration, `next`, `format`, `preview`, সমান্তরাল test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC2.9 সবুজ (`VoucherSequenceTest`-এর অংশ, `P2-16`-এ পূর্ণ)।
- OD-8 লিখিতভাবে বন্ধ।

**Architecture constraints**
- Counter কখনো আলাদা transaction-এ বাড়ানো যাবে না।
- Post ছাড়া কেউ `next` ডাকবে না।
- Lock-ক্রমের শেষ ধাপ।

**Open point**
1. "বছরে পুনরায় শুরু" যদি `voucher_format`-এ বন্ধ থাকে, `fiscal_year` কী মান নিয়ে counter চলবে (যেমন একটাই row নাকি সবসময় বছর ধরে)? Source-এ `fiscal_year` কলাম না-null, তাই সম্ভবত "বন্ধ" হলে একটা স্থির মান (যেমন 0) ধরা হবে, কিন্তু source এটা লেখেনি। `format` শুধু display-এ বছর বাদ দেবে নাকি counter-ও একত্র হবে, লিখিত সিদ্ধান্ত লাগবে।
2. Company code কোথা থেকে আসে (`voucher_format` setting-এর ভেতরে, নাকি `companies` table-এ কোনো field) source-এ স্পষ্ট নেই; `P1-03` registry দেখে নিতে হবে।
3. `fiscal_year`-র মান কীভাবে বের হয়: journal-এর তারিখের period-এর `fiscal_year` (শুরুর বছর, OD-4) ধরা হয়েছে।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Feature (service + api + ui)
Title
Approval rule-এর service ও configuration UI — Backend ও Frontend
Branch Name
`feature/accounting-approval-rules`
View Order
26
Description
**Summary**
Admin UI দিয়ে approval rule বানানো ও বদলানো যাবে — amount ও journal ধরন অনুযায়ী ধাপ কনফিগার, কোডে হার্ড-কোড rule থাকবে না।

**Task serial**
`ACC-P2-06 — Approval rule-এর service ও configuration UI` · Feature (service + api + ui)

**Depends on**
- `ACC-P2-05` (rule ও level table)
- **OD-6** (বিকল্প (গ) ধরে)

**Relation: আগের task**
- `P2-05` দিয়েছে `acc_approval_rules` (`journal_type`, `min_amount`, `max_amount`, `mode`, `self_approval`, `is_active`, `effective_from/to`, `version`, check `ck_acc_ar_range`, index `idx_acc_ar_lookup`) ও `acc_approval_rule_levels` (`level_no`, `approver_role_id`/`approver_user_id`, `condition` JSON, check `ck_acc_arl_approver`, unique `uq_acc_arl_rule_level`)।
- `P1-02` দিয়েছে slug `accounting.approval.manage` (rule/delegation বানানো) আর `accounting.journal.approve` (approver-এর যাচাই)।
- `P1-03` দিয়েছে `approval_no_rule_behavior` ও `SET_INVALID`, `JRN_CONFLICT`। `P1-05`-এর Setup Checklist ধাপ `S07_approval_rule` ("কমপক্ষে একটা সক্রিয় rule") এই card-এর data পড়ে।
- `P1-12`: `AccountingAudit`, error handler।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P2-07` (resolver): এই card-এ সংরক্ষিত rule ও ধাপই পড়ে। Overlap-হীনতা (এই card নিশ্চিত করে) না থাকলে resolver-এর `LIMIT 1` অনির্ণায়ক হতো।
- `P2-08`: Submit-এ resolver যে rule পায়, তার `rule_snapshot` বানায়।
- `P4-06`/`P4-08`: `journal_type = 'event_generated'` rule এই card-এ আগে থেকেই বানানো যায়; কোনো rule স্বয়ংক্রিয়ভাবে বানানো হয় না।
- `P1-05` `S07` ধাপ: শেষ সক্রিয় rule নিষ্ক্রিয় করলে লাল হয়।
- `P2-10` (`sod_rule_editor_cannot_approve`): rule বদলানো ব্যক্তির `updated_by` এই card-এর save-এ বসে।

**Related tables / schema**
কোনো নতুন table নেই। লেখে: `acc_approval_rules`, `acc_approval_rule_levels` (দেখুন `P2-05`)। পড়ে: `acc_settings` (`approval_no_rule_behavior`), RBAC role/permission table (approver-এর permission যাচাই, `P0-02`-এর পদ্ধতিতে)।

**Business rules**
- **Save-এর যাচাই:**
  1. `min_amount < max_amount` (max থাকলে)।
  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)। ফাঁকে পড়া journal-এর আচরণ `approval_no_rule_behavior` setting ঠিক করে (`block` → `APR_NO_RULE`, `fallback_approver`)।
- Blueprint-এ "ফাঁক ও overlap ধরা পড়ে" ছিল; এই Pack-এ overlap = error, ফাঁক = সতর্কতা।
- Rule বদলালে চলমান request বদলায় না, কারণ `rule_snapshot` (`P2-08`)।
- শেষ সক্রিয় rule নিষ্ক্রিয় করা যায়, তবে `S07` Setup ধাপ লাল হয়।
- `self_approval`: `inherit` (default), `allow`, `deny`। (Maker নিজের journal approve করতে পারে কি না; চূড়ান্ত সিদ্ধান্ত `SodGuard` + এই মান; ব্যবহার `P2-08`-এ।)
- Amount = journal-এর `total_debit`; পরিসর `[min_amount, max_amount)`।

**DB flow**
Transaction; rule row `SELECT ... FOR UPDATE`; `version` মিলিয়ে; overlap query:

```sql
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 <= ?)
```

(`any` ধরনের rule শুধু অন্য `any`-এর সাথে মিলিয়ে দেখা হয়।) ধাপ পুরো তালিকা হিসেবে বদলানো: পুরোনো মুছে নতুন বসানো, rule-এর `version + 1`। নতুন rule তৈরিতে `FOR UPDATE` লাগে না, শুধু insert ও overlap query।

**Task scope**
1. Save validator (উপরের ৭ নিয়ম), overlap query, gap হিসাব।
2. ধাপ বদল (`POST /{id}/levels`, পুরো তালিকা)।
3. API (`GET`/`POST`/`PUT` rule, `POST /{id}/deactivate`, `POST /{id}/levels`)।
4. UI: rule-এর তালিকা, rule সম্পাদনার ফর্ম (নাম, ধরন, amount-পরিসর, mode, ধাপের তালিকা: role বা user বেছে, শর্ত যোগ; self-approval)। সংরক্ষণের সময় overlap error আর ফাঁকের সতর্কতা।
5. "Rule পরীক্ষা" বাটন: amount আর ধরন দিলে কোন rule ও কোন ধাপ খাটবে তা দেখায় (`P2-07`-এর resolver ব্যবহার করে)।
6. Test।

**Out of scope**
Resolver নিজে (`P2-07`), request লেখা (`P2-08`), delegation (`P2-09b`)।

**Contract**
- `GET /accounting/approval-rules`, `POST /accounting/approval-rules`, `PUT /accounting/approval-rules/{id}` (`version` সহ)
- `POST /accounting/approval-rules/{id}/deactivate`
- `POST /accounting/approval-rules/{id}/levels` (ধাপ পুরো তালিকা হিসেবে)
- "Rule পরীক্ষা" endpoint: source-এ নাম নেই; `P2-07` resolver-কে ডাকা একটা শুধু-পড়া endpoint (Open point)।

**Permission**
`accounting.approval.manage` (লেখা); দেখা: `accounting.journal.view` (প্রস্তাব, tech lead/product-এর কাছে নিশ্চিত করো)

**Error**
`APR_RULE_GAP` (422), `JRN_CONFLICT` (409), `SET_INVALID` (422)

**Audit**
`accounting.approval.rule_changed` (আগে ও পরে সহ)

**Acceptance**
AC2.13

**Test**
- Overlap ধরা (একই ধরনে, `any` বনাম `any`, নির্দিষ্ট বনাম `any` মিলে না), সীমানা (max_amount = অন্যটার min_amount: overlap নয়)।
- ফাঁকের সতর্কতা (`details.gaps`)।
- শর্তহীন ধাপ ছাড়া rule আটকায়।
- Approver-এর `accounting.journal.approve` permission নেই = আটকায়।
- ধাপ নম্বরে ফাঁক আটকায়; role ও user দুটোই বা কোনোটাই নয় আটকায়।
- `version` ভাঙা (`JRN_CONFLICT`)।
- Rule বদল চলমান request-এ প্রভাব ফেলে না (snapshot, `P2-08` এর সাথে মিলিয়ে)।
- শেষ সক্রিয় rule নিষ্ক্রিয় করলে `S07` লাল।
- IDOR: অন্য company-র rule।

**Deliverable**
Service, API, UI।

**Sub-tasks**
save validator, overlap query, ধাপ বদল, API, rule UI, "rule পরীক্ষা" বাটন, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC2.13 সবুজ (`ApprovalRuleValidationTest`-এর অংশ)।
- Rule save-এ `updated_by` সঠিক বসে (`SodGuard`-এর জন্য)।

**Architecture constraints**
- কোডে কোনো rule বা ধাপ হার্ড-কোড নয়।
- Overlap যাচাই DB query-তে (application-এ বসে মেলানো নয়) আর rule row lock-সহ।
- Approver-যোগ্যতার যাচাই `P2-09a`-এর `ApproverEligibility`-র নিয়ম মেনে (role/user), তবে এই card save-এর সময় শুধু permission আছে কি না দেখে।

**Open point**
1. "Rule পরীক্ষা" endpoint-এর path ও permission source-এ নেই।
2. `fallback_approver` মোডে "settings-এ নির্ধারিত approver" কোন setting key-তে থাকে তা Section ২.৪ registry-তে নেই (শুধু `approval_no_rule_behavior`)। `P2-07`-এর জন্য আটকাতে পারে; `P1-03` registry-তে key যোগ করতে হবে কি না লিখিত সিদ্ধান্ত লাগবে।
3. `self_approval = allow` হলে `sod_maker_cannot_approve` (true) এর সাথে কীভাবে মিলবে: কোনটা প্রাধান্য পায় source-এ লেখা নেই।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P2-09a — ApproverEligibility

Project
ERPFlow
Module
Accounting Module
Type
Feature (service)
Title
ApproverEligibility (কে কোন ধাপে সিদ্ধান্ত দিতে পারে) — Backend
Branch Name
`feature/accounting-approver-eligibility`
View Order
27
Description
**Summary**
Approver-কে সিস্টেমই বলবে কোন ধাপে সিদ্ধান্ত দিতে পারবে (নিজের role/user, নাকি delegate) — অযোগ্য কেউ approve করলে চলবে না।

**Task serial**
`ACC-P2-09a — ApproverEligibility` · Feature (service)

**Depends on**
- `ACC-P2-05` (approval table: levels, delegations, requests)
- **OD-6** (বিকল্প (গ) ধরে)

**Relation: আগের task**
- `P2-05` দিয়েছে `acc_approval_rule_levels` (`approver_role_id` / `approver_user_id`, `condition`), `acc_approval_delegations` (`delegator_id`, `delegate_id`, `valid_from`, `valid_to`, `is_active`; index `idx_acc_adel_lookup (company_id, delegate_id, is_active, valid_from, valid_to)`), `acc_approval_requests.rule_snapshot` (Submit-এর সময়ের ধাপের তালিকা), `acc_approval_actions.acted_for_id`।
- `P0-02` (Convention Sheet) বলেছে RBAC কীভাবে role ও user-কে company ধরে জোড়ে; এই task সেই পদ্ধতিতে "user-এর ওই company-তে ওই role আছে কি না" দেখে।
- `P1-02` দিয়েছে `accounting.journal.approve` slug।
- Source-এ `P2-09` এক card ছিল; Master sequence (Section ১০.১) তাকে ভাগ করেছে: `P2-09a` = শুধু এই service (কারণ `P2-08` এটা ব্যবহার করে, তাই আগে লাগে), `P2-09b` = delegation CRUD, override, পর্যালোচনা ও UI।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P2-08` (approve/reject): ধাপ 5-এ `ApproverEligibility`। Sequential-এ শুধু `current_level`-এর জন্য, Parallel-এ যেকোনো অপূর্ণ ধাপের জন্য।
- `P2-09b`: delegation তৈরি/শেষ করার API, override flow (override-এও `APR_NOT_ELIGIBLE` নয় কিন্তু আলাদা permission), পর্যালোচনা। Delegation-এর যাচাই (delegate-এর `accounting.journal.approve` permission) `P2-09b`-এর।
- `P2-15` (আমার অনুমোদনের অপেক্ষায়): "যে pending request-এ আমি (বা আমার delegation-এ) এখন সিদ্ধান্ত দিতে পারি" এই নিয়মের SQL সংস্করণ লাগে; এই service-এর নিয়ম ও SQL-এর নিয়ম একই হতে হবে।
- `P2-16`: `ApprovalRoutingTest`, `ApprovalSodTest`।

**Related tables / schema**
কোনো নতুন table নেই, কোনো লেখা নেই। শুধু পড়া:
- `acc_approval_requests` (`rule_snapshot`, `current_level`, `mode`, `status`)
- `acc_approval_rule_levels` / `rule_snapshot` (ধাপ: `level_no`, `approver_role_id`, `approver_user_id`)
- `acc_approval_delegations`
- RBAC (user-role, company ধরে)
- `acc_approval_actions` (কোন ধাপে সিদ্ধান্ত হয়ে গেছে কি না)

**Business rules** — `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`-র মধ্যে (দুই প্রান্ত অন্তর্ভুক্ত), `is_active = 1`, আর `delegator_id` ধাপের জন্য যোগ্য (বিধি ১ অনুযায়ী delegator নিজে ধাপের জন্য যোগ্য)।
3. Delegation-এ কাজ করলে `acc_approval_actions.acted_for_id` = delegator। Delegate-এর জন্যও SoD খাটে (সে Maker হলে নয়, `P2-10`)।
- যোগ্যতা শুধু ধাপের জন্য; permission `accounting.journal.approve` আলাদা শর্ত (`P2-08`: "Approve বা reject করার জন্য `accounting.journal.approve` permission আর ধাপের যোগ্যতা দুটোই লাগে")।
- ধাপের তালিকা `rule_snapshot` থেকে (Submit-এর সময়ের), বর্তমান rule থেকে নয়, যাতে rule পরে বদলালে চলমান request-এর যোগ্যতা বদলায় না। User-এর role/delegation অবশ্য এখনকার সময়ে পড়া হয় (বর্তমান RBAC)।
- Sequential mode: শুধু `current_level`-এর ধাপ; Parallel: যেকোনো অপূর্ণ ধাপ (যে ধাপের জন্য `acc_approval_actions`-এ row নেই)।
- ধাপের `condition` (`account_ids`/`cost_center_ids`) Submit-এই resolver (`P2-07`) প্রয়োগ করে "প্রযোজ্য ধাপের তালিকা" বানিয়েছে, snapshot-এ শুধু প্রযোজ্য ধাপ আছে; এই service-এ আবার condition দেখার দরকার নেই।
- `APR_NOT_ELIGIBLE` (403) যোগ্যতা নেই হলে; যোগ্যতা-সংক্রান্ত অস্বীকৃতি audit-এ `accounting.guard.denied`।

**DB flow**
কোনো লেখা নেই। পড়া: (১) request ও snapshot (caller-এর হাতেই থাকে), (২) ধাপ-সংশ্লিষ্ট role/user মেলানো (RBAC), (৩) `SELECT ... FROM acc_approval_delegations WHERE company_id = ? AND delegate_id = ? AND is_active = 1 AND valid_from <= CURDATE() AND valid_to >= CURDATE()` (index `idx_acc_adel_lookup`) এবং প্রতিটা delegator-কে ধাপের জন্য যাচাই। Caller (`P2-08`) journal ও request `FOR UPDATE` ধরে রেখেই ডাকে, তাই এই service নিজে lock নেয় না।

**Task scope**
1. `ApproverEligibility::canAct(user, request, level): EligibilityResult` (যোগ্য কি না + `acted_for_id`)।
2. সহায়ক method (প্রস্তাব, source-এ নাম নেই): `eligibleLevels(user, request)` (Sequential-এ `current_level`, Parallel-এ অপূর্ণ ধাপগুলো) যাতে `P2-08` ধাপ বেছে নিতে পারে।
3. Delegation-এর সময়সীমা, role-এ লোকের সদস্যপদ, multiple delegator (একজন delegate একাধিক delegator-এর হয়ে)।
4. Test।

**Out of scope**
Delegation তৈরি/শেষ (`P2-09b`), UI (`P2-09b`), override (`P2-09b`), SoD যাচাই (`P2-10`)।

**Contract**
- `ApproverEligibility::canAct(User $user, ApprovalRequest $request, int $level): EligibilityResult` (`eligible`, `actedForId` null/delegator)
- (প্রস্তাব) `ApproverEligibility::eligibleLevels(User, ApprovalRequest): array`

**Permission**
নেই (ভেতরের service)।

**Error**
`APR_NOT_ELIGIBLE` (403) (caller `P2-08` ছোঁড়ে)।

**Audit**
নেই নিজে। অস্বীকৃতি `accounting.guard.denied` (caller-এর মাধ্যমে)।

**Acceptance**
AC2.3

**Test**
- সরাসরি user ধাপ; role-ভিত্তিক ধাপ (user-এর role আছে/নেই)।
- যোগ্য delegate (মেয়াদের ভেতরে, `is_active`)।
- মেয়াদ পেরোনো delegation (আজ > `valid_to`) এবং শুরু হয়নি (আজ < `valid_from`) = যোগ্য নয়।
- Delegator নিজে ধাপের জন্য যোগ্য নয় হলে delegate-ও নয়।
- `is_active = 0` delegation।
- Sequential-এ `current_level` ছাড়া অন্য ধাপ = অযোগ্য; Parallel-এ যেকোনো অপূর্ণ ধাপ যোগ্য; ইতিমধ্যে সিদ্ধান্ত হওয়া ধাপ যোগ্য নয়।
- অন্য company-র role/delegation গণ্য হয় না।
- Rule বদলালেও snapshot-এর ধাপ অনুযায়ী যোগ্যতা।

**Deliverable**
Eligibility service, test।

**Sub-tasks**
role/user মেলানো, delegation খোঁজা, `acted_for` নির্ধারণ, sequential/parallel যোগ্যতা, test

**Definition of done**
- সাধারণ DoD পূরণ।
- `P2-08` এই service ডাকতে পারে (contract স্থির)।
- OD-6 বন্ধ।

**Architecture constraints**
- যোগ্যতার নিয়ম একটাই জায়গায়; `P2-15`-এর SQL list-এর নিয়ম এর সাথে মিলিয়ে রাখা (test দিয়ে প্রমাণ)।
- লেখা নেই, lock নেই।

**Open point**
1. Delegator ধাপের জন্য যোগ্য কি না যাচাই কি delegator-এর নিজের delegation পর্যন্ত যায় (chain)? Source শুধু "delegator ধাপের জন্য যোগ্য" বলে; এই ফাইলে ধরা হয়েছে শুধু সরাসরি যোগ্যতা (role/user), chain নয়।
2. `eligibleLevels` helper source-এর নাম নয়, প্রস্তাব।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Feature (service + api + ui)
Title
Draft তৈরি ও সম্পাদনা — Backend ও Frontend
Branch Name
`feature/accounting-journal-draft`
View Order
28
Description
**Summary**
Maker-এর Draft editor লাগবে — header, line, attachment; Submit-এর আগে নিরাপদে save ও edit করা যাবে।

**Task serial**
`ACC-P2-02 — Draft তৈরি ও সম্পাদনা (API ও UI)` · Feature (service + api + ui)

**Depends on**
- `ACC-P2-01` (state machine, `can()`, `cancel`)
- `ACC-P2-03` (`SaveValidator`)
- `ACC-P1-07` (CoA API, account picker)

**Relation: আগের task**
- `P2-01` দিয়েছে `JournalTransitionService::can()` (বাটন দেখানো) ও `apply(cancel)`। এই task বাটনের নিয়ম নিজে লেখে না, `can()` থেকে নেয়।
- `P2-03` দিয়েছে `SaveValidator` (account এই company-র, active ও leaf; Debit/Credit একটাই, ঋণাত্মক নয়, precision-এর মধ্যে; currency = base currency)। Submit-এর কঠোর নিয়ম (`SubmitValidator`) এই task-এ চলে না, `P2-08`-এ।
- `P1-09` দিয়েছে `acc_journals`, `acc_journal_lines`, `acc_journal_attachments`, `acc_idempotency_keys` + middleware (`POST`/`PUT`-এ), composite FK, `ck_acc_jl_amounts`, `uq_acc_jl_journal_line (journal_id, line_no)`।
- `P1-10`-এর trigger: line/attachment শুধু `draft` বা `rejected` অবস্থায় বদলানো যায়, তাই "পুরোনো line `DELETE`, নতুন `INSERT`" সম্পাদনা-পদ্ধতি এই অবস্থায় বৈধ।
- `P1-07` দিয়েছে CoA API (`GET /accounting/accounts` filter: type, active, search, `is_group`), যা account picker ব্যবহার করে।
- `P1-11` (Opening balance) একটা বিশেষ ধরনের Draft editor আগেই বানিয়েছে: একই line-ডিলিট-আর-ইনসার্ট পদ্ধতি ও মোট/পার্থক্য UI। এই task সাধারণ journal-এর জন্য একই ধাঁচে।
- `P1-12`: audit/error/correlation।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P2-08`: Submit endpoint এই editor থেকে চালানো হয়; Re-submit rejected journal-এ। Source বলে `P2-02` "P2-08"-কে অবরুদ্ধ করে (Master sequence-এ `P2-08`-এর নির্ভরতা তালিকায় `P2-02` নেই; Open point)।
- `P2-15` (Register ও তালিকা): `GET /accounting/journals` এই task-এর list API-র ওপর filter/pagination বাড়ায়।
- `P4-06` (Event থেকে Draft): event-generated Draft সম্পাদনার নিয়ম (ব্যতিক্রম) এই task-এ আছে।
- `P2-13`: তৈরি হওয়া reversal/adjustment Draft এই editor-এ খোলে।
- `P3-03`: GL-এর drill-down journal পাতা (এই task-এর `GET /accounting/journals/{id}`)।

**Related tables / schema**
নতুন table নেই। পড়ে ও লেখে `acc_journals`, `acc_journal_lines`, `acc_journal_attachments` (`P1-09`), `acc_journal_status_history` (`P2-04`), `acc_idempotency_keys`; পড়ে `acc_accounts`, `acc_settings` (`base_currency`)।

আদৌ লেখা header column: `journal_type` (`manual`), `status` (`draft`), `journal_date`, `narration`, `reference_no`, `currency` (= `base_currency`), `total_debit`, `total_credit`, `created_by`, `updated_by`, `version`, `deleted_at`; line: `line_no`, `account_id`, `debit`, `credit`, `line_narration`, `cost_center_id`; attachment: `storage_path`, `original_name`, `mime_type`, `size_bytes`, `uploaded_by`।

**Business rules**
- Line-এর সর্বোচ্চ সংখ্যা **500**।
- Attachment-এর আকার ও ধরনের সীমা ERPFlow-এর বিদ্যমান file নিয়ম অনুযায়ী (`P0-02`)।
- **শুধু নিজের Draft সম্পাদনা:** `created_by` = actor। অন্যের Draft সম্পাদনা `ACC_FORBIDDEN`।
  - **ব্যতিক্রম:** `journal_type = 'event_generated'` Draft, যার `created_by` null (system তৈরি করেছে), যেকোনো `accounting.journal.create`-ওয়ালা সম্পাদনা করতে পারে (`P4-06`)। এমন journal-এ SoD-র "Maker" হলো যে Submit করেছে, `submitted_by`।
- সম্পাদনা শুধু `draft` বা `rejected` অবস্থায়; নইলে `JRN_STATE_INVALID` (DB guard-ও আটকায়)।
- মোছা: শুধু `draft` (soft delete, `deleted_at`); `rejected` মোছা যায় না, `cancel` করতে হয়।
- Copy: নতুন Draft, একই line, তারিখ আজ, `reference_no` ও attachment কপি হয় না, `source_*` null।
- কোনো company-র `accounting.enabled` বন্ধ হলে সব বন্ধ।
- `PUT` পুরো line তালিকা বদলায় (partial patch নয়), `version` মেলাতে হয়।
- `POST`, `PUT`-এ idempotency key।
- Save-এ শুধু হালকা যাচাই (`SaveValidator`): Debit/Credit সমান না হলেও Draft সংরক্ষণ চলে (কঠোর যাচাই Submit-এ)। UI চলতি মোট ও পার্থক্য দেখায়।

**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`, `action = create`)।
8. Audit (`accounting.journal.draft_created`)।
9. commit।

**DB flow: সম্পাদনা**
1. header `FOR UPDATE`।
2. `status` শুধু `draft` বা `rejected`, নইলে `JRN_STATE_INVALID` (DB guard-ও আটকায়)।
3. শুধু `created_by`, নইলে `ACC_FORBIDDEN` (ব্যতিক্রম ওপরে)।
4. `version` মেলানো (না মিললে `JRN_CONFLICT`)।
5. পুরোনো line `DELETE`, নতুন `INSERT`।
6. totals।
7. `version + 1`।
8. Audit (`accounting.journal.draft_updated`, before ও after-এ line-এর সারাংশ)।
9. commit।

**DB flow: মোছা** — Draft হলে `deleted_at` বসে, line আর attachment ফিজিক্যালি থাকে। `rejected` মোছা যায় না, `cancel` করতে হয়। Audit `accounting.journal.draft_deleted`।

**DB flow: copy** — নতুন Draft: একই line, তারিখ আজ, `reference_no` ও attachment কপি হয় না, `source_*` null।

**DB flow: cancel** — `POST /{id}/cancel` → `JournalTransitionService::apply(cancel)` (`P2-01`), `cancelled_at`, audit `accounting.journal.cancelled`।

**Task scope**
1. Service: create, update, delete (soft), copy, cancel (transition service ডাকা), attachment upload/delete।
2. API (নিচে)।
3. তালিকা API (`GET /accounting/journals` filter সহ প্রাথমিক রূপ; `P2-15` পূর্ণ করে)।
4. Editor UI:
   - Header: তারিখ, narration, reference।
   - Line গ্রিড: account picker (leaf ও active, code বা নাম দিয়ে অনুসন্ধান), Debit ও Credit, line-এর বিবরণ, cost center।
   - নিচে চলতি মোট ও পার্থক্য।
   - Attachment আপলোড।
   - বাটন: সংরক্ষণ (Draft), Submit, কপি, বাতিল। বাটন `can()` দেখে দেখায়।
   - Reject হওয়া journal-এ সংশোধন ও আবার submit, আর reject-এর মন্তব্য উপরে।
5. Test।

**Out of scope**
Submit/approve লজিক (`P2-08`), কঠোর যাচাই (`P2-03` `SubmitValidator`), history timeline UI (`P2-15`), বড় register filter (`P2-15`)।

**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 /accounting/journals/{id}/copy`
- `POST /accounting/journals/{id}/attachments`
- `DELETE /accounting/journals/{id}/attachments/{aid}`
- `POST /accounting/journals/{id}/cancel`
- `POST`, `PUT`-এ `Idempotency-Key`।

**Permission**
`accounting.journal.create` (লেখা), `accounting.journal.view` (দেখা)

**Error**
`JRN_LINE_INVALID`, `JRN_ACCOUNT_INVALID`, `JRN_STATE_INVALID`, `JRN_CONFLICT`, `ACC_FORBIDDEN`

**Audit**
`accounting.journal.draft_created`, `.draft_updated`, `.draft_deleted`, `.cancelled`

**Acceptance**
AC2.1, AC2.7

**Test**
- Draft তৈরি, সম্পাদনা।
- অন্যের Draft সম্পাদনা (নিষেধ, `ACC_FORBIDDEN`); `event_generated` Draft অন্য `journal.create`-ওয়ালা সম্পাদনা পারে।
- Submitted-এ সম্পাদনা (নিষেধ, `JRN_STATE_INVALID`); `rejected`-এ সম্পাদনা চলে।
- মোছা (Draft soft delete; rejected মোছা নিষেধ)।
- Copy (reference/attachment কপি হয় না, তারিখ আজ)।
- Idempotency (একই key দুইবার, ভিন্ন body = `ACC_IDEMPOTENCY_CONFLICT`)।
- `version` ভাঙা।
- ৫০১ line আটকায়।
- অন্য company-র account আটকায় (`JRN_ACCOUNT_INVALID`)।
- Flag বন্ধে সব বন্ধ।
- সরাসরি SQL-এ submitted journal-এর line বদলানো trigger আটকায় (`P1-10`-এর প্রমাণ এখানেও)।

**Deliverable**
API ও service, editor UI।

**Sub-tasks**
create, update, delete, copy, attachment, তালিকা API, editor UI, গ্রিড, মোট ও পার্থক্য, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC2.1 (Save অংশ), AC2.7 (সম্পাদনা বনাম অন্য কাজ concurrency) সবুজ।
- UI-র বাটন পুরোপুরি `can()` থেকে।
- UI test: গ্রিডে মোট ও পার্থক্য সঠিক।

**Architecture constraints**
- `status` সরাসরি set নয়; শুধু transition service।
- Line সম্পাদনা "পুরোনো মুছে নতুন বসানো", যাতে `line_no` ক্রমিক থাকে।
- Account picker `P1-07`-এর API দিয়ে, নিজের query নয়।

**Open point**
1. Source-এ `P2-02` "অবরুদ্ধ করে P2-08, P2-15" কিন্তু `P2-08`-এর নির্ভরতায় `P2-02` নেই। এই ফাইলে ধরা হয়েছে `P2-08`-এর backend আলাদা চলতে পারে, কিন্তু UI integration ও end-to-end test এই task শেষ হওয়ার পর।
2. Attachment-এর সর্বোচ্চ সংখ্যা source-এ নেই (শুধু আকার/ধরন)।
3. `GET /accounting/journals` এই card ও `P2-15` দুজনই সংজ্ঞায়িত করে; প্রস্তাব: এই card প্রাথমিক (নিজের Draft তালিকা ও সাধারণ filter), `P2-15` পূর্ণ filter ও cursor pagination।

Owner
Dev C
Estimate Hours
6
Status
Todo
QA Status
-
QA Comment

---

## ACC-P2-07 — Approval resolver

Project
ERPFlow
Module
Accounting Module
Type
Feature (service)
Title
Approval resolver (কোন rule ও কোন ধাপ খাটবে) — Backend
Branch Name
`feature/accounting-approval-resolver`
View Order
29
Description
**Summary**
Journal Submit হলে কোন rule আর কোন ধাপ খাটবে — একটাই resolver ঠিক করবে; একই input-এ সবসময় একই ফল, না হলে approval অনির্ণায়ক হয়ে যাবে।

**Task serial**
`ACC-P2-07 — Approval resolver` · Feature (service)

**Depends on**
- `ACC-P2-06` (rule ও ধাপ, overlap-হীনতার নিশ্চয়তা)
- **OD-6**

**Relation: আগের task**
- `P2-06` নিশ্চিত করেছে: একই `(company, journal_type)`-এ সক্রিয় rule-এর পরিসর overlap করে না; প্রতিটা rule-এ অন্তত একটা শর্তহীন ধাপ আছে; ধাপ নম্বর ফাঁকহীন; approver-এর permission আছে। এই নিশ্চয়তার ওপর resolver-এর "প্রযোজ্য ধাপ ফাঁকা হতে পারে না" নিয়ম দাঁড়ায়।
- `P2-05` দিয়েছে table ও index `idx_acc_ar_lookup (company_id, is_active, journal_type, min_amount)`।
- `P1-03` দিয়েছে `approval_no_rule_behavior` (`block` বা `fallback_approver`, default `block`)।
- `P2-06`-এর "Rule পরীক্ষা" বাটন এই resolver-ই ডাকে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P2-08`: Submit-এর ধাপ 5-এ `ApprovalResolver::resolve(journal)`; ফল থেকে `rule_snapshot` (rule id, `rule_version`, `updated_by`, mode, প্রযোজ্য ধাপ) বানায়।
- `P2-06` UI: "Rule পরীক্ষা"।
- `P4-08` (event-journal approval): `event_generated` ধরনের rule এই resolver-ই খুঁজে পায়।
- `P2-16`: `ApprovalRoutingTest` (AC2.2)।

**Related tables / schema**
কোনো নতুন table নেই, কোনো লেখা নেই। পড়ে `acc_approval_rules`, `acc_approval_rule_levels`, `acc_journal_lines` (condition মেলানোর জন্য `account_id`, `cost_center_id`), `acc_journals` (`journal_type`, `total_debit`), `acc_settings`।

**Business rules**
- Amount = `total_debit`।
- নির্দিষ্ট `journal_type` rule আগে প্রাধান্য পায়, তারপর `any`; একই ধরনের মধ্যে বড় `min_amount` আগে।
- ফল নির্ণায়ক: একই input-এ সবসময় একই ফল।
- প্রযোজ্য ধাপ ফাঁকা হতে পারে না (কারণ `P2-06`-এ শর্তহীন ধাপ বাধ্যতামূলক)।
- Rule না পেলে `approval_no_rule_behavior`: `block` হলে `APR_NO_RULE` (422); `fallback_approver` হলে settings-এ নির্ধারিত approver দিয়ে এক ধাপের নকল (synthetic) rule।
- Rule-এর তারিখ-পরিসর `CURDATE()` (Submit-এর দিন) ধরে `effective_from/to` মেলানো।
- ধাপের `condition`: `account_ids` বা `cost_center_ids`-এর **কোনো একটা** line-এ থাকলে ধাপ প্রযোজ্য; `condition` null হলে সবসময় প্রযোজ্য।

**DB flow** — `ApprovalResolver::resolve(journal)`
1. Amount = `total_debit`।
2. একটা query-তে প্রার্থী rule:

```sql
SELECT ... FROM acc_approval_rules
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` অনুযায়ী (`APR_NO_RULE` বা fallback)।
4. ধাপ পড়া: `acc_approval_rule_levels` (`level_no` ক্রমে)।
5. প্রতিটা ধাপের `condition` journal-এর line-এর সাথে মিলিয়ে বাদ বা রাখা।
6. ফল: rule, mode, প্রযোজ্য ধাপের তালিকা (ক্রম সহ)। **কোনো লেখা নেই।**

**Task scope**
1. Rule query (ওপরের SQL), ধাপ পড়া ও ছাঁকা (condition), fallback synthetic rule।
2. `ResolvedApproval` value object: rule id, version, mode, levels[] (`level_no`, approver, condition)।
3. Test।

**Out of scope**
`rule_snapshot` লেখা ও request তৈরি (`P2-08`), eligibility (`P2-09a`), rule save (`P2-06`)।

**Contract**
`ApprovalResolver::resolve(Journal): ResolvedApproval` (rule id, version, mode, levels[])

**Permission**
নেই (ভেতরের service); "Rule পরীক্ষা" endpoint `accounting.approval.manage` (`P2-06`)।

**Error**
`APR_NO_RULE` (422)

**Audit**
নেই (শুধু পড়া)।

**Acceptance**
AC2.2

**Test**
- সীমানার মান: ঠিক `max_amount`-এ কোন rule (`amount < max_amount` তাই পরের rule)।
- নির্দিষ্ট বনাম `any`।
- শর্তযুক্ত ধাপ থাকা ও না থাকা (account বা cost center line-এ আছে/নেই)।
- `block` বনাম `fallback_approver`।
- `effective_from`/`effective_to` তারিখের পরিসর।
- একই input বারবার = একই ফল।
- প্রযোজ্য ধাপ কখনো ফাঁকা নয়।
- অন্য company-র rule আসে না।

**Deliverable**
Resolver class, test।

**Sub-tasks**
rule query, ধাপ ছাঁকা, fallback, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC2.2 সবুজ (`ApprovalRoutingTest`)।
- Resolver কোনো DB write করে না।

**Architecture constraints**
- Rule-নির্বাচন একটাই জায়গায় (`P2-06`-এর "Rule পরীক্ষা" ও `P2-08` উভয়ই এটা ডাকে)।
- `CURDATE()`-নির্ভরতা কোডে একটা clock abstraction দিয়ে, যাতে test নির্ণায়ক হয়।

**Open point**
1. `fallback_approver`-এ "settings-এ নির্ধারিত approver" কোন setting key (Section ২.৪ registry-তে নেই); `P2-06`-এর Open point 2 এর সাথে একসাথে সিদ্ধান্ত।
2. Journal-এর `journal_type` (যেমন `reversal`) এর জন্য কোনো rule না থাকলে `any` rule-ই ব্যবহার হয়; যদি না থাকে `approval_no_rule_behavior`।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Feature (service + api + ui)
Title
Approval request ও সিদ্ধান্ত (Submit থেকে Approve) — Backend ও Frontend
Branch Name
`feature/accounting-approval-requests`
View Order
30
Description
**Summary**
Submit থেকে শেষ approver পর্যন্ত পুরো flow (approve, reject, withdraw, return) — যোগ্য কেউ না দিলে Post-এর দিকে যাবে না।

**Task serial**
`ACC-P2-08 — Approval request ও সিদ্ধান্ত` · Feature (service + api + ui)

**Depends on**
- `ACC-P2-07` (resolver)
- `ACC-P2-10` (SoD)
- `ACC-P2-04` (status history)
- `ACC-P2-09a` (`ApproverEligibility`)

**Relation: আগের task**
- `P2-01` দিয়েছে `JournalTransitionService` (পূর্ণ `apply` + "ভেতরের অংশ") ও transition table (`submit`, `withdraw`, `approve`, `reject`, `return`)। এই task ওই service-এর ভেতরের অংশ ডাকে (একই transaction-এ), নিজে `acc_journals.status` লেখে না।
- `P2-03` দিয়েছে `SubmitValidator` (Submit-এর ধাপ 4)।
- `P2-04` দিয়েছে history writer (প্রতিটা transition-এ row)।
- `P2-05` দিয়েছে `acc_approval_requests`, `acc_approval_actions` (`uq_acc_aact_request_level`, append-only), `rule_snapshot`।
- `P2-07` দিয়েছে `ApprovalResolver::resolve(journal)` (rule, mode, প্রযোজ্য ধাপ)।
- `P2-09a` দিয়েছে `ApproverEligibility::canAct` (ধাপ 5)।
- `P2-10` দিয়েছে `SodGuard::assert('approve', ...)`।
- `P2-02` দিয়েছে Draft editor ও Draft journal (Submit-এর উৎস)।
- `P1-09` idempotency middleware, `P1-12` error/audit।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P2-12` (Post): journal `approved` ও request `approved` শর্ত।
- `P2-09b` (delegation, override): এই flow-এর মধ্যে override ঢোকে; delegation-এ `acted_for_id` এই task-এর approve ধাপে বসে।
- `P2-15` (Queue/Register): `GET /accounting/approvals/pending`, register, stuck-তালিকা এই task-এর data ও snapshot-এর ওপর।
- `P4-08` (event-journal approval): এই flow-ই ব্যবহার, SoD শাখা যোগ।
- `P2-16`: `ApprovalFlowTest`, `ApprovalSodTest`, `ConcurrencyTest`।

**Related tables / schema**
কোনো নতুন table নেই। লেখে: `acc_approval_requests`, `acc_approval_actions`, `acc_journals` (transition service-এর মাধ্যমে), `acc_journal_status_history`, `acc_idempotency_keys`। পড়ে: `acc_approval_rules`/`acc_approval_rule_levels` (resolver-এর মাধ্যমে), `acc_approval_delegations` (`P2-09a`-এর মাধ্যমে), `acc_journal_lines`।

**`rule_snapshot` (JSON) যা Submit-এর সময় বসে** (কমপক্ষে): rule id, `rule_version`, rule-এর `updated_by` (কে শেষবার বদলেছে), mode, `self_approval`, প্রযোজ্য ধাপের তালিকা (`level_no`, `approver_role_id`/`approver_user_id`)। `rule_version` ও `updated_by` **অবশ্যই** থাকবে, কারণ `SodGuard` (`P2-10`) "rule editor cannot approve" নিয়মে এগুলো পড়ে।

**Business rules**
- Approve বা reject করার জন্য `accounting.journal.approve` permission আর ধাপের যোগ্যতা, দুটোই লাগে।
- Reject-এ মন্তব্য বাধ্যতামূলক (R2.11); `return`-এও।
- **Lock-এর ক্রম:** সবসময় `acc_journals` → `acc_approval_requests` → `acc_voucher_counters` (Blueprint-এর ক্রম) / Pack-এর সম্পূর্ণ ক্রম journal → request → account (`FOR SHARE`) → period (`FOR SHARE`) → counter। এই ক্রম ভাঙলে deadlock হয়। এই task শুধু journal তারপর request লক করে (account/period/counter Post-এর, `P2-12`)।
- Sequential: শুধু `current_level`-এর ধাপে সিদ্ধান্ত; Parallel: যেকোনো অপূর্ণ ধাপ। প্রতিটা ধাপে ঠিক একটা সিদ্ধান্ত (`uq_acc_aact_request_level` DB-তেও ঠেকায় → `APR_ALREADY_DECIDED`)।
- সব প্রযোজ্য ধাপ পূর্ণ হলে request `approved` আর journal `approved` (`approved_at`)। নইলে sequential হলে `current_level` পরের ধাপে।
- Reject হলে আর কোনো ধাপ চলে না; request `rejected`, journal `rejected`।
- Withdraw: শুধু Maker, আর request-এ কোনো action নেই; request `cancelled`, journal `draft`।
- Return: শুধু `approved` journal-এ; request `returned`, journal `rejected`, মন্তব্য বাধ্যতামূলক; কে করতে পারে: Approver বা Post-অনুমতিওয়ালা (`P2-01`)।
- **Re-submit:** `rejected` journal আবার Submit হলে নতুন `submission_no`, নতুন request, নতুন `rule_snapshot` (তখনকার rule); আগের request-এর ইতিহাস থাকে।
- Rule বদলালে চলমান request আগের snapshot-এই চলে।
- Delegate-এর জন্যও SoD খাটে (সে Maker হলে নয়)।

**DB flow: submit**
1. Idempotency (`P1-09` middleware)।
2. transaction।
3. Journal `FOR UPDATE`।
4. Submit validator (`P2-03`, `submit` mode)।
5. `ApprovalResolver::resolve` (`P2-07`)।
6. `INSERT acc_approval_requests` (`rule_snapshot` সহ, `status = 'pending'`, `current_level` = প্রথম ধাপ বা null (parallel), `total_levels`, `amount` = `total_debit`, `requested_by`)।
7. `JournalTransitionService::apply(submit)`-এর ভেতরের অংশ (`P2-01`, একই transaction): `submission_no + 1`, `submitted_at`, `submitted_by`, status `submitted`, history।
8. commit।

**DB flow: approve**
1. Idempotency।
2. transaction।
3. Journal `FOR UPDATE`, তারপর request `FOR UPDATE`।
4. Request `pending` কি না, নইলে `JRN_STATE_INVALID`।
5. কর্তা কোন ধাপের জন্য যোগ্য (`P2-09a`-এর `ApproverEligibility`)। Sequential: শুধু `current_level`। Parallel: যেকোনো অপূর্ণ ধাপ।
6. SoD (`SodGuard::assert('approve', ...)`, `P2-10`)।
7. `INSERT acc_approval_actions` (`decision = 'approve'`, delegation হলে `acted_for_id`)। 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`, মন্তব্য বাধ্যতামূলক।

**Task scope**
1. Services: `SubmitService`, `ApprovalDecisionService` (approve, reject, withdraw, return, re-submit); সবাই `P2-01` service-এর ভেতরের অংশ ডাকে।
2. `rule_snapshot` builder (উপরের field সহ)।
3. API: `POST /accounting/journals/{id}/submit`, `.../withdraw`, `.../approve`, `.../reject`, `.../return`; `GET /accounting/approvals/pending` (আমার অপেক্ষায়)।
4. UI:
   - "অপেক্ষমাণ" তালিকা: journal, amount, Maker, কোন ধাপে, কত দিন ধরে।
   - Journal দেখার পাতায় approval-এর অবস্থা: ধাপ ধরে কে করেছে, কে বাকি, মন্তব্য।
   - Approve ও Reject বাটন (`can()` দেখে), Reject-এ মন্তব্যের ঘর।
   - Maker-এর জন্য Withdraw।
5. Test।

**Out of scope**
Delegation CRUD, override, পর্যালোচনা (`P2-09b`), Post (`P2-12`), বড় Register/stuck তালিকা (`P2-15`), rule বানানো (`P2-06`)।

**Contract**
- `POST /accounting/journals/{id}/submit`, `POST .../withdraw`, `POST .../approve`, `POST .../reject` (মন্তব্য বাধ্যতামূলক), `POST .../return` (মন্তব্য বাধ্যতামূলক) — সবগুলোতে `Idempotency-Key`
- `GET /accounting/approvals/pending`
- Request body: `version`, মন্তব্য (যেখানে লাগে)।

**Permission**
- Submit/withdraw: `accounting.journal.create` (Maker, নিজের journal)।
- Approve/reject: `accounting.journal.approve` + ধাপের যোগ্যতা।
- Return: Approver (`accounting.journal.approve`) বা Post-অনুমতিওয়ালা (`accounting.journal.post`)।

**Error**
`APR_NO_RULE` (422), `APR_NOT_ELIGIBLE` (403), `APR_ALREADY_DECIDED` (409), `JRN_SOD_VIOLATION` (403), `JRN_STATE_INVALID` (409), `JRN_CONFLICT` (409)। Submit-validator-এর ত্রুটিও (`JRN_UNBALANCED` ইত্যাদি) এখানে ফেরে।

**Audit**
`accounting.journal.submitted`, `.approved`, `.rejected`, `.withdrawn` (ধাপ ও মন্তব্য সহ); ব্যর্থ/অস্বীকৃত চেষ্টা `accounting.guard.denied`।

**Acceptance**
AC2.2, AC2.3, AC2.7, AC2.10

**Test**
- সাধারণ এক ধাপ; দুই ধাপ sequential; parallel।
- দুজন একসাথে একই ধাপে (MySQL, দুই connection): একজন সফল, অন্যজন `APR_ALREADY_DECIDED` বা `JRN_CONFLICT`।
- Reject ও পুনরায় পাঠানো (নতুন `submission_no`, নতুন snapshot, আগের request-এর ইতিহাস থাকে)।
- Withdraw (সিদ্ধান্তের আগে চলে, পরে আটকায়)।
- Return (`approved` journal, মন্তব্য ছাড়া আটকায়)।
- যোগ্য নয় এমন user (`APR_NOT_ELIGIBLE`); permission আছে কিন্তু ধাপ নেই; ধাপ আছে কিন্তু permission নেই।
- Maker নিজে approve (`JRN_SOD_VIOLATION`)।
- Rule বদলের পরেও চলমান request আগের rule-এ (snapshot)।
- Rule না থাকলে `APR_NO_RULE` ও journal `draft`-এই থাকে (rollback)।
- সরাসরি API-তেও অযোগ্য user আটকায় (UI লুকানোর ওপর নির্ভর নয়)।

**Deliverable**
Service, API, অপেক্ষমাণ তালিকা ও journal-এর approval UI।

**Sub-tasks**
submit, approve, reject, withdraw, return, re-submit, pending API, UI, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC2.2, AC2.3, AC2.7, AC2.10 (এই flow-এর অংশ) সবুজ।
- Lock-ক্রম code review-তে যাচাই।
- `rule_snapshot`-এ `rule_version` ও `updated_by` আছে (test প্রমাণ)।

**Architecture constraints**
- Journal `status` লেখা শুধু `P2-01` service।
- Lock-ক্রম: journal → request → (account → period →) counter। উল্টো নয়।
- `acc_approval_actions` শুধু `INSERT`।
- Idempotency প্রতিটা write endpoint-এ।

**Open point**
1. **Lock-ক্রমের দুই রূপ:** এই card-এ ক্রম বলা "journal → request → counter" (Blueprint)। Section ৯.১ ও ১০.৩ বলছে Pack-এ ক্রম হলো journal → request → account (`FOR SHARE`) → period (`FOR SHARE`) → counter, আর "WBS বানানোর সময় এই Pack-কে সত্যের উৎস ধরতে হবে"। দুটো সঙ্গতিপূর্ণ (শুধু ধাপ 3-4 স্পষ্ট)। এই card শুধু প্রথম দুটো লক করে, তাই পার্থক্য এখানে প্রভাব ফেলে না, কিন্তু `P2-12` সম্পূর্ণ ক্রম মানবে।
2. **`return` action row:** `acc_approval_actions.decision`-এ `return` আছে, কিন্তু `uq_acc_aact_request_level (request_id, level_no)`-এর কারণে `approved` request-এর সব ধাপে আগেই `approve` row আছে। `return` কোন `level_no`-তে লেখা হবে বা আদৌ row লেখা হবে কি না source-এ নেই (flow-তে শুধু "request `returned`")। লিখিত সিদ্ধান্ত লাগবে।
3. **`self_approval` (`inherit`/`allow`/`deny`)** কীভাবে `sod_maker_cannot_approve`-এর সাথে মেলে তা source-এ নেই (`P2-06`-এর Open point 3)।
4. **`return` audit event** registry-তে নেই (`P2-01`-এর Open point 2)।
5. `GET /accounting/approvals/pending` এই card ও `P2-15` দুজনেই বলে; প্রস্তাব: এই card সহজ রূপ (নিজের অপেক্ষায়), `P2-15` delegation ও `JSON_TABLE` সহ পূর্ণ।
6. Source-এ `P2-08`-এর নির্ভরতায় `P2-02` নেই, যদিও `P2-02` "অবরুদ্ধ করে P2-08" লেখে (দেখুন `P2-02`)।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P2-12 — Posting service

Project
ERPFlow
Module
Accounting Module
Type
Feature (service + api)
Title
Posting service (লেজারে বদল আনার একমাত্র পথ) — Backend
Branch Name
`feature/accounting-posting-service`
View Order
31
Description
**Summary**
Approved journal এক transaction-এ Post হয়ে লেজারে যাবে — মাঝে fail হলে কিছুই বদলাবে না; অর্ধেক entry লেজারে থাকবে না।

**Task serial**
`ACC-P2-12 — Posting service` · Feature (service + api)

**Depends on**
- `ACC-P2-08` (approved journal ও approved request)
- `ACC-P2-10` (`SodGuard::assert('post', ...)`)
- `ACC-P2-11` (`VoucherService::next`)
- `ACC-P1-10` (DB guard trigger, `posted_hash` column)
- `ACC-P2-03` (`SubmitValidator` Post-মোডে)
- OD-5 (period lock-এর কঠোর রূপ Phase 3-এ, এখানে শুধু hook আর সতর্কতা), OD-14 (ledger-এর উৎস: ধরা হয়েছে posted line নিজেই)

**Relation: আগের task**
- `P2-08` journal `approved` ও request `approved` করে। Post এই দুটোই আবার মিলিয়ে দেখে (ধাপ 5, 7)।
- `P2-10` `SodGuard` দেয় (`sod_maker_cannot_post`; event-journal-এ Maker = `submitted_by`)।
- `P2-11` `VoucherService::next` দেয়, শুধু Post transaction-এর ভেতরে ডাকা যায়।
- `P2-03` `SubmitValidator`-এর "Post (আবার)" মোড দেয়।
- `P1-10` trigger: `approved → posted` transaction-এর ভেতরে অনুমোদিত; `posted → reversed` শুধু অনুমোদিত column সহ; `acc_journals.posted_hash CHAR(64) NULL` column।
- `P1-09`: `ck_acc_journals_posted` (posted হলে `voucher_no`, `posted_at`, `period_id` না-null), `ck_acc_journals_balanced_when_posted` (`total_debit = total_credit`), `fk_acc_journals_period`, `uq_acc_journals_company_voucher`; idempotency middleware; probe `PostedJournalProbe` (Post হলে settings lock হয়)।
- `P1-08` `PeriodResolver::forDate`, `acc_periods.status` (Phase 2-এ সবসময় `open`)।
- `P2-01` transition service (`post`, `mark_reversed`), `P2-04` history writer।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P2-13` (reversal/adjustment): reversal Post হলে ধাপ 14-এ মূল journal `reversed` হয়।
- `P2-14` (opening post): opening journal এই service-ই ব্যবহার করে।
- `P2-15` (Queue): "অনুমোদিত, Post হয়নি" তালিকা; Post-এর পর Register।
- `P2-16`: `PostPreconditionTest` (AC2.4), `PostedImmutabilityTest` (AC2.5), `PostAtomicityTest` (AC2.6), `IdempotencyTest` (AC2.11)।
- `P3-01`/`P3-02` (Phase 3): ledger query posted line পড়ে; `JournalPosted` event-এর listener `acc_periods.balance_version` বাড়ায়।
- `P3-12` (PeriodGuard-এর কঠোর রূপ): এই task-এর `PeriodGuard` hook ভরে।
- `P4-02` (Finance Event Inbox): Post-এর পর event-journal ফল; Section ৯.২ ধাপ 14: OD-11 (খ) হলে event-journal-এর জন্য commit-এর পরে `AcknowledgeEventJob` dispatch হয় (Phase 4-এর কাজ, এই task শুধু commit-এর পরের hook রাখে)।
- Phase 6: `P6-04` (VoucherService facade, ধাপ 11-এর বদলে facade ডাকা) আর `P6-10` (dimension validation, ধাপ 11a) পরে এই Post-এ hook যোগ করবে (শুধু জানিয়ে রাখা, এই task-এর scope নয়)।

**Related tables / schema**
কোনো নতুন table নেই। লেখে: `acc_journals` (ধাপ 13 ও 14), `acc_voucher_counters` (`P2-11`-এর মাধ্যমে), `acc_journal_status_history`, `activity_logs`, `acc_idempotency_keys`। পড়ে/লক করে: `acc_approval_requests`, `acc_accounts`, `acc_periods`, `acc_journal_lines`, `acc_settings`।

**Post-এ বসানো column:** `status = 'posted'`, `voucher_no`, `period_id`, `posted_at` (`NOW()`), `posted_by`, `posted_hash`, `version = version + 1`।

**Business rules (R2.12, R2.13, R2.14, R2.17, R2.18, R2.20)**
- Post লেজারে বদল আনার একমাত্র পথ; সবকিছু একটাই transaction-এ।
- কোনো ব্যর্থ ধাপে আংশিক কিছু থাকে না। Voucher নম্বর শুধু সফল Post-এ খরচ হয়।
- `status = 'posted'` আগেই হলে সেই ফলই ফেরত (R2.13), অন্য কোনো কারণে নয়।
- `approved` না হলে `JRN_NOT_APPROVED`; request-ও `approved` না হলে `JRN_NOT_APPROVED`।
- Post করার জন্য `accounting.journal.post` + `SodGuard::assert('post', ...)`; Maker নিজের journal Post করতে পারে না (`sod_maker_cannot_post`)।
- Post-এর আগে Submit-validator আবার (account active/leaf/এই company, balance, period আছে, cutover-এর তারিখ)।
- Line থেকে সরাসরি `SUM(debit)`/`SUM(credit)` header-এর `total_*`-এর সাথে মেলানো (সুরক্ষা, header ভুল হলে ধরা পড়ে)।
- `posted_hash` = journal-এর নির্দিষ্ট column আর line (`line_no` ক্রমে) নিয়ে canonical JSON-এর SHA-256।
- Reversal journal Post হলে একই transaction-এ মূল journal `posted → reversed`।
- Phase 2-এ period সবসময় `open` (বন্ধ করা Phase 3-এ)। তাই `PeriodGuard` hook আছে, কিন্তু শুধু `period_lock_mode`-এর `warn`-এর পথ চলে।

**DB flow (ধাপে ধাপে; ক্রমাঙ্ক source-এর P2-12 ধাপ, Section ৯.২-এর অতিরিক্ত লক ধাপ `*` চিহ্নিত)**

| ধাপ | কী | ব্যর্থ হলে |
| --- | --- | --- |
| 1 | Idempotency (`P1-09`): মূল transaction-এর **আগে**, ছোট আলাদা transaction-এ `acc_idempotency_keys` row (`processing`) | আগের ফল ফেরত |
| 2 | transaction শুরু |  |
| 3 | `SELECT ... FROM acc_journals WHERE id = ? AND company_id = ? FOR UPDATE` (লক-ক্রম 1) |  |
| 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` (লক-ক্রম 2), `status = 'approved'` মিলিয়ে | `JRN_NOT_APPROVED` |
| 7* | `acc_accounts` `WHERE id IN (...) ORDER BY id FOR SHARE` (লক-ক্রম 3), active ও leaf দেখা (Section ৯.২ ধাপ 3) | `JRN_ACCOUNT_INVALID` |
| 8 | `SubmitValidator` Post-মোডে আবার (P2-03-এর Post কলাম): account active কি না, balance, period আছে কি না, তারিখ | `JRN_ACCOUNT_INVALID`, `JRN_UNBALANCED`, `JRN_DATE_INVALID` |
| 9 | `PeriodGuard::check(company, journal_date)`; `acc_periods` `FOR SHARE` (লক-ক্রম 4, Section ৯.২ ধাপ 4) | `JRN_PERIOD_LOCKED` |
| 10 | Line থেকে সরাসরি `SELECT SUM(debit), SUM(credit)` আর header-এর `total_*` মিলিয়ে দেখা | `JRN_UNBALANCED` |
| 11 | `VoucherService::next` (`P2-11`, লক-ক্রম 5) |  |
| 12 | `posted_hash` = canonical JSON-এর SHA-256 (journal-এর নির্দিষ্ট column + line, `line_no` ক্রমে) |  |
| 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` | `JRN_CONFLICT` |
| 14 | `journal_type = 'reversal'` হলে একই transaction-এ মূল journal `posted → reversed` (`reversed_at`, `reversed_by_journal_id`; `P2-13`, DB guard-এর অনুমোদিত পথ; লক-ক্রম 6: দ্বিতীয় journal `FOR UPDATE`, শেষে) | `JRN_STATE_INVALID` |
| 15 | `acc_journal_status_history` (reversal হলে দুটো row: প্রতিটা journal-এর জন্য), audit `accounting.journal.posted`, `accounting.voucher.assigned` |  |
| 16 | commit (সব lock ছাড়ে) |  |
| 17 | commit-এর পরে (`afterCommit`): `JournalPosted` event, যা পরে (`P3-02`) derived cache `stale` করে। Phase 2-এ কেউ শোনে না |  |
| — | Idempotency `done` + response সংরক্ষণ (Section ৯.২ ধাপ 13) |  |

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

**`PeriodGuard::check`:** `PeriodResolver::forDate` দিয়ে period। Period `closed` আর mode `strict` হলে `JRN_PERIOD_LOCKED`। `warn` হলে সতর্কতা ফেরত আর `accounting.period.locked_post_attempt` audit। Phase 3-এ (`P3-12`) পূর্ণ রূপ।

**Task scope**
1. `PostingService::post(journalId, actor, idempotencyKey)` ধাপ 1-17।
2. `PeriodGuard` hook (এখানে শুধু `warn` পথ, `strict`+`closed` শাখাও কোডে থাকবে কিন্তু Phase 2-এ সক্রিয় হয় না কারণ period `open`)।
3. Canonical hash builder (`PostedHashBuilder`), পরে `P3-13` (RC9) একই builder দিয়ে যাচাই করবে।
4. `POST /accounting/journals/{id}/post` (Idempotency key বাধ্যতামূলক)।
5. `JournalPosted` event dispatch (`afterCommit`)।
6. Failure-injection test helper (প্রতিটা ধাপের পর জোর করে exception)।

**Out of scope**
Voucher কোড (`P2-11`), reversal তৈরি (`P2-13`), period বন্ধ/খোলা (`P3-11`), `PeriodGuard`-এর পূর্ণ strict রূপ (`P3-12`), event-এর listener (`P3-02`)।

**Contract**
- `POST /accounting/journals/{id}/post` (header: `Idempotency-Key` বাধ্যতামূলক; body: `version`)।
- Response: journal (voucher_no, period, posted_at, posted_by), `warnings[]` (period `warn`)।
- `PostingService::post(...)`; `PeriodGuard::check(company, date)`; `JournalPosted` event।

**Permission**
`accounting.journal.post`

**Error**
`JRN_NOT_APPROVED` (409), `JRN_SOD_VIOLATION` (403), `ACC_FORBIDDEN` (403), `JRN_ACCOUNT_INVALID` (422), `JRN_UNBALANCED` (422), `JRN_DATE_INVALID` (422), `JRN_PERIOD_LOCKED` (409), `JRN_CONFLICT` (409), `SET_INCOMPLETE` (`voucher_format` নেই)

**Audit**
`accounting.journal.posted`, `accounting.voucher.assigned`, ব্যর্থ চেষ্টাও।

**Acceptance**
AC2.4, AC2.5, AC2.6, AC2.7, AC2.9, AC2.11

**Test**
- সফল Post।
- দুইবার Post চাপা (আগের ফল, একই idempotency key এবং ভিন্ন key দুই ক্ষেত্রেই)।
- দুজন একসাথে Post (MySQL, দুই connection): একজন সফল, অন্যজন আগের ফল বা `JRN_CONFLICT`।
- ধাপ 11-র পরে জোর করে exception: journal `approved`-এ, counter আগের মানে, কোনো line বদলায়নি, history/audit নেই।
- ধাপ 13 ব্যর্থ (version মেলেনি)।
- Account inactive হয়ে যাওয়া journal (`JRN_ACCOUNT_INVALID`)।
- Header `total_*` ভুল করা journal (সরাসরি SQL দিয়ে) = ধাপ 10 `JRN_UNBALANCED`।
- SoD: Maker নিজের journal Post (`JRN_SOD_VIOLATION`), event-journal-এ `submitted_by`।
- Request `approved` নয় (`JRN_NOT_APPROVED`)।
- Reversal journal Post হলে মূল `reversed`; reversal Post ব্যর্থ হলে মূল `posted`-ই।
- Post-এর পর সরাসরি SQL-এ `UPDATE`/`DELETE` ব্যর্থ (`JRN_IMMUTABLE`, AC2.5)।
- `posted_hash` নির্ণায়ক (একই journal একই hash)।
- `warn` মোডে `period_lock_mode` সতর্কতা ও audit।

**Deliverable**
`PostingService`, API, `PeriodGuard`-এর hook, canonical hash।

**Sub-tasks**
service, ধাপ ১ থেকে ১৭, hash, `PeriodGuard` hook, API, failure-injection test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC2.4, AC2.5, AC2.6, AC2.7, AC2.9, AC2.11 সবুজ (MySQL, একাধিক connection)।
- Golden suite-এর (Blueprint ১২.২) Post-সংক্রান্ত test সবুজ।
- সম্পূর্ণ লক-ক্রম code review-তে যাচাই।

**Architecture constraints**
- লেজারে (posted line) বদল আনার একমাত্র পথ এই service।
- সবকিছু একটাই transaction-এ; commit-এর পরে শুধু event dispatch (Post-এ কোনো hot-row বাড়ানো নয়, `balance_version` commit-এর পরে বাড়ে)।
- Voucher counter আলাদা transaction-এ নয়।

**Open point**
1. **ধাপের ক্রম:** source-এর P2-12 table-এ account ও period লক (Section ৯.২ ধাপ 3, 4) নেই; Section ৯.১/৯.২ আর ১০.৩-এ আছে ("Post-এর সময় account বা period বদল ঠেকাতে")। এই ফাইলে দুটোই একত্র করা হয়েছে (7* ও 9), Pack-কে সত্যের উৎস ধরে। Tech lead নিশ্চিত করবে।
2. **`status` লেখা:** ধাপ 13 সরাসরি `UPDATE` লেখে, অথচ `P2-01` বলে "শুধু transition service status লেখে"। প্রস্তাব: ধাপ 13 ও 14 উভয়ই transition service-এর ভেতরের অংশ দিয়ে (`P2-01` Open point 4)।
3. **Canonical hash-এর column তালিকা** ("নির্দিষ্ট column") source-এ নেই। এটা `P3-13` (RC9)-এর সাথে একত্রে ঠিক করতে হবে।
4. **Post-মোড validator বনাম ধাপ 8/9 ক্রম:** Section ৯.২ অনুযায়ী `FOR SHARE` account লকের পর validator; কোড একই query-তে account পড়বে কি না (দুবার নয়) implementation সিদ্ধান্ত।
5. ধাপ 17 `JournalPosted` event-এর payload source-এ নেই (প্রস্তাব: `company_id`, `journal_id`, `period_id`)।

Owner
Dev A
Estimate Hours
6
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service + ui)
Title
Delegation, emergency override ও পর্যালোচনা — Backend ও Frontend
Branch Name
`feature/accounting-delegation-override`
View Order
32
Description
**Summary**
ছুটিতে approver-এর কাজ delegate করা, জরুরিতে controlled override, আর প্রতিটা override-এর পর আলাদা reviewer — তিনটাই Head of Finance-এর হাতে থাকবে।

**Task serial**
`ACC-P2-09b — Delegation, emergency override ও পর্যালোচনা` · Feature (schema + service + ui)

**Depends on**
- `ACC-P2-08` (approval flow, request ও actions)
- (Master sequence-এ শুধু `P2-08` লেখা; বাস্তবে `P2-05`, `P2-09a`, `P2-10` আগেই হয়ে আছে বলে ধরা)

**Relation: আগের task**
- `P2-05` দিয়েছে `acc_approval_delegations` (schema, check, index) ও `acc_approval_actions` (`decision = 'override'` সহ)। এই task `acc_approval_override_reviews` যোগ করে।
- `P2-09a` দিয়েছে `ApproverEligibility` (delegation পড়ে)। এই task delegation **লেখে**। Delegation তৈরি/শেষ হলে `ApproverEligibility`-র ফল বদলায়।
- `P2-08` দিয়েছে approve flow ও lock-ক্রম, request/journal state। Override ওই flow-র একটা শর্টকাট।
- `P2-10` দিয়েছে `SodGuard`: "Maker নিজের journal override করতে পারে না" ও "Override-কারী নিজের override পর্যালোচনা করতে পারে না" (দুটো সবসময় চালু)।
- `P2-01` transition service: override `submitted → approved` (approve কাজের পথে)।
- `P1-10` `Schema::appendOnly` helper: `acc_approval_override_reviews` append-only।
- `P1-02` slug: `accounting.journal.override`, `accounting.approval.manage`, `accounting.journal.approve`।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P2-15` (Queue/Register): "অপর্যালোচিত override" (Head of Finance) তালিকা `GET /accounting/approvals/overrides/unreviewed` এই task-এর data পড়ে; Approval Register-এ override দেখায়।
- `P2-16`: `OverrideTest`, `ApprovalSodTest` (AC2.3), `AuditHistoryTest`।
- `P5-03` (Monitoring ও alert): অপর্যালোচিত override নিয়ে alert (পরের Phase)।
- `P5-01` (UAT): U06 "Delegation ও emergency override ও পর্যালোচনা"।

**Related tables / schema**

**`acc_approval_override_reviews`** (নতুন; append-only)

| 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 (`Schema::appendOnly`)। Composite FK ও company নিয়ম সাধারণ নিয়ম অনুযায়ী।

আরও লেখে/পড়ে: `acc_approval_delegations` (`P2-05`), `acc_approval_actions` (override row), `acc_approval_requests`, `acc_journals` (transition service-এর মাধ্যমে), `acc_journal_status_history`।

**Business rules**
- **Delegation তৈরি:** `POST /accounting/approval-delegations`। যাচাই: delegate-এর `accounting.journal.approve` permission আছে, তারিখ বৈধ (`valid_to >= valid_from`), `delegator_id <> delegate_id` (DB check-ও)। Delegator নিজে বা Admin (`accounting.approval.manage`) তৈরি করতে পারে।
- **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-এ।
- Delegate-এর জন্যও SoD খাটে (সে Maker হলে নয়)।
- Delegation মেয়াদ পেরোলে স্বয়ংক্রিয় অকার্যকর (eligibility আজকের তারিখ দেখে); নিজে শেষ করার সময় `is_active = 0` (`accounting.approval.delegation_ended` audit)।
- `acc_approval_actions`-এ override row-এর `comment` বাধ্যতামূলক।

**DB flow: override**
1. Idempotency।
2. transaction।
3. Journal `FOR UPDATE`, তারপর request `FOR UPDATE` (লক-ক্রম)।
4. Request `pending` কি না (নইলে `JRN_STATE_INVALID`); `accounting.journal.override` permission (নইলে `ACC_FORBIDDEN`); `SodGuard` (Maker হলে `JRN_SOD_VIOLATION`)।
5. `INSERT acc_approval_actions` (`decision = 'override'`, মন্তব্য/কারণ)।
6. Request `approved` (`decided_at`) আর journal `submitted → approved` (`P2-01`, একই transaction)।
7. History, audit `accounting.approval.override_used`।
8. commit।

**DB flow: পর্যালোচনা** — transaction; override action row পড়া; `SodGuard` (reviewer ≠ override-কারী, ≠ Maker); `INSERT acc_approval_override_reviews` (`uq_acc_aor_action` দুইবার পর্যালোচনা ঠেকায়); audit `accounting.approval.override_reviewed`; commit।

**DB flow: delegation** — `INSERT acc_approval_delegations`; audit `accounting.approval.delegated`; শেষ করা `UPDATE is_active = 0`, audit `.delegation_ended`।

**Task scope**
1. Migration `acc_approval_override_reviews` (+ `Schema::appendOnly`)।
2. Delegation CRUD (তৈরি, তালিকা, শেষ করা) service + API।
3. Override flow (service + `POST /accounting/journals/{id}/override`)।
4. পর্যালোচনা service + API।
5. UI ×3: delegation পাতা (তালিকা + তৈরি/শেষ); journal পাতায় override বাটন/dialog (কারণ বাধ্যতামূলক); পর্যালোচনা পাতা (অপর্যালোচিত override-এর তালিকা থেকে accept/flag)।
6. Test।

**Out of scope**
`ApproverEligibility` নিজে (`P2-09a`), পূর্ণ Register/unreviewed তালিকা API (`P2-15`), alert (`P5-03`)।

**Contract**
- `POST /accounting/approval-delegations`; (তালিকা ও শেষ করার endpoint source-এ নাম নেই — Open point)
- `POST /accounting/journals/{id}/override` (body: `reason`, `version`; `Idempotency-Key`)
- পর্যালোচনা endpoint: source-এ নাম নেই (Open point)

**Permission**
- Delegation: Delegator নিজে (`accounting.journal.approve`-ওয়ালা) বা `accounting.approval.manage`।
- Override: `accounting.journal.override`।
- পর্যালোচনা: source-এ নির্দিষ্ট slug নেই (Open point); তালিকা দেখে Head of Finance।

**Error**
`APR_NOT_ELIGIBLE` (403), `JRN_SOD_VIOLATION` (403), `ACC_FORBIDDEN` (403); এ ছাড়া `JRN_STATE_INVALID`, `JRN_CONFLICT` (অন্তর্নিহিত)।

**Audit**
`accounting.approval.delegated`, `.delegation_ended`, `.override_used`, `.override_reviewed`

**Acceptance**
AC2.3, AC2.10

**Test**
- যোগ্য delegate approve করতে পারে (`acted_for_id` বসে)।
- মেয়াদ পেরোনো delegation (পারে না)।
- Maker-ই delegate (SoD আটকায়)।
- Override কারণ ছাড়া (আটকায়)।
- Maker-এর override (আটকায়, `sod_maker_cannot_approve` বন্ধ থাকলেও)।
- পর্যালোচনা নিজের override-এ (নিষেধ); Maker-এর পর্যালোচনা (নিষেধ); দ্বিতীয়বার পর্যালোচনা (`uq_acc_aor_action`)।
- Delegation তৈরি: delegate-এর `accounting.journal.approve` permission নেই = আটকায়।
- Override সফল হলে request `approved`, journal `approved`, এরপর Post করা যায়।

**Deliverable**
Migration, eligibility service-এর সাথে সংযুক্ত delegation API ও UI, override flow ও পর্যালোচনা UI।

**Sub-tasks**
migration, delegation CRUD, override, review, UI ×3, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC2.3 (override/delegation অংশ), AC2.10 সবুজ।
- অপর্যালোচিত override `P2-15` তালিকায় আসে।

**Architecture constraints**
- Override আলাদা permission ও কঠোর SoD ছাড়া নয়; কোনো setting দিয়ে Maker-override খোলা যাবে না।
- Override ও পর্যালোচনা append-only, মোছা/সম্পাদনা নেই।
- Lock-ক্রম মানতে হবে (journal → request)।

**Open point**
1. `override` action row-র `level_no` কত হবে (বাকি সব ধাপ "পূর্ণ ধরা হয়" কিন্তু `uq_acc_aact_request_level` এক ধাপে এক row)? `current_level` ধরার প্রস্তাব, source-এ নেই।
2. `override_reason_required` false হলে `acc_approval_actions.comment` বাধ্যতামূলক নিয়মের সাথে সংঘাত (schema: override-এ comment বাধ্যতামূলক)।
3. Delegation তালিকা/শেষ করার ও পর্যালোচনা endpoint নাম এবং পর্যালোচনার permission slug।
4. Override কি শুধু `submitted` (pending request) journal-এ, নাকি `draft`-এ (প্রথম ধাপের আগে)-ও? Source বলে "request-এর বাকি সব ধাপ", অর্থাৎ pending request আছে এমন journal; এই ফাইলে সেটাই ধরা হয়েছে।
5. Source Master sequence-এ `P2-09b` শুধু `P2-08`-এর ওপর নির্ভর করে; বাস্তবে `P2-05`/`P2-09a`/`P2-10` আগেই শেষ।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P2-13 — Reversal ও Adjustment

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service + api + ui)
Title
Reversal ও Adjustment — Backend ও Frontend
Branch Name
`feature/accounting-reversal-adjustment`
View Order
33
Description
**Summary**
Post হয়ে গেলে ভুল ঠিক করতে মূল journal ছোঁয়া যাবে না — Reversal (পুরো উল্টো) বা Adjustment (শুধু পার্থক্য); লেজারের ইতিহাস অক্ষত থাকবে।

**Task serial**
`ACC-P2-13 — Reversal ও Adjustment` · Feature (schema + service + api + ui)

**Depends on**
- `ACC-P2-12` (Posting service: reversal Post হলে মূল journal `reversed` হয়)
- OD-15 (বন্ধ period-এ reversal-এর তারিখ। Phase 2-এ period সব `open`, তাই `reversal_default_date` setting-ই চলে)

**Relation: আগের task**
- `P2-12` দিয়েছে Post, আর তার ধাপ 14 ("`journal_type = 'reversal'` হলে একই transaction-এ মূল journal `posted → reversed`")। এই task সেই পথ ব্যবহারের জন্য reversal Draft বানায়।
- `P2-03` দিয়েছে validator: Reversal/Adjustment-এর Draft Submit-এ একই `SubmitValidator`-এ যায় (inactive account হলে `JRN_ACCOUNT_INVALID`)। Source `P2-03`-কে `P2-13`-এর "অবরুদ্ধ করে" তালিকায় রেখেছে (যদিও `P2-13`-এর নির্ভরতায় নেই)।
- `P2-01` transition table: `mark_reversed` শুধু system; এই task-এর Draft সাধারণ Draft।
- `P2-02` Draft editor: তৈরির পর নতুন Draft এখানে খোলে।
- `P2-08` Submit/approve, `P2-10` SoD সাধারণ পথে চলে।
- `P1-09` দিয়েছে `acc_journals.reverses_journal_id`, `adjusts_journal_id`, `reversal_reason`, composite FK `fk_acc_journals_reverses`/`fk_acc_journals_adjusts`, `ck_acc_journals_reversal` (`reverses_journal_id IS NULL` অথবা `journal_type = 'reversal'`; অনুরূপ `adjusts`)।
- `P1-10` trigger: মূল journal-এ শুধু অনুমোদিত column বদলে `posted → reversed` চলে।
- `P1-11`/`P2-14`: opening journal reversal নিষেধ, শুধু Adjustment।
- `P1-03`: `reversal_default_date` (`today`/`original`/`ask`, default `today`)।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P2-16` (test): `ReversalTest` (AC2.8)।
- `P3-12` (PeriodGuard strict): "Reversal-এর তারিখ ঠিক করার সময় (`P2-13`): `reversal_default_date` অনুযায়ী পাওয়া তারিখ `strict` ও `closed` period-এ পড়লে `JRN_PERIOD_LOCKED`" — এই hook `P3-12` যোগ করবে।
- `P3-11` (period বন্ধ): reversal pending থাকলে `no_pending_journals` যাচাই।
- `P3-03` (GL): মূল আর reversal দুটোই ledger-এ (R3.2), নিট প্রভাব শূন্য; `reversed` অবস্থার journal আসলে posted।
- `P4-10`/RC8 আর `P3-13` সংশোধনের শৃঙ্খল দেখে।

**Related tables / schema**

**Schema যোগ (expand migration) `acc_journals`-এ:** `reversal_active` একটা generated stored column:

```sql
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 বানানো যায়।

লেখে: `acc_journals` (নতুন Draft), `acc_journal_lines`, `acc_journal_status_history`; পড়ে: মূল journal ও line।

**Business rules (R2.15, R2.16)**
- Reversal: মূল journal `posted` হতে হবে; `journal_type = 'opening'` হলে নিষেধ (opening-এর সংশোধন শুধু Adjustment, `P2-14`)।
- একটা journal-এর একটাই সক্রিয় reversal (`JRN_ALREADY_REVERSED`, DB unique-ও আটকায়)।
- Reversal-এর প্রতিটা line একই account-এ, Debit আর Credit অদলবদল।
- তারিখ: `date` দিলে সেটা, নইলে `reversal_default_date` (`today`, `original`, `ask` হলে `date` বাধ্যতামূলক)।
- `reversal_reason` বাধ্যতামূলক (`reason`)।
- Reversal ও Adjustment-এর company মূলের company-ই (composite FK ও validator)।
- Reversal-এর account-গুলো আজ inactive হলেও Submit-এর validator আটকাবে (`JRN_ACCOUNT_INVALID`)। তখন Admin account সাময়িক চালু করে বা Adjustment দিয়ে অন্য পথে যেতে হবে। এটা **ইচ্ছাকৃত**, চুপচাপ অবৈধ account-এ post নয়।
- Adjustment: `journal_type = 'adjustment'`, `adjusts_journal_id`, line ব্যবহারকারীর দেওয়া (শুধু পার্থক্য)। মূল journal-এ একাধিক adjustment চলে।
- একটা posted journal-এ আগে adjustment থাকলে reversal-এর সময় সতর্কতা (reversal পুরো মূল amount-ই উল্টায়, adjustment-এর প্রভাব নয়)।
- Reversal ও Adjustment দুটোই সাধারণ Draft, তাই Submit, approve, post (`P2-08`, `P2-12`)। এই task Post করে না।
- Reversal Post হলে মূল journal `reversed` চিহ্ন পায় (`P2-12` ধাপ 14)। Reversal reject বা বাতিল হলে মূল `posted`-ই থাকে।

**DB flow: reversal**
1. Idempotency।
2. transaction।
3. মূল journal `FOR UPDATE`।
4. `status = 'posted'` না হলে `JRN_STATE_INVALID`। `journal_type = 'opening'` হলে reversal নিষেধ (`JRN_STATE_INVALID`)।
5. আগে থেকে সক্রিয় reversal থাকলে `JRN_ALREADY_REVERSED` (DB-র unique-ও আটকায়)।
6. তারিখ: `date` দিলে সেটা, নইলে `reversal_default_date` (`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` (`create`)।
11. commit। **এখানে Post হয় না।**

**DB flow: adjustment** — একই কাঠামো। `journal_type = 'adjustment'`, `adjusts_journal_id`, line ব্যবহারকারীর দেওয়া (শুধু পার্থক্য)। মূল journal-এ একাধিক adjustment চলে। একটা posted journal-এ আগে adjustment থাকলে reversal-এর সময় সতর্কতা।

**Task scope**
1. Migration: `reversal_active` generated column + `uq_acc_journals_reversal` (expand-only)।
2. `ReversalService::create(journalId, reason, date?)`, `AdjustmentService::create(journalId, reason, lines[])`।
3. API:
   - `POST /accounting/journals/{id}/reverse` (`reason` বাধ্যতামূলক, `date` ঐচ্ছিক)
   - `POST /accounting/journals/{id}/adjust` (`reason`, `lines[]`)
4. UI: Posted journal-এর পাতায় "Reverse" ও "Adjust" বাটন (`journal.reverse` permission আর `can()` দেখে)। Reverse dialog: কারণ (বাধ্যতামূলক), তারিখ, উল্টানো line-এর পূর্বরূপ। তৈরির পর নতুন Draft-এ নিয়ে যায়। মূল আর reversal পরস্পরের সাথে লিংক করা, `reversed` ব্যাজ।
5. Test।

**Out of scope**
Reversal/Adjustment-এর Submit/approve/post (`P2-08`, `P2-12`), period `closed` যাচাই (`P3-12`), auto-reversal / recurring (পরের Phase)।

**Contract**
- `POST /accounting/journals/{id}/reverse` — body: `reason` (বাধ্যতামূলক), `date` (ঐচ্ছিক; `ask` হলে বাধ্যতামূলক); `Idempotency-Key`; response: নতুন Draft journal।
- `POST /accounting/journals/{id}/adjust` — body: `reason`, `lines[]`; response: নতুন Draft।
- `JournalTransitionService::can(journal, 'reverse'/'adjust', actor)` UI-র জন্য (বা সমতুল্য; `can()`-এ এই দুটো action যোগ হবে কি না Open point)।

**Permission**
`accounting.journal.reverse` (Reversal ও Adjustment শুরু)। পরে Submit/approve/post-এর permission সাধারণ।

**Error**
`JRN_ALREADY_REVERSED` (409), `JRN_STATE_INVALID` (409), `ACC_FORBIDDEN` (403), `JRN_ACCOUNT_INVALID` (422)

**Audit**
`accounting.journal.reversal_created`, `.adjustment_created`, `.marked_reversed`

**Acceptance**
AC2.8

**Test**
- সফল reversal ও তার post-এর পর মূল `reversed` ও মূল line অক্ষত।
- দুবার reverse (DB আটকায়: সরাসরি SQL-এ দ্বিতীয় reversal row ঢোকানোর চেষ্টাও `uq_acc_journals_reversal` ভাঙে)।
- Reversal বাতিলের পর আবার reverse করা যায়।
- Opening reverse (নিষেধ)।
- Adjustment একাধিক।
- Reversal-এর পরে ভুল account inactive (Submit-এ `JRN_ACCOUNT_INVALID`)।
- `reversal_default_date` তিন মান (`today`, `original`, `ask` + `date` ছাড়া আটকায়)।
- Reversal reject/cancel হলে মূল `posted`-ই থাকে।
- অন্য company-র journal reverse করা যায় না (IDOR + composite FK)।
- আগে adjustment থাকা journal-এ reversal-এ সতর্কতা।
- Reversal Post-এ ব্যর্থতা হলে মূল `posted`-ই থাকে (atomic, `P2-12` ধাপ 14)।

**Deliverable**
Migration, service, API, UI।

**Sub-tasks**
migration, reversal, adjustment, API, UI dialog, link ও ব্যাজ, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC2.8 সবুজ (`ReversalTest`: মূল অক্ষত, চিহ্ন বসা, জোড়া)।
- OD-15 লিখিত (Phase 2-এ `reversal_default_date` যথেষ্ট)।
- মূল journal কোনোভাবে মূল Phase-এ বদলায় না (সরাসরি SQL-এও ব্যর্থ, `P1-10`)।

**Architecture constraints**
- মূল journal-এর কোনো column এই task বদলায় না; `reversed` চিহ্ন বসে শুধু `P2-12`-এর ধাপ 14 দিয়ে।
- Reversal ও Adjustment সবসময় সাধারণ Draft → সাধারণ approval। কোনো শর্টকাট নেই।
- `reversal_active` generated column-টাই একমাত্র "একটাই সক্রিয় reversal"-এর প্রয়োগ, application-যাচাই শুধু ভালো বার্তার জন্য।

**Open point**
1. Adjustment-এর ক্ষেত্রে মূল journal `posted` কি না যাচাই (সম্ভবত হ্যাঁ, "একই কাঠামো") এবং মূল `reversed` হলে adjustment চলবে কি না source-এ নেই।
2. `reversal_default_date = 'original'` হলে মূলের তারিখ বন্ধ period-এ পড়লে Phase 2-এ সমস্যা নেই (সব `open`), Phase 3-এ `P3-12` সামলাবে।
3. `can()`-এ `reverse`/`adjust` action যোগ হবে কি না (transition table-এ এগুলো নেই, কারণ এগুলো নতুন journal বানায়, মূলের status বদলায় না)। প্রস্তাব: `JournalActionPolicy` বা `can()`-এর বর্ধিত রূপ।
4. Reversal-এর reversal (reversal journal Post হওয়ার পর সেটাকে আবার reverse করা) নিষিদ্ধ কি না source-এ নেই।
5. `JRN_ALREADY_REVERSED` বনাম `JRN_STATE_INVALID` যখন মূল ইতিমধ্যে `reversed`: ধাপ 4 আগে ঘটে, তাই `reversed` অবস্থার journal-এ `JRN_STATE_INVALID` (আর `JRN_ALREADY_REVERSED` যখন সক্রিয় reversal Draft/pending আছে অথচ মূল এখনো `posted`)। ব্যবহারকারীর বার্তা সুবিধার জন্য tech lead/product-এর কাছে নিশ্চিত করো।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P2-14 — Opening journal-এর Post

Project
ERPFlow
Module
Accounting Module
Type
Feature (service)
Title
Opening journal-এর Post ও lock চালু — Backend
Branch Name
`feature/accounting-opening-post`
View Order
34
Description
**Summary**
Phase 1-এর opening Draft সাধারণ Submit → approve → post পথে Post হবে; Post-এর পর settings lock চালু আর Setup Checklist (`S10`) সবুজ।

**Task serial**
`ACC-P2-14 — Opening journal-এর Post` · Feature (service)

**Depends on**
- `ACC-P1-11` (Opening balance Draft)
- `ACC-P2-12` (Posting service)

**Relation: আগের task**
- `P1-11` বানিয়েছে opening Draft: একটা company-র একটাই সক্রিয় opening journal (`uq_acc_journals_opening`), `journal_date` = `opening_cutover_date`, `PUT /accounting/opening-balance` (পুরো line তালিকা, না মিললে সংরক্ষণই হয় না), `DELETE` (বাতিল)। Post হলে `PUT` আর চলে না (`JRN_STATE_INVALID`, সংশোধন Adjustment-এ)।
- `P2-12` দিয়েছে Posting service; `P2-08` approval flow; `P2-10` SoD। Opening journal এই সাধারণ পথে চলে, কোনো বিশেষ post পথ নেই।
- `P1-09` দিয়েছে `PostedJournalProbe::hasPosted(companyId)` আসল implementation (`SELECT EXISTS(SELECT 1 FROM acc_journals WHERE company_id = ? AND status = 'posted')`) যা `P1-03` settings lock ব্যবহার করে। তাই Opening Post হলেই lock আপনা-আপনি চালু হয়, নতুন কোড লাগে না।
- `P1-05` দিয়েছে `ChecklistService::evaluate` ও ধাপ `S10_opening` ("Opening journal post হয়েছে বা Admin স্পষ্ট করে এড়িয়েছে")।
- `P1-03` দিয়েছে `opening_cutover_date` (lock = হ্যাঁ, কিন্তু Phase 1-এ শুধু posted থাকলে lock)।
- `P2-13` সিদ্ধান্ত নিয়েছে: Opening Reversal নিষেধ, শুধু Adjustment।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P2-16` (test): `OpeningPostTest` (AC2.12)।
- `P5-02` (Cutover মহড়া): opening post প্রক্রিয়া মহড়ায় চলে।
- `P5-06` (Release gate / flag চালু): `S10` সবুজ লাগে।
- `P3-04`/`P3-06` (Trial Balance / Balance Sheet): opening posted line ledger-এ শুরুর balance।
- Phase 6 `P6-10`, `P6-12` এই task-এর ওপর নির্ভর করে (শুধু জানিয়ে রাখা)।

**Related tables / schema**
কোনো নতুন table নেই। পড়ে `acc_journals` (`journal_type = 'opening'`, `journal_date`), `acc_settings` (`opening_cutover_date`), `acc_periods`। লেখে (অন্য card-এর মাধ্যমে) `acc_journals`, `acc_approval_requests`, `acc_approval_actions`, `acc_setup_status` (checklist evaluate)।

**Business rules**
- Opening journal সাধারণ Submit, approve, post পথ ধরে চলে (`journal_type = 'opening'`; approval rule `opening` বা `any` ধরনের থাকতে হবে, নইলে `APR_NO_RULE`)।
- **Submit ও Post দুই জায়গাতেই** `journal_date` আবার `opening_cutover_date`-এর সাথে মেলানো। Draft আর Post-এর মাঝে setting বদলালে `JRN_DATE_INVALID` (ইচ্ছাকৃত; Phase 1-এ cutover তারিখ lock হয় না, শুধু posted থাকলে lock)।
- Post হলে `PostedJournalProbe::hasPosted` সত্য, তাই lock হওয়া সব setting এখন থেকে `SET_LOCKED` (Section ৩.৩)।
- Post-এর পর `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` ব্যবহার। শুধু (ক) Submit ও Post-এ অতিরিক্ত যাচাই: `journal_type = 'opening'` হলে `journal_date = opening_cutover_date` (নইলে `JRN_DATE_INVALID`); (খ) Post-এর পর checklist trigger।

**Task scope**
1. Date-যাচাই Submit-এ (`SubmitValidator`-এ opening শাখা) ও Post-এ (`P2-12` ধাপ 8-এ) — দুই জায়গায় একই নিয়ম-class।
2. Post-এর পর checklist trigger: `ChecklistService::evaluate(companyId)` (শুধু opening journal Post হলে)।
3. Test।

**Out of scope**
Opening Draft (`P1-11`), reversal নিষেধ (`P2-13`-এর), Cutover মহড়া (`P5-02`)।

**Contract**
নতুন public contract নেই। Hook: opening-journal-posted → `ChecklistService::evaluate`।

**Permission**
সাধারণ: Submit (`journal.create`), approve (`journal.approve`), post (`journal.post`)।

**Error**
`JRN_DATE_INVALID`, `SET_LOCKED` (পরবর্তী setting বদলে), `APR_NO_RULE`, সাধারণ সব

**Audit**
সাধারণ `accounting.journal.*`; `accounting.setup.step_changed` (S10 বদলালে)।

**Acceptance**
AC2.12

**Test**
- Opening Post-এর পর `SET_LOCKED` (lock-ওয়ালা setting বদলানোর চেষ্টা)।
- Post-এর আগে cutover বদলানো আর Post চেষ্টা (`JRN_DATE_INVALID`); Submit-এ আগেই ধরা।
- Opening reverse (নিষেধ)।
- Adjustment চলে।
- `S10` সবুজ (Post-এর পর)।
- Opening Draft-এর `period_id` Post-এ cutover-এর period।
- Opening journal এর SoD (Maker নিজে Post নয়)।

**Deliverable**
Date-যাচাই, post-এর পর checklist trigger, test।

**Sub-tasks**
date check Submit ও Post-এ, checklist trigger, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC2.12 সবুজ (`OpeningPostTest`)।
- Opening Post-এ আলাদা কোড-পথ নেই (code review)।

**Architecture constraints**
- Opening journal-এর জন্য কোনো SoD/approval ছাড় নয়।
- Cutover তারিখের সমতা-যাচাই শুধু `journal_type = 'opening'`-এর জন্য; অন্য journal-এ "cutover-এর আগে নয়" (R2.5) নিয়মই থাকে।

**Open point**
1. `P2-12` বলে `JournalPosted` event "Phase 2-এ কেউ শোনে না", কিন্তু এই card-এর (4) trigger লাগে। প্রস্তাব: opening journal-এর জন্য `afterCommit` listener (কেবল `journal_type = 'opening'`), নইলে `PostingService`-এ সরাসরি call। লিখিত সিদ্ধান্ত লাগবে।
2. `P2-03`-এর নিয়ম "তারিখ cutover-এর আগে নয়" আর এই card-এর "মেলানো" (সমান) আলাদা; এই ফাইলে opening-এর জন্য সমান (`=`), বাকিদের জন্য `>=` ধরা হয়েছে।
3. Section ৩.৩ (lock তালিকা) এই ডকুমেন্টে পাওয়া যায়নি; কোন setting lock হয় তা `P1-03` registry-র Lock কলাম।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P2-15 — Queue ও Register

Project
ERPFlow
Module
Accounting Module
Type
Feature (api + ui)
Title
Queue ও Register (কাজের তালিকা, Journal Register, Approval Register) — Backend ও Frontend
Branch Name
`feature/accounting-queues-registers`
View Order
35
Description
**Summary**
Maker, Approver, Poster, Head of Finance — প্রত্যেকে নিজের কাজের queue দেখবে; journal ও approval register filter করে খুঁজতে পারবে।

**Task serial**
`ACC-P2-15 — Queue ও Register` · Feature (api + ui)

**Depends on**
- `ACC-P2-02` (Draft API ও list)
- `ACC-P2-08` (approval request, pending API)
- `ACC-P2-12` (Post, `approved` তালিকা)

**Relation: আগের task**
- `P2-02` দিয়েছে `GET /accounting/journals` প্রাথমিক রূপ ও journal দেখার পাতা।
- `P2-08` দিয়েছে `acc_approval_requests` (`status`, `current_level`, `rule_snapshot`) ও `GET /accounting/approvals/pending` প্রাথমিক রূপ; approval UI।
- `P2-09a` দিয়েছে eligibility নিয়ম (delegation সহ): "আমার অনুমোদনের অপেক্ষায়" তালিকা এই নিয়মের SQL রূপ।
- `P2-09b` দিয়েছে `acc_approval_override_reviews` ও override row: "অপর্যালোচিত override" তালিকা।
- `P2-12` দিয়েছে Post: `approved` তালিকা ও posted history।
- `P2-04` দিয়েছে history (timeline)। Source বলে `P2-04` "অবরুদ্ধ করে P2-15 (journal-এর timeline দেখানো)"।
- `P1-03` registry-র `monitor_approval_stuck_days` (default 3; registry-তে Phase 5 লেখা, কিন্তু key আগে থেকেই আছে)।
- `P1-02` slug: `accounting.journal.view`।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P2-16` (test): AC2.10 (`AuditHistoryTest`), তালিকার ঠিক-লোকের test।
- `P3-09` (অন্য report): Journal Register (পূর্ণ) ও Approval Register (পূর্ণ) এই task-এর filter-সহ + export।
- `P5-03` (Monitoring ও alert): আটকে থাকা approval, অপর্যালোচিত override এই task-এর API থেকে।
- `P3-03` (GL drill-down): voucher নম্বর → journal পাতা ও approval ইতিহাস।

**Related tables / schema**

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

নতুন table নেই। পড়ে: `acc_journals`, `acc_journal_lines`, `acc_approval_requests`, `acc_approval_actions`, `acc_approval_delegations`, `acc_approval_override_reviews`, `acc_journal_status_history`, `acc_accounts`, RBAC (role)।

| তালিকা | কে দেখে | কী দেখায় |
| --- | --- | --- |
| আমার 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-09b` |

**Business rules**
- Register সবসময় company-scoped। Reference বা voucher নম্বর দিয়ে খোঁজা `company_id` সহ index-এ।
- Register-এর filter: অবস্থা, ধরন, তারিখ পরিসর, voucher নম্বর, reference, account, amount পরিসর, Maker, Approver।
- Pagination `id` ধরে cursor (offset নয়)।
- "আটকে থাকা": `pending` request যার বয়স `monitor_approval_stuck_days` দিনের বেশি।
- "আমার অনুমোদনের অপেক্ষায়" তালিকায় Sequential হলে শুধু `current_level`-এর ধাপ; Parallel হলে অপূর্ণ ধাপ।
- তালিকা ও Register শুধু পড়ে; কিছু লেখে না।

**DB flow: অপেক্ষমাণ** — `acc_approval_requests` `status = 'pending'` ছেঁকে, `rule_snapshot` থেকে `JSON_TABLE(rule_snapshot, '$.levels[*]' ...)` দিয়ে ধাপ বের করে, user-এর role ও সক্রিয় delegation-এর সাথে join। Sequential হলে শুধু `current_level`-এর ধাপ। `pending` সংখ্যা ছোট বলে এটা দ্রুত। বড় হলে অপটিমাইজেশন: ধাপগুলো আলাদা table-এ ছড়িয়ে দেওয়া (এখন নয়)।

**Task scope**
1. Index migration (`idx_acc_journals_company_creator_status`, `idx_acc_journals_company_date`)।
2. API: প্রতিটা তালিকা (নিচে)।
3. UI: তালিকা পাতা ও Register পাতা, filter bar, cursor pagination ("আরও দেখুন")।
4. Delegation-সচেতন "আমার অনুমোদনের অপেক্ষায়" SQL।
5. Test।

**Out of scope**
Export (`P3-09`/`P3-10`), alert (`P5-03`), approval করার বাটন (`P2-08`), বড় সংখ্যায় pending হলে ধাপ-table অপটিমাইজেশন।

**Contract**
- `GET /accounting/journals?...` (filter: অবস্থা, ধরন, তারিখ পরিসর, voucher নম্বর, reference, account, amount পরিসর, Maker, Approver; cursor)
- `GET /accounting/approvals/register?...`
- `GET /accounting/approvals/pending`
- `GET /accounting/approvals/stuck`
- `GET /accounting/approvals/overrides/unreviewed`

**Permission**
`accounting.journal.view` (সব register); "অনুমোদিত, Post হয়নি" Post-অনুমতিওয়ালার (`accounting.journal.post`) জন্য; আটকে থাকা ও অপর্যালোচিত override কোন slug-এ সীমিত তা Open point।

**Error**
`ACC_FORBIDDEN`, `ACC_COMPANY_FORBIDDEN`

**Audit**
নেই (শুধু পড়া)।

**Acceptance**
AC2.10

**Test**
- প্রতিটা তালিকা ঠিক লোকেদের ঠিক journal দেখায়।
- Delegation-এর সাথে (delegate-এর তালিকায় delegator-এর ধাপ আসে, মেয়াদ পেরোলে আসে না)।
- অন্য company-র কিছু আসে না।
- Cursor pagination (পেজ জুড়ে কোনো ডুপ্লিকেট/বাদ নেই, নতুন row ঢুকলেও)।
- Sequential-এ শুধু `current_level`।
- Stuck: `monitor_approval_stuck_days` সীমা।
- Filter সংমিশ্রণ; voucher/reference খোঁজা index ব্যবহার করে (EXPLAIN)।
- তালিকা API আর `ApproverEligibility`-র ফল একই (consistency test)।

**Deliverable**
সব API, তালিকা ও register UI।

**Sub-tasks**
index migration, প্রতিটা তালিকা API ও UI, filter, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC2.10 সবুজ।
- `EXPLAIN` প্রমাণ: Register query-তে `company_id` index ব্যবহৃত।

**Architecture constraints**
- Offset pagination নয়, শুধু `id` cursor।
- তালিকার নিয়ম নতুন করে লেখা নয়; যেখানে সম্ভব `ApproverEligibility`-র সাথে একই নিয়ম (SQL সংস্করণ test দিয়ে মিলানো)।
- সব query-তে `company_id` সবার আগে।

**Open point**
1. "আটকে থাকা approval" (Admin, Head of Finance) আর "অপর্যালোচিত override" (Head of Finance)-এর জন্য কোন permission slug/role লাগবে source-এ নির্দিষ্ট নেই (slug তালিকায় "view only for HoF" ধরনের কিছু নেই)। প্রস্তাব: role-ভিত্তিক সীমা `P1-02`-এর seed অনুযায়ী `accounting.approval.manage`/`journal.override`; tech lead/product-এর কাছে নিশ্চিত করো।
2. `GET /accounting/approvals/pending` ও `GET /accounting/journals` একাধিক card-এ আছে (দেখুন `P2-02`, `P2-08` Open point)।
3. `monitor_approval_stuck_days` Phase 5-এর setting হিসেবে registry-তে চিহ্নিত, কিন্তু এই Phase-এ ব্যবহার হয়; `P1-03` registry আগে থেকেই সব key ধরে নিয়েছে, নিশ্চিত করে নিতে হবে।
4. "আমার reject করা, ফেরত দেওয়া" তালিকার ডেটা-উৎস `acc_approval_actions` (`decision` = `reject`, `return`) ধরা হয়েছে।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P2-16 — Phase 2-এর test suite

Project
ERPFlow
Module
Accounting Module
Type
Test
Title
Phase 2-এর test suite ও regression
Branch Name
`feature/accounting-phase2-tests`
View Order
36
Description
**Summary**
Phase 2-এর সব AC (AC2.1–AC2.14) MySQL-এ automated test-এ সবুজ — concurrency সহ; বিদ্যমান ERPFlow regression অক্ষত।

**Task serial**
`ACC-P2-16 — Phase 2-এর test suite` · Test

**Depends on**
- `ACC-P2-01` থেকে `ACC-P2-15` সব (`P2-09a` ও `P2-09b` সহ)

**Relation: আগের task**
প্রতিটা Phase 2 task নিজের test লিখেছে (state machine, validator, SoD, history, schema, rule, eligibility, Draft, resolver, approval, voucher, posting, delegation/override, reversal, opening post, queue)। এই task সেগুলোকে AC ধরে এক জায়গায় আনে, ফাঁক ধরে, সবগুলো MySQL-এ একাধিক connection-এ চালায়, আর বিদ্যমান ERPFlow regression দেখে। `P1-13`-এর factory, MySQL CI job ও test helper (দুই company, পাঁচ role) এখানে ব্যবহার হয়।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `ACC-P2-17` (Runbook, demo, সিদ্ধান্ত বন্ধ)।
- Phase 3 শুরু (`P3-01`: নির্ভর করে `P2-16`)।

**Related tables / schema**
কোনো নতুন table নেই। Test-এর জন্য factory ও helper।

**Business rules**
- Phase 2-এর প্রতিটা AC (AC2.1 থেকে AC2.14) একটা নির্দিষ্ট test class দিয়ে প্রমাণিত।
- সব concurrency আর atomicity test MySQL-এ, একাধিক connection দিয়ে (SQLite-এ trigger, generated column, composite FK আলাদা আচরণ করে)।
- **Golden suite** (Blueprint ১২.২)-এর প্রথম ৬টা এই Phase-এ সবুজ হতে হবে।

| 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` (সব অবস্থা × সব কাজ, `P2-01`), `ApprovalFlowTest` (এক ধাপ, দুই ধাপ sequential, parallel, reject ও পুনরায়, withdraw, return)।

**Task scope**
1. প্রতিটা test class আলাদা (উপরের table), প্রতিটার নিজের task-এর test-কে এখানে সংগঠিত করা।
2. Failure-injection helper: Post-এর (বা অন্য transaction-এর) যেকোনো ধাপের পর জোর করে exception ছুঁড়ে দেখা কিছু বদলায়নি।
3. সমান্তরাল runner: একাধিক DB connection / process দিয়ে ৫০টা সমান্তরাল Post, দুজন একসাথে approve/post/edit।
4. CI: MySQL job-এ সব concurrency/atomicity class চালানো।
5. বিদ্যমান ERPFlow regression (AC2.14)।
6. Test report।
7. Golden suite প্রথম ৬টা সবুজ।

**Out of scope**
Phase 3+ test, performance test (Phase 3 `P3-14`)।

**Contract**
নেই (test-ই deliverable)।

**Permission**
প্রযোজ্য নয় (test সব role-এর user দিয়ে চলে)।

**Error**
প্রযোজ্য নয় (প্রতিটা error code কোনো না কোনো test-এ ঘুরে দেখা)।

**Audit**
`AuditHistoryTest` AC2.10: প্রতিটা সিদ্ধান্তে user ও সময়, `activity_logs` ও `acc_journal_status_history` ও `acc_approval_actions` তিনটাতেই।

**Acceptance**
AC2.1 থেকে AC2.14 সব।

**Test**
(এই card নিজেই test; উপরের table দেখুন।) অতিরিক্ত: প্রতিটা Phase 2 error code (`JRN_NOT_APPROVED`, `JRN_SOD_VIOLATION`, `JRN_PERIOD_LOCKED` (warn পথ), `JRN_IMMUTABLE`, `JRN_STATE_INVALID`, `JRN_CONFLICT`, `JRN_ALREADY_REVERSED`, `APR_NO_RULE`, `APR_NOT_ELIGIBLE`, `APR_ALREADY_DECIDED`, `APR_RULE_GAP`) অন্তত একবার end-to-end API-তে ঘটে।

**Deliverable**
সব test class, test report।

**Sub-tasks**
প্রতিটা class আলাদা, failure-injection helper, সমান্তরাল runner

**Definition of done**
- AC2.1 থেকে AC2.14 সবুজ।
- Golden suite-এর প্রথম ৬টা সবুজ।
- সব concurrency ও atomicity test MySQL-এ, একাধিক connection-এ সবুজ।
- বিদ্যমান ERPFlow suite সবুজ (Project, Payroll, Asset, Commissioning অক্ষত)।
- Test report সংযুক্ত।

**Architecture constraints**
- Concurrency/atomicity test SQLite-এ নয়।
- Failure-injection production code-এ ঢোকানো নয়; test-only hook বা DB-স্তরের ব্যর্থতা।

**Open point**
1. Golden suite (Blueprint ১২.২)-এর কনটেন্ট এই ডকুমেন্টে নেই; প্রথম ৬টা কোনগুলো তা Blueprint থেকে নিতে হবে।
2. AC নম্বরের পূর্ণ সংজ্ঞা (AC2.x এর লিখিত বিবরণ) ডকুমেন্টে নেই, শুধু এই table-এর "মূল দৃশ্য" আছে।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Operations (`ops` + `gov`)
Title
Runbook, accountant demo ও সিদ্ধান্ত বন্ধ (Phase 2 merge gate)
Branch Name
`feature/accounting-phase2-runbook`
View Order
37
Description
**Summary**
Phase 2 `development`-এ merge flag বন্ধ রেখে; runbook হালনাগাদ, accountant demo + মতামত, OD-6 ও OD-8 লিখিতভাবে বন্ধ — তারপর Phase 3।

**Task serial**
`ACC-P2-17 — Runbook, demo ও সিদ্ধান্ত বন্ধ` · Ops (`ops` + `gov`)

**Depends on**
- `ACC-P2-16`

**Relation: আগের task**
`P2-16` প্রমাণ করেছে সব Phase 2 AC সবুজ (MySQL-এ, concurrency ও atomicity সহ) আর বিদ্যমান ERPFlow অক্ষত। এখন সেই প্রমাণ নিয়ে merge-এর অনুমতি, runbook-এর হালনাগাদ আর মানুষের মতামত নেওয়া হবে। `P1-14` যে Runbook খসড়া ও merge gate বানিয়েছিল (module কী, flag, Setup Checklist, trigger-এর সতর্কতা, `down()` চালানো নিষেধ, সাধারণ সমস্যা), এই task সেটাই বাড়ায়।

**Relation: পরের task (কে অপেক্ষা করছে)**
Phase 3 শুরু (source-এ `P2-17` "Phase 3 শুরু"-কে অবরুদ্ধ করে; Master sequence-এ `P3-01` সরাসরি `P2-16`-এর ওপর নির্ভর করে, তাই practice-এ দুটো একই গেট)।

**Related tables / schema**
কোনো নতুন table নেই। Runbook-এ উল্লেখ্য table: `acc_journals`, `acc_voucher_counters`, `acc_approval_requests`, `acc_approval_actions`, `acc_approval_delegations`, `acc_approval_override_reviews`, `acc_journal_status_history`, `acc_idempotency_keys`।

**Business rules**
- Phase 2 exit criteria (Blueprint ৭.১০) পূরণ।
- `development`-এ push মানে production deploy, তাই merge gate কঠোর (`P0-03`)।
- Merge শুধু `accounting.enabled` (এবং অন্য flag) Default বন্ধ রেখে।
- OD-6 (approval engine বিকল্প) ও OD-8 (voucher নম্বর কখন বসে) লিখিতভাবে বন্ধ থাকতে হবে।

**Task scope**
1. **Runbook হালনাগাদ:**
   - Post ব্যর্থ হলে কী করা (rollback হয়েছে কি, journal `approved`-এ আছে কি, voucher counter আগের মানে কি, কোন ধাপে ব্যর্থ, `correlation_id` দিয়ে log)।
   - Voucher নম্বরে গর্ত সন্দেহ হলে কীভাবে যাচাই (`acc_voucher_counters.last_no` বনাম posted journal-এর সর্বোচ্চ `voucher_no`; `uq_acc_journals_company_voucher`)।
   - আটকে থাকা approval (কীভাবে খুঁজে বের করা, কাকে জানানো, withdraw/return/override-এর পথ)।
   - Delegation (কীভাবে দেওয়া, শেষ করা, মেয়াদ)।
   - Override-এর পর পর্যালোচনা (অপর্যালোচিত তালিকা, কে দেখবে)।
2. **Accountant demo:** সাধারণ journal (Draft → Submit → Approve → Post), reject ও পুনরায় পাঠানো, reversal। মতামত লিখে রাখা।
3. **OD-6 আর OD-8 লিখিতভাবে বন্ধ আছে কি না যাচাই।**
4. **Merge gate (`P1-14`-এর মতো):** সব flag Default বন্ধ প্রমাণ; প্রতিটা নতুন migration পিছনে সামঞ্জস্যপূর্ণ প্রমাণ; বিদ্যমান suite সবুজ; Project/Payroll/Asset/Commissioning-এর code অপরিবর্তিত (diff দিয়ে প্রমাণ)।

**Out of scope**
Phase 3 কাজ, Rollback পরিকল্পনা (`P5-04`), UAT (`P5-01`)।

**Contract**
নেই (নথি ও সই)।

**Permission**
প্রযোজ্য নয়।

**Error**
প্রযোজ্য নয়।

**Audit**
নেই (সিদ্ধান্তের লিখিত রেকর্ড নথিতে)।

**Acceptance**
Phase 2 exit criteria (Blueprint ৭.১০)

**Test**
Merge gate checklist-এর প্রতিটা আইটেম প্রমাণসহ টিক; demo-র দৃশ্যগুলো নমুনা company-তে সফল।

**Deliverable**
Runbook, demo-র মতামত নথি, সই করা merge checklist।

**Sub-tasks**
runbook হালনাগাদ, accountant demo, মতামত নথি, OD-6/OD-8 বন্ধ যাচাই, merge checklist

**Definition of done**
- সই করা merge checklist আছে।
- Accountant-এর লিখিত মতামত আছে।
- OD-6 ও OD-8 লিখিতভাবে বন্ধ।
- Phase 2 exit criteria (Blueprint ৭.১০) পূরণ।
- Runbook-এ Post ব্যর্থতা, voucher গর্ত, আটকে থাকা approval, delegation, override পর্যালোচনা, প্রতিটার সমাধান আছে।

**Architecture constraints**
- Merge হবে শুধু flag বন্ধ রেখে।
- Phase 3 শুরু হবে না এই task ছাড়া।

**Open point**
1. Blueprint ৭.১০ (Phase 2 exit criteria)-এর লিখিত তালিকা এই ডকুমেন্টে নেই; Blueprint থেকে নিতে হবে।
2. OD-5 (period lock হুক), OD-14 (ledger উৎস), OD-15 (reversal তারিখ) এই Phase-এ অংশত ব্যবহৃত; source শুধু OD-6 ও OD-8 বন্ধ যাচাই করতে বলে। প্রস্তাব: runbook-এ বাকি তিনটার অবস্থাও লিখে রাখা (Phase 3 আগে বন্ধ হবে)।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

<!-- ===== END: ACC-Phase-2-Tasks.md ===== -->


<!-- ===== BEGIN: ACC-Phase-3-Tasks.md ===== -->

# Accounting Module — Phase 3 Tasks (General Ledger, Period ও Reports)

> এখানকার প্রতিটা task আলাদা — একজন বা একটা টিম নিজেরটা নিয়ে এগোতে পারে।
> Phase 3 শেষ মানে post হওয়া data থেকে GL, Trial Balance, P&L, Balance Sheet, Cash Flowসহ financial report বের হবে; Excel/PDF-এ নামানো যাবে; period বন্ধ ও আবার খোলা যাবে; হিসাবের ভেতরের মিল (Reconciliation) নিজে নিজেই চেক হবে। Client-এর accountant হাতে মিলিয়ে সই না করলে Phase 4 শুরু হয় না।
> ক্রম: ledger query → cache → GL → Trial Balance → P&L → Balance Sheet → Cash Flow mapping → অন্য report → Cash Flow → reconciliation → export → period বন্ধ → period lock → performance ও test → accountant-এর সই।
> Estimate, Owner, Sprint ইচ্ছা করে খালি — planning শেষ হলে ভর্তি হবে। View Order master build sequence (Pack ১০.১, ক্রম ২২ থেকে ৩২) অনুযায়ী, Phase 2-এর পর ধারাবাহিক (৩৮ থেকে ৫২)।

## সব Phase 3 task-এ প্রযোজ্য সাধারণ নিয়ম (প্রতিটা task নিজের DoD-তে এগুলো ধরে নেয়)

- Database: MySQL, InnoDB, `utf8mb4`। Table prefix `acc_`।
- Primary key ধরন `ACC-P0-02`-এর Convention Sheet অনুযায়ী (অনুমান: bigint unsigned বা ULID)। সব `*_id` FK একই ধরনের।
- প্রতিটা business table-এ `company_id NOT NULL`, `companies(id)`-এর FK, আর প্রতিটা composite index-এ `company_id` সবার আগে।
- টাকা `DECIMAL(20,4)`। Enum `VARCHAR(32)` + `CHECK`, MySQL enum নয়।
- বদলানো যায় এমন row-এ `version INT UNSIGNED NOT NULL DEFAULT 1`; update-এ `WHERE version = ?` আর `version = version + 1`।
- Posted বা লেজারের row কখনো মোছা হয় না। `ON DELETE RESTRICT` ডিফল্ট; লেজারের table-এ `CASCADE` নয়।
- Migration: প্রতি table-এ আলাদা, প্রয়োগ হওয়া migration সম্পাদনা নয়, পিছনে সামঞ্জস্যপূর্ণ (আগে যোগ, পরে বদল, শেষে মোছা), `down()` নিরাপদ। এই Phase-এ একটা column-যোগ (expand) migration আছে: `acc_periods.balance_version` (P3-02)।
- **account_id বা journal_id বা period_id রাখা table-এ composite FK** `(company_id, account_id)` / `(company_id, journal_id)` / `(company_id, period_id)`, একা `*_id`-র FK নয়।
- **Ledger-এ কোন journal আসে (R3.1, R3.2, AC3.5):** `acc_journals.status IN ('posted','reversed')`। Pack-এর স্পষ্টীকরণ: Blueprint-এর "শুধু posted journal report-এ আসে"-র মানে এটাই। `reversed` অবস্থার journal আসলে posted-ই, শুধু তার reversal-ও posted হয়েছে। দুটো একসাথে ধরলে নিট প্রভাব শূন্য। `reversed` বাদ দিলে হিসাব ভুল হবে। `draft`, `submitted`, `approved`, `rejected`, `cancelled` কখনো কোনো report-এ আসে না। এই Phase-এর **প্রতিটা** query এই নিয়ম মানে।
- **তারিখ (R3.3):** `journal_date`-এর ওপর, শুরু ও শেষ দিন অন্তর্ভুক্ত।
- **চিহ্ন (R3.4):** Debit-প্রধান type (asset, expense): balance = Debit − Credit। Credit-প্রধান (liability, equity, revenue): Credit − Debit।
- **টাকার অঙ্ক string:** PHP-র float কোথাও নয়। BCMath বা decimal library, API response-এ string।
- **Snapshot:** যে report একাধিক query চালায় সেটা একটা read-only `START TRANSACTION WITH CONSISTENT SNAPSHOT` (REPEATABLE READ) transaction-এ, যাতে চলার মাঝে নতুন Post এলেও সংখ্যা নিজের সাথে মেলে।
- **সব report ও export একই `company_id` scope-এ** (AC3.6)। অন্য company-র account/journal চাইলে `ACC_COMPANY_FORBIDDEN`। Report চালানো `accounting.report.run` audit-এ যায়।
- **Report দেখার permission:** `accounting.report.view`। নামানো `accounting.report.export` (আলাদা)।
- **সব report test MySQL-এ চলবে** (SQLite-এ snapshot, window function আর generated column আলাদা আচরণ করে)।
- **DoD-র সাধারণ অংশ:** Service–Repository architecture (interface সহ), Form Request, API Resource; `company_id` tenant scope + IDOR test; Feature test (happy path, validation, unauthorized/forbidden); audit event `AccountingAudit` দিয়ে; error `AccountingError` enum-এর code দিয়ে (নতুন code পরে যোগ হলে শুধু enum-এ); flag বন্ধ রেখে merge নিরাপদ; বিদ্যমান ERPFlow regression সবুজ; Project/Payroll/Asset/Commissioning-এর code অপরিবর্তিত।

**Phase 3 নির্ভরতা-ছবি:**

```
Phase 2 (P2-16 সবুজ) + P0-05 (OD-14) ─► P3-01 ─┬─► P3-02 (+P1-08) ──┐
                                                 ├─► P3-03 ───────────┤
                                                 ├─► P3-05 ─► P3-06 ──┤
                                    P3-01+P3-02 ─► P3-04 ─────────────┤
                       P1-07+P1-04 ─► P3-07 ─┐                         │
                            P3-01+P3-06+P3-07 ─► P3-08 ───────────────┤
                       P3-01+P2-15 ─► P3-09 ──────────────────────────┤
                                                                       ▼
                     P3-03..06 + P3-08 ─► P3-13 (Reconciliation) ─┬─► P3-11 (Period বন্ধ, +P1-08,P2-10,P3-01) ─► P3-12 (+P2-12)
                     P3-03..09 ─► P3-10 (Export)                  │
                                                                   ▼
                     P3-01..P3-13 সব ─► P3-14 (Performance ও test) ─► P3-15 (সই) ─► Phase 4 (P4-01), P5-01, P5-02
```

**আগের Phase থেকে Phase 3 যা পায় (প্রতিটা card এগুলো ধরে নেয়):**

| উৎস | কী পায় | কোথায় ব্যবহার |
| --- | --- | --- |
| `P1-06` | `acc_accounts` (type, `parent_id` গাছ, `is_group`, `is_active`, `cashflow_class`, `is_cash_equivalent`), `uq_acc_accounts_company_id` | সব report, P3-07 |
| `P1-08` | `acc_periods` (`status` `open`/`soft_closed`/`closed`, `closed_at/by`, `reopened_at/by`, `version`), `PeriodResolver::forDate`, `PeriodGenerator` | P3-02, P3-06, P3-11, P3-12 |
| `P1-09` | `acc_journals`, `acc_journal_lines`, index `idx_acc_journals_company_status_date`, `idx_acc_jl_company_account`, `PostedJournalProbe` আসল implementation | P3-01, P3-13 (RC6) |
| `P1-10` | DB guard trigger, `Schema::appendOnly('table')` helper, `acc_journals.posted_hash` column | P3-13 (RC9, `acc_reconciliation_runs`), P3-11 |
| `P1-12` | `AccountingJob`/`RunsInCompanyContext`, `AccountingAudit`, `AccountingError` enum, correlation id | সব job, error, audit |
| `P1-03`/`P1-04` | `SettingsService`, `SettingRegistry`, second approval (change request), `acc_settings_history` | P3-07, সব setting পড়া |
| `P2-10` | `SodGuard::assert(...)` | P3-11 |
| `P2-12` | `PostingService`, `PeriodGuard::check` hook (Phase 2-এ শুধু `warn`), `JournalPosted` afterCommit event | P3-02, P3-12 |
| `P2-13` | Reversal/Adjustment (reversal Post হলে মূল journal `reversed`) | সব report-এর `reversed` নিয়ম, P3-12 |
| `P2-15` | Journal Register ও Approval Register (filter সহ) | P3-09 |
| `P2-16` | Phase 2 test suite সবুজ, Golden suite-এর প্রথম ৬টা | P3-01 শুরুর শর্ত |

---

## ACC-P3-01 — Ledger query layer

Project
ERPFlow
Module
Accounting Module
Type
Feature (service)
Title
Ledger query layer — Backend
Branch Name
`feature/accounting-ledger-query-layer`
View Order
38
Description
**Summary**
Report বানানোর সবাই একই query layer থেকে সংখ্যা তুলবে — Trial Balance, GL, P&L, Balance Sheet, Cash Flow যেখানেই দেখো, একই ফল আসবে; কেউ আলাদা SQL লিখলে পরে মিল মেলানোর ঝামেলা হবে।

**Task serial**
`ACC-P3-01 — Ledger query layer` · Feature

**Depends on**
- `ACC-P2-16` (Phase 2 exit: Phase 2 test suite সবুজ, Golden suite-এর প্রথম ৬টা সবুজ)
- `ACC-P0-05` (OD-14 spike-এর ফল)
- OD-14 (ধরা হয়েছে: Option A, posted `acc_journal_lines` নিজেই ledger, আলাদা projection নয়)

**Relation: আগের task**
- `P2-16` প্রমাণ করেছে Post, Reversal, Opening post, Register সব সঠিক। তাই `status IN ('posted','reversed')` data নির্ভরযোগ্য, এই task সেটার ওপর পড়ে।
- `P2-12` (Posting service) ledger-এ বদল আনার একমাত্র পথ। এটা `posted` journal বানায়, `period_id` আর `voucher_no` বসায়। `P2-13` reversal Post হলে মূল journal `reversed` করে। এই দুই অবস্থাই এই task-এর ছাঁকনি।
- `P1-09` দিয়েছে `acc_journals`, `acc_journal_lines` আর দুটো index `idx_acc_journals_company_status_date (company_id, status, journal_date)` ও `idx_acc_jl_company_account (company_id, account_id, journal_id)`। এই index-দুটোই এই task-এর query-র গতি।
- `P1-06` দিয়েছে `acc_accounts` (type, `parent_id` গাছ, সর্বোচ্চ গভীরতা 10)। `account_group_id` filter-এর descendant খোঁজা এই গাছ ধরে।
- `P0-05` OD-14 নিয়ে prototype-এ বড় data-য় GL query-র গতি মেপে Option A-কে সমর্থন করেছে বলে ধরা হয়েছে। Option B (আলাদা projection) এই task-এ নেই।
- `P1-12` দিয়েছে `AccountingError`, `AccountingAudit`, company context। এই task-এ নিজস্ব business error নেই (শুধু পড়া), তাই এগুলো সরাসরি ব্যবহার হয় না, কিন্তু query-র `company_id` সবসময় context থেকে আসে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- **P3-02 থেকে সব report card** (`P3-03`, `P3-04`, `P3-05`, `P3-06`, `P3-08`, `P3-09`) এই task-এর চারটা method ছাড়া কিছু ডাকে না। কেউ নিজের SQL লিখবে না। এটাই "সংখ্যা সবখানে এক" নিশ্চিত করার উপায়।
- `P3-02` cache `LedgerQueryService::balancesAsOf`-এর ভেতরে বসে: শুরুর তারিখের আগের সম্পূর্ণ period cache থেকে, বাকিটুকু সরাসরি line থেকে। তাই এই task-এ cache-হীন সরল রূপ আগে আসে, `P3-02` সেটাকে ভেতরে বদলায়, বাইরের contract বদলায় না।
- `P3-11` period বন্ধের যাচাই (`tb_balanced` ইত্যাদি) এই layer ব্যবহারকারী service-ই ডাকে।
- `P3-13` check-গুলোর মধ্যে RC6 ইচ্ছা করে এই layer-কে **পার না হয়ে** raw SQL দিয়ে একই সংখ্যা হিসাব করে, যাতে এই task-এর ভুল ধরা পড়ে। তাই এই task-এর লেখা query আর RC6-এর query দুটো আলাদা path হতে হবে।
- `P3-14` (performance): ধীর হলে নিচের contingency চালু হবে কি না সেখানে ঠিক হয়।
- Phase 6-এর `P6-09`, `P6-12`, `P6-13` (dimension) পরে এই layer-এ `withDimensions` বসাবে। এই task-এ সেটা নেই।
- **খোলা প্রশ্ন OP-1:** চার method-এর return-এর সঠিক আকার (array/DTO), আর `account_group_id`-এর descendant খোঁজার কৌশল (গাছ PHP-তে নাকি recursive query) Pack-এ নির্ধারিত নয়। শর্ত শুধু: টাকার অঙ্ক string, গাছ একবার পড়া, গভীরতা ≤ 10। Tech lead PR-এর আগে ঠিক করবে।
- **খোলা প্রশ্ন OP-2:** Pack বলছে "AC3.10 (পরে)": cache-সংক্রান্ত AC `P3-02`-এ প্রমাণ হবে, এই task-এ নয়।

**Related tables / schema**
কোনো নতুন table নেই। শুধু পড়া:
- `acc_journal_lines` (`company_id`, `journal_id`, `line_no`, `account_id`, `debit`, `credit`, `cost_center_id`)
- `acc_journals` (`status`, `journal_date`, `period_id`, `voucher_no`, `narration`, `reference_no`)
- `acc_accounts` (`type`, `parent_id`, `is_group`, `is_active`)

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

```sql
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
```

**Business rules (R3.1 থেকে R3.5, R3.11)**
- Ledger = `status IN ('posted','reversed')` journal-এর line (উপরের সাধারণ নিয়ম)। `draft`, `submitted`, `approved`, `rejected`, `cancelled` কখনো আসে না।
- তারিখ `journal_date`-এর ওপর, শুরু ও শেষ দিন অন্তর্ভুক্ত (R3.3)।
- চিহ্ন নিয়ম (R3.4): Debit-প্রধান type (asset, expense) balance = Debit − Credit। Credit-প্রধান (liability, equity, revenue) balance = Credit − Debit।
- সব query `company_id` দিয়ে শুরু। Join-এও `company_id` মেলানো (`j.company_id = l.company_id`), যাতে composite index কাজ করে।
- সব টাকার অঙ্ক **string** হিসেবে ফেরত, PHP-র float কোথাও নয় (BCMath বা decimal library)।
- যে report একাধিক query চালায় সে একটাই `START TRANSACTION WITH CONSISTENT SNAPSHOT` (REPEATABLE READ, read-only) transaction-এ চালাবে, যাতে চলার মাঝে নতুন Post এলেও সংখ্যা নিজের সাথে মেলে। এই wrapper এই task বানায়, সব report ব্যবহার করে।
- Filter: `account_ids`, `account_group_id` (সব descendant সহ), `cost_center_id`, `type`।
- Ledger-এর উৎস posted line নিজেই (OD-14 Option A)। আলাদা projection table নেই।

**DB flow**
কোনো লেখা নেই। শুধু পড়া:
1. (যে report একাধিক query চালায় তার জন্য) snapshot transaction শুরু।
2. `balancesAsOf(companyId, date, filters)`: `journal_date <= :asof` পর্যন্ত account ধরে Debit/Credit যোগফল।
3. `movements(companyId, from, to, filters)`: `journal_date BETWEEN from AND to` ধরে একই।
4. `accountRows(companyId, accountId, from, to, cursor, limit)`: এক account-এর line, `ORDER BY j.journal_date, j.id, l.line_no`, keyset cursor।
5. `netProfit(companyId, from, to)`: `revenue` ও `expense` type-এর movement, নিট লাভ = Revenue (Credit − Debit) − Expense (Debit − Credit)।
6. Snapshot শেষ।

**Task scope**
1. `LedgerQueryService`-এর চারটা method: `balancesAsOf`, `movements`, `accountRows`, `netProfit`।
2. চিহ্ন নিয়ম প্রয়োগ (type ধরে)।
3. Snapshot wrapper (read-only, consistent snapshot, REPEATABLE READ)।
4. Decimal helper (string in, string out, BCMath বা decimal library)।
5. Filter বাস্তবায়ন: `account_ids`, `account_group_id` (descendant সহ), `cost_center_id`, `type`।
6. Fixture data (হাতে হিসাব করা মান সহ) আর test।

**Out of scope**
- কোনো report endpoint বা UI (সেগুলো `P3-03` থেকে)।
- Derived cache (`P3-02`)। এই task-এ `balancesAsOf` শুধু সরাসরি line থেকে।
- আলাদা ledger projection (OD-14 Option B)।
- **Performance contingency এখন করা হচ্ছে না।** `P3-14`-এর পরিমাপে join ধীর হলে দুটো বিকল্প: (১) `acc_journal_lines`-এ `entry_date` কপি রাখা (draft-এ journal-এর তারিখের সাথে সিঙ্ক, post-এর পর অপরিবর্তনীয়) আর `(company_id, account_id, entry_date)` index, (২) OD-14-এর Option B। দুটোর কোনোটাই এখন করা হচ্ছে না, মেপে ঠিক হবে।

**Contract**
```
LedgerQueryService::balancesAsOf(companyId, date, filters)
LedgerQueryService::movements(companyId, from, to, filters)
LedgerQueryService::accountRows(companyId, accountId, from, to, cursor, limit)
LedgerQueryService::netProfit(companyId, from, to)
```
সব টাকার অঙ্ক string। কোনো HTTP endpoint নেই।

**Permission**
কোনো নতুন permission নেই। এই service HTTP থেকে সরাসরি ডাকা হয় না, ডাকে report controller (যাদের `accounting.report.view` লাগে)।

**Error**
কোনো business error নেই (শুধু পড়া)।

**Audit**
এই service নিজে audit লেখে না। `accounting.report.run` লেখে report endpoint (`P3-03` থেকে)।

**Acceptance**
AC3.1, AC3.5, AC3.10 (পরে, `P3-02`-এ), AC3.12

**Test**
- নির্দিষ্ট fixture data দিয়ে প্রতিটা method-এর ফল হাতে হিসাব করা সংখ্যার সাথে মেলে।
- `reversed` journal ধরা না হলে test ব্যর্থ হয় (মূল ও reversal একসাথে ধরলে নিট শূন্য)।
- Snapshot test: query-র মাঝে অন্য connection থেকে Post করে দেখা সংখ্যা বদলায়নি।
- Draft, submitted, approved, rejected, cancelled কখনো আসে না।
- Debit-প্রধান ও Credit-প্রধান type-এর চিহ্ন ঠিক।
- `account_group_id` filter descendant সহ।
- টাকার অঙ্ক string, float নয়।

**Deliverable**
`LedgerQueryService`, snapshot wrapper, decimal helper, fixture, test।

**Sub-tasks**
চারটা method, চিহ্ন নিয়ম, snapshot wrapper, decimal helper, test

**Definition of done**
- সাধারণ DoD (উপরে) পূরণ।
- AC3.1, AC3.5 এই layer-এর স্তরে সবুজ (`LedgerStatusFilterTest`-এর ভিত্তি)।
- কোনো float ব্যবহার নেই (code review-র পরীক্ষা)।
- MySQL-এ test চলেছে।

**Architecture constraints**
- Report গুলো এই service ছাড়া `acc_journal_lines` সরাসরি query করতে পারবে না (RC6 ছাড়া, যেটা ইচ্ছাকৃত স্বাধীন path)।
- সব query `company_id` দিয়ে শুরু।
- `reversed` কখনো বাদ নয়।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P3-02 — Derived balance cache

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service + job)
Title
Derived balance cache — Backend
Branch Name
`feature/accounting-balance-cache`
View Order
39
Description
**Summary**
পুরোনো period-এর মোট বারবার গণনা না করে cache দিয়ে report দ্রুত চলবে; cache কখনোই ভুল সংখ্যা দেখাবে না — stale হলে সরাসরি line থেকে হিসাব, পেছনে rebuild।

**Task serial**
`ACC-P3-02 — Derived balance cache` · Feature

**Depends on**
- `ACC-P3-01` (`LedgerQueryService`)
- `ACC-P1-08` (`acc_periods`)

**Relation: আগের task**
- `P3-01` দিয়েছে `movements(from, to)`, যা `rebuild` এই period-র পরিসরে ডাকে, আর `balancesAsOf`, যার ভেতরে cache বসানো হবে।
- `P1-08` দিয়েছে `acc_periods` (`id`, `start_date`, `end_date`, `status`, `version`) আর `uq_acc_periods_company_id (company_id, id)`, যেটা `acc_period_balances`-এর composite FK-র লক্ষ্য। এই task `acc_periods`-এ `balance_version` column যোগ করে (expand migration)।
- `P2-12` Post-এর শেষে (ধাপ 17, commit-এর পরে `afterCommit`) `JournalPosted` event ছাড়ে। Phase 2-এ কেউ শোনে না। এই task-এ listener বসে।
- `P1-12` দিয়েছে company context-সহ job base (`AccountingJob`/`RunsInCompanyContext`) আর audit।
- `P1-06` থেকে `acc_accounts` (composite FK `(company_id, account_id)`-এর লক্ষ্য `uq_acc_accounts_company_id`)।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P3-04` (Trial Balance): গতির জন্য cache পড়ে (opening balance)।
- `P3-13` RC7: প্রতিটা **বৈধ** cache row মূল line-এর যোগফলের সাথে মেলায়।
- `P3-11` (Period বন্ধ): `acc_periods.balance_version` পড়ে লম্বা যাচাই-র আগে ও পরে তুলনা করে, আর "Post আটকে না রেখে বন্ধ" নকশা (Pack ৯.৫) এই column-এর ওপর দাঁড়িয়ে। তাই `balance_version` কেবল commit-এর পরে বাড়ে।
- `P3-14`: cache থাকা-না-থাকা অবস্থায় performance পরিমাপ আর `CacheStalenessTest` (AC3.10)।
- Phase 6-এর `P6-13` একই `balance_version` ধারণা অন্য table-এ নেবে ("P3-02-এর মতো"), এই task-এর নকশা পরিবর্তন করতে গেলে সেটা মনে রাখা।
- **খোলা প্রশ্ন OP-3:** `P3-03`, `P3-05`, `P3-06` card-গুলোর `নির্ভর করে` তালিকায় `P3-02` নেই, কিন্তু build sequence-এ `P3-02` আগে (ক্রম ২৩) আর `balancesAsOf` cache ব্যবহার করে। এখানে ধরা হলো: তারা শুধু `P3-01`-এর contract দেখে, cache ভেতরে স্বচ্ছভাবে বসে, কোনো report কোড বদলাতে হয় না।

**Related tables / schema**

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

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `id` | PK | না |  |
| `company_id` | FK | না |  |
| `period_id` | FK | না | Composite FK `(company_id, period_id)` → `acc_periods (company_id, id)` |
| `account_id` | FK | না | Composite FK `(company_id, account_id)` → `acc_accounts (company_id, id)` |
| `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 | না |  |

Constraint ও index:
- 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`।

**Business rules (R3.12)**
- এটা কখনো উৎস নয়, শুধু দ্রুত করার জন্য। ভুল হলে ফেলে দিয়ে আবার বানানো যায়।
- **Cache কখনো ভুল সংখ্যা দেবে না।** `stale` হলে ব্যবহার হয় না।
- বৈধ কি না: একটা cache row বৈধ হলে `row.period_version = acc_periods.balance_version`। না মিললে `stale`।
- বাতিল (invalidate) Post-এর transaction-এ নয়, commit-এর **পরে** (`afterCommit`), তাই Post-এ hot row (এক period-এর row-এ সব Post-এর লড়াই) তৈরি হয় না।
- Post এবং balance_version বাড়ার মাঝের ফাঁকে কেউ cache পড়লে ক্ষণিকের জন্য পুরোনো সংখ্যা দেখার ঝুঁকি: Pack এটাকে "বৈধ" row ধরে, কারণ `balance_version` তখনো বদলায়নি। (এই ছোট ফাঁকের জন্য `P3-11` অতিরিক্ত `MAX(posted_at)` তুলনা রাখে।)

**DB flow: invalidate**
1. Post commit হয় (`P2-12`)।
2. `JournalPosted` event `afterCommit`-এ listener-এ আসে।
3. `UPDATE acc_periods SET balance_version = balance_version + 1 WHERE company_id = ? AND id = ?` (ওই journal-এর period)।
4. ব্যর্থ হলে আবার চেষ্টা; Post-কে প্রভাবিত করে না (Pack ৯.২ ধাপ 12)।

**DB flow: `PeriodBalanceCache::rebuild(companyId, periodId)`**
1. `balance_version` পড়া (`v1`)।
2. `LedgerQueryService::movements(period-র পরিসর)` (`P3-01`)।
3. Transaction-এ ওই period-এর পুরোনো row মুছে নতুন row বসানো, `period_version = v1`।
4. নোট: rebuild-এর মাঝে কোনো Post এলে `balance_version` `v2` হয়ে যাবে আর এই row আপনা-আপনি `stale`; ক্ষতি নেই।

**DB flow: ব্যবহার**
`LedgerQueryService::balancesAsOf` রিপোর্টের শুরুর তারিখের আগের **সম্পূর্ণ** period-গুলো cache থেকে, বাকিটুকু সরাসরি line থেকে। কোনো period `stale` পেলে সেটা সরাসরি line থেকে হিসাব, আর পেছনে `RebuildPeriodBalancesJob` queue-তে।

**Task scope**
1. Migration ×2: `acc_period_balances` আর `acc_periods.balance_version`।
2. `PeriodBalanceCache` service: `rebuild`, বৈধতা পরীক্ষা, পড়া।
3. `JournalPosted` listener (commit-এর পরে, retry সহ)।
4. `LedgerQueryService::balancesAsOf`-এ cache ব্যবহার (`stale` হলে সরাসরি + rebuild queue)।
5. `RebuildPeriodBalancesJob(companyId, periodId)`: idempotent, company context (`P1-12`) সহ।
6. রাতের sweep: সব `stale` period-এর জন্য rebuild।
7. Test।

**Out of scope**
- RC7 check class নিজে (`P3-13`)।
- Cache কোনো report-এর উৎস হিসেবে নয়, শুধু গতি।
- `balance_version`-কে Post-এর transaction-এ বাড়ানো (ইচ্ছাকৃতভাবে নিষিদ্ধ)।

**Contract**
- `PeriodBalanceCache::rebuild(companyId, periodId)`
- Event listener: `JournalPosted` → `acc_periods.balance_version + 1`
- Job: `RebuildPeriodBalancesJob(companyId, periodId)` (idempotent, company context সহ), আর রাতের sweep job।
- কোনো public HTTP endpoint নেই।

**Permission**
প্রযোজ্য নয় (HTTP নেই, job)।

**Error**
কোনো user-মুখী error নেই।

**Audit**
`accounting.cache.rebuilt`

**Acceptance**
AC3.10

**Test**
- Post-এর পর cache `stale`।
- Rebuild-এর পর বৈধ।
- Rebuild-এর মাঝে Post (row `stale` হয়ে যায়)।
- Cache ও সরাসরি query-র ফল সবসময় এক।
- Listener ব্যর্থ হলে Post প্রভাবিত হয় না।
- `RebuildPeriodBalancesJob` দুইবার চালালে একই ফল (idempotent)।
- Job company context ছাড়া চলে না।

**Deliverable**
Migration ×2, cache service, listener, job।

**Sub-tasks**
migration, `rebuild`, listener, ব্যবহার, job, রাতের sweep, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC3.10 সবুজ (`CacheStalenessTest`-এর ভিত্তি)।
- Post-এর transaction-এ `acc_periods` row বদলানো হয় না (প্রমাণিত)।
- Expand migration `down()` নিরাপদ।

**Architecture constraints**
- Cache কখনো উৎস নয়। `stale` row কখনো report-এ ব্যবহার হয় না।
- `balance_version` বাড়ানো শুধু commit-এর পরে।
- Lock-ক্রম (Pack ৯.১) ভাঙা যাবে না: rebuild কোনো ledger row lock করে না।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P3-03 — General Ledger report

Project
ERPFlow
Module
Accounting Module
Type
Feature (api + ui)
Title
General Ledger report — Backend ও Frontend
Branch Name
`feature/accounting-report-general-ledger`
View Order
40
Description
**Summary**
একটা account-এর সব লেনদেন চলতি balance সহ দেখতে পারবে, আর যেকোনো সংখ্যায় ক্লিক করলে মূল journal পর্যন্ত নামতে পারবে।

**Task serial**
`ACC-P3-03 — General Ledger report` · Feature

**Depends on**
- `ACC-P3-01`
- `ACC-P1-02` (permission slug, middleware)

**Relation: আগের task**
- `P3-01`: `balancesAsOf` (opening), `accountRows` (পৃষ্ঠার row), `movements`/aggregate (মোট), snapshot wrapper, চিহ্ন নিয়ম।
- `P1-02`: `accounting.report.view` slug ও route middleware।
- `P1-06`/`P1-07`: account গাছ (group account ও descendant), UI-র account picker।
- `P2-02`, `P2-08`, `P2-04`: drill-down-এর গন্তব্য: journal-এর পাতা (`P2-02`), তার approval ইতিহাস (`P2-08`), status history (`P2-04`)।
- `P1-12`: `ACC_COMPANY_FORBIDDEN`, audit।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P3-13`: RC2 প্রতিটা account-এর GL closing = Trial Balance closing মেলায়। তাই GL service-এর ফল আলাদা, পুনঃব্যবহারযোগ্য class-এ থাকতে হবে (controller-এ লেখা হিসাব RC2 ডাকতে পারবে না)।
- `P3-04` (TB) এবং `P3-05`, `P3-06`: প্রতিটা account-এ click → এই GL-এর পাতা।
- `P3-10`: GL-এর Excel ও PDF export। UI-র বাটন `P3-10` তে সংযোগ হবে। `report` নাম `general-ledger`।
- `P3-14`: GL, ১ লক্ষ row-এর account, এক পৃষ্ঠা: লক্ষ্য এক সেকেন্ডের কাছাকাছি।
- `P3-15`: accountant এই report নিজের হিসাবের সাথে মেলায়।
- **খোলা প্রশ্ন OP-4:** Response-এর `reconciliation` (RC2 ফল) পাশে দেখানোর কথা, কিন্তু RC2 check class-টা `P3-13`-এ (ক্রম ২৮), আর TB (RC2-র অপর অংশ) `P3-04` একই ক্রমে (২৪)। Pack স্পষ্ট করেনি `P3-03` রিলিজে `reconciliation` field কী থাকবে। ধরা হলো: field ও UI-র লাল ব্যানার এখন তৈরি থাকবে, ফল `P3-13` যুক্ত হলে (inline, সংরক্ষণ ছাড়া) ভরবে; তার আগে `null`। Tech lead নিশ্চিত করবে।

**Related tables / schema**
কোনো নতুন table নেই। পড়া: `acc_journals`, `acc_journal_lines`, `acc_accounts` (`P3-01` ছাড়া সরাসরি নয়)।

**Business rules (R3.1 থেকে R3.4, RC2)**
- Ledger-এর অবস্থা `posted` ও `reversed` (সাধারণ নিয়ম)।
- `closing = opening + period_debit − period_credit` (debit-প্রধান account হলে), নইলে উল্টো (`opening + period_credit − period_debit`)।
- **RC2:** ওই account-এর closing Trial Balance-এর closing-এর সাথে মেলে। না মিললে UI-তে লাল সতর্কতা (`RPT_RECON_FAILED` তথ্য হিসেবে, error নয়, report তবু দেখায়)।
- Group account বেছে নিলে সব descendant leaf-এর row একসাথে, একটা বাড়তি "account" কলাম সহ। Opening আর closing হলো group-এর যোগফল।
- চলতি balance পৃষ্ঠা বদলালেও ধারাবাহিক থাকতে হবে।

**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 দিয়ে (পৃষ্ঠা-নিরপেক্ষ)।
7. Audit `accounting.report.run`।

**Task scope**
1. Opening হিসাব।
2. পৃষ্ঠার query (keyset cursor)।
3. চলতি balance (পৃষ্ঠার শুরুর মান + window function)।
4. মোট (`period_debit`, `period_credit`, `closing`)।
5. Group account: descendant-এর row ও যোগফল, "account" কলাম।
6. Drill-down লিংক।
7. API ও UI।
8. Test।

**Out of scope**
- Excel/PDF তৈরি (`P3-10`)।
- RC2 check class (`P3-13`)।
- Dimension filter (Phase 6)।

**Contract**
`GET /accounting/reports/general-ledger?account_id=&from=&to=&cursor=&limit=&cost_center_id=`

Response:
- `opening` (`from`-এর আগের দিন পর্যন্ত balance)
- `rows[]`: তারিখ, voucher নম্বর, journal id, line নম্বর, narration, reference, Debit, Credit, চলতি balance
- `period_debit`, `period_credit`, `closing`
- `next_cursor`
- `reconciliation` (RC2 ফল)

UI:
- Account বাছাই (অনুসন্ধান সহ, group সহ), তারিখ পরিসর, cost center filter।
- তালিকা, চলতি balance কলাম, শেষে মোট ও closing।
- RC2 না মিললে লাল সতর্কতা।
- Excel ও PDF বাটন (`P3-10`)।
- প্রতিটা row-এর voucher নম্বরে click → journal-এর পাতা (`P2-02`) ও তার approval ইতিহাস (`P2-08`) ও status history (`P2-04`)।

**Permission**
`accounting.report.view`

**Error**
`ACC_COMPANY_FORBIDDEN` (অন্য company-র account), `ACC_FORBIDDEN`; `RPT_RECON_FAILED` শুধু তথ্য হিসেবে।

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

**Definition of done**
- সাধারণ DoD পূরণ।
- AC3.1 (GL অংশ), AC3.5, AC3.6 সবুজ।
- পৃষ্ঠার সীমানায় চলতি balance না ভাঙার test আছে।
- GL service আলাদা class, যাতে `P3-13` ডাকতে পারে।

**Architecture constraints**
- সব হিসাব `LedgerQueryService` (`P3-01`) দিয়ে, নিজের SQL নয়।
- Offset pagination নয়, keyset cursor।
- Float নয়, সব অঙ্ক string।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P3-04 — Trial Balance

Project
ERPFlow
Module
Accounting Module
Type
Feature (api + ui)
Title
Trial Balance — Backend ও Frontend
Branch Name
`feature/accounting-report-trial-balance`
View Order
41
Description
**Summary**
সব account-এর opening, period-এর নড়াচড়া আর closing এক পাতায়; মোট Debit আর Credit মিলছে কি না একনজরে বোঝা যাবে (Trial Balance)।

**Task serial**
`ACC-P3-04 — Trial Balance` · Feature

**Depends on**
- `ACC-P3-01`
- `ACC-P3-02`

**Relation: আগের task**
- `P3-01`: `balancesAsOf`, `movements`, snapshot wrapper, চিহ্ন নিয়ম।
- `P3-02`: cache (শুরুর balance দ্রুত আনতে `balancesAsOf`-এর ভেতরে স্বচ্ছভাবে)। `stale` হলে সরাসরি line থেকে, ফল একই।
- `P1-06`: CoA-র গাছ (group/leaf, `is_active`), group roll-up এই গাছ ধরে।
- `P3-03` (একই ক্রম ২৪, সমান্তরাল): প্রতিটা account-এ click → GL। GL-এর URL কাঠামো সেখান থেকে।
- `P1-03`: `report_show_zero_balance` setting (default `false`)।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P3-13`: RC1 (মোট closing Debit = মোট closing Credit), RC2 (GL closing = TB closing), RC5 (P&L নিট লাভ = TB-র Revenue ও Expense পার্থক্য)। তাই TB service পুনঃব্যবহারযোগ্য class।
- `P3-10`: `trial-balance` export।
- `P3-11`: period বন্ধের `tb_balanced` যাচাই period-শেষ তারিখে এই TB-র RC1-এর ওপর।
- `P3-14` ও `P3-15` (accountant হাতে মেলাবে)।
- `P5-02` (Cutover মহড়া): ধাপ ৫-এ পুরোনো system-এর সাথে account ধরে TB মেলানো এই report দিয়ে।
- **খোলা প্রশ্ন OP-5:** RC1 এই report-এর ভেতরে লাল ব্যানার হিসেবে আসে, আবার `P3-13`-এ আলাদা check class। ধরা হলো: `balanced` flag ও পার্থক্য এই task নিজেই হিসাব করে (সরল যোগফল), `P3-13` সেটা স্বাধীনভাবে আবার চালায় ও সংরক্ষণ করে।

**Related tables / schema**
কোনো নতুন table নেই। `P3-01` ও `acc_accounts` গাছ।

**Business rules (R3.1 থেকে R3.7, RC1)**
- প্রতিটা account-এ: `opening_debit`, `opening_credit`, `period_debit`, `period_credit`, `closing_debit`, `closing_credit`।
- **Presentation:** Closing balance Debit-কলামে (নিট Debit হলে) বা Credit-কলামে (নিট Credit হলে), **দুটোতে নয়**।
- Group account-এ descendant-এর যোগফল।
- `report_show_zero_balance` false হলে শূন্য balance আর শূন্য নড়াচড়ার account বাদ।
- Inactive account-এর balance থাকলে দেখায় (R3.7)।
- `as_of` মোডে opening শূন্য, closing সেই তারিখ পর্যন্ত জমা।
- Closing = opening + movement।
- **RC1:** মোট closing Debit = মোট closing Credit। না মিললে লাল ব্যানার, পার্থক্য সহ, report তবু দেখায় (R3.9, আটকায় না)।

**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 কলামে (একটাতেই)।
7. মোট সারি আর `balanced` flag।
8. Audit `accounting.report.run`।

**Task scope**
1. Movement ও opening সংগ্রহ।
2. গাছের roll-up।
3. Debit বা Credit কলাম নির্ধারণ।
4. RC1 (`balanced`, পার্থক্য)।
5. `report_show_zero_balance` filter।
6. `from/to` ও `as_of` দুই মোড।
7. API ও UI।
8. Test।

**Out of scope**
- RC1 সংরক্ষণ ও রাতের চালানো (`P3-13`)।
- Export (`P3-10`)।
- Dimension (Phase 6)।

**Contract**
`GET /accounting/reports/trial-balance?from=&to=` বা `?as_of=`

Response: প্রতিটা account-এ `opening_debit`, `opening_credit`, `period_debit`, `period_credit`, `closing_debit`, `closing_credit`। Group account-এ descendant-এর যোগফল। শেষে মোট ও `balanced` flag (RC1)।

UI:
- তারিখ বা পরিসর বাছাই।
- গাছের মতো সাজানো তালিকা (expand ও collapse)।
- শূন্য দেখানোর toggle।
- মোট সারি ও `balanced` চিহ্ন।
- প্রতিটা account-এ click → GL (`P3-03`)।

**Permission**
`accounting.report.view`

**Error**
`ACC_FORBIDDEN`; `RPT_RECON_FAILED` শুধু তথ্য (RC1 না মিললে, error নয়)।

**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)।
- Inactive account-এর balance দেখায়।

**Deliverable**
API, UI

**Sub-tasks**
movement ও opening, গাছের roll-up, Debit বা Credit কলাম, RC1, API, UI, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC3.1, AC3.5, AC3.6 সবুজ।
- TB service আলাদা class (`P3-13` ও `P3-11` ডাকবে)।
- Float নেই।

**Architecture constraints**
- সব হিসাব `LedgerQueryService` দিয়ে।
- Leaf থেকে group-এ যোগ, group-এর নিজের line নেই (`is_group = true` account-এ line বসানো যায় না)।
- Closing একই account-এ Debit ও Credit দুই কলামে দেখানো যাবে না।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P3-05 — Profit & Loss

Project
ERPFlow
Module
Accounting Module
Type
Feature (api + ui)
Title
Profit & Loss — Backend ও Frontend
Branch Name
`feature/accounting-report-profit-loss`
View Order
42
Description
**Summary**
বেছে নেওয়া সময়ের আয়, খরচ আর নিট লাভ/ক্ষতি দেখা যাবে; চাইলে আগের period বা গত বছরের সাথে তুলনাও করা যাবে।

**Task serial**
`ACC-P3-05 — Profit & Loss` · Feature

**Depends on**
- `ACC-P3-01`
- OD-1 (হিসাবের ভিত্তি `accounting_basis`: এই card Accrual ধরে লেখা)
- OD-16 (cash basis-এর সিদ্ধান্ত: নিচে দেখুন)

**Relation: আগের task**
- `P3-01`: `movements(from, to)` আর `netProfit`। P&L শুধু `type IN ('revenue','expense')` account-এর movement চায়।
- `P1-03`: `accounting_basis` (`accrual` বা `cash`, lock হয়), `report_comparison_enabled` (default `true`)।
- `P1-06`: CoA-র group গাছ (উপমোটের জন্য)।
- `P1-11`/`P2-14`: opening journal-এ Revenue বা Expense account থাকলে সাধারণভাবে ধরা হয় (নিচে OD-23)।
- `P1-12`: error enum। **নতুন code `RPT_BASIS_UNSUPPORTED` (422) এই task enum-এ যোগ করে** (Pack ১০.৩-এর নতুন code তালিকা)।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P3-06` (Balance Sheet) এই task-এর `netProfit` ও P&L logic ব্যবহার করে Equity-র দুটো উপাদান বানায়। তাই `P3-06` শুরুর আগে এটা শেষ।
- `P3-13` RC5: P&L-এর নিট লাভ = TB-র Revenue ও Expense পার্থক্য।
- `P3-08` (Indirect Cash Flow): Operating অংশের শুরু এই P&L-এর সময়ের নিট লাভ থেকে।
- `P3-10`: `profit-loss` export।
- `P3-15`: accountant নমুনা company-র P&L হাতে মেলায়, আর OD-16 (ক) হলে নতুন card `P3-05b` (cash basis) লাগবে, যা এই Pack-এ নেই।
- **খোলা প্রশ্ন OD-23:** opening balance-এ Revenue বা Expense account থাকতে পারবে কি, আর P&L-এ opening journal-এর line ধরা হবে কি। বিকল্প: (ক) যেমন আছে, কোনো বিশেষ আচরণ নেই (এই Pack-এর ধরে নেওয়া, এই card এভাবেই), (খ) opening-এ শুধু Asset/Liability/Equity (`P1-11`-এর validator বাড়ে), (গ) opening-এর Revenue ও Expense line P&L থেকে বাদ। (ক)-তে বছরের মাঝে cutover হলে চালু-মাসের P&L ফুলে যেতে পারে। সিদ্ধান্ত `P3-15`-এর আগে লিখিত হতে হবে (Accountant)।
- **খোলা প্রশ্ন OP-6:** OD-1 ও OD-16-এর বিকল্পগুলোর বিস্তারিত Pack-এ নেই (Blueprint-এর OD register-এ)। এই card-এ ধরা হয়েছে (খ): cash basis পরের ধাপ।

**Related tables / schema**
কোনো নতুন table নেই।

**Business rules (R3.1, R3.2, R3.4)**
- শুধু `revenue` ও `expense` type।
- Revenue = Credit − Debit। Expense = Debit − Credit।
- `net_profit = মোট আয় − মোট খরচ`।
- Group অনুযায়ী উপমোট (গাছ ধরে)।
- **Basis:** `accounting_basis = 'accrual'` হলে `journal_date`-এর ওপর (এই card)। `cash` হলে এখন `RPT_BASIS_UNSUPPORTED` (422), কারণ cash basis আলাদা logic, OD-16-এর (খ) অনুযায়ী পরের ধাপ। OD-16 (ক) হলে নতুন card `P3-05b` লাগবে।
- **Comparison:** `report_comparison_enabled` চালু থাকলে `compare=previous_period` (একই দৈর্ঘ্যের আগের সময়) বা `previous_year`।
- Reversal-সহ নিট প্রভাব শূন্য (কারণ `reversed` ও reversal দুটোই ধরা)।
- Opening journal-এ Revenue বা Expense থাকলে সাধারণভাবে ধরা হয়, বিশেষ আচরণ নেই (OD-23 সম্ভাব্য সমস্যা)।

**DB flow**
1. Snapshot।
2. `accounting_basis` পড়া; `cash` হলে `RPT_BASIS_UNSUPPORTED`।
3. `movements(from, to)` শুধু `type IN ('revenue','expense')` account-এর।
4. Revenue = Credit − Debit, Expense = Debit − Credit; গাছ ধরে group উপমোট।
5. `compare` চাইলে (এবং setting চালু) একই হিসাব আগের সময়ের জন্য, পরিবর্তন (পার্থক্য) সহ।
6. Audit `accounting.report.run`।

**Task scope**
1. Movement সংগ্রহ।
2. Group উপমোট।
3. `compare` (`previous_period`, `previous_year`)।
4. Basis পরীক্ষা ও নতুন error code।
5. API ও UI।
6. Test।

**Out of scope**
- Cash basis P&L (OD-16 (খ) অনুযায়ী পরের ধাপ; (ক) হলে `P3-05b`)।
- Opening journal-এর বিশেষ আচরণ (OD-23 (খ)/(গ) এই card-এ নেই)।
- Export (`P3-10`)।

**Contract**
`GET /accounting/reports/profit-loss?from=&to=&compare=`

Response:
- Revenue ও Expense আলাদা অংশে, প্রতিটায় group অনুযায়ী উপমোট
- মোট আয়, মোট খরচ
- `net_profit` (= মোট আয় − মোট খরচ)
- `compare` চাইলে আগের সময়ের পাশাপাশি আর পরিবর্তন

UI:
- সময় বাছাই, Revenue ও Expense অংশ, উপমোট।
- নিট লাভ হাইলাইট (ক্ষতি হলে ঋণাত্মক ও লাল)।
- তুলনা toggle।
- প্রতিটা account-এ click → GL (`P3-03`)।

**Permission**
`accounting.report.view`

**Error**
`RPT_BASIS_UNSUPPORTED` (422, নতুন), `ACC_FORBIDDEN`, `ACC_COMPANY_FORBIDDEN`

**Audit**
`accounting.report.run`

**Acceptance**
AC3.2, AC3.5, AC3.6

**Test**
- নমুনা data-য় হাতে হিসাব।
- Reversal-সহ যোগফল শূন্য প্রভাব।
- তুলনা (`previous_period`, `previous_year`)।
- `cash` basis-এ `RPT_BASIS_UNSUPPORTED`।
- ক্ষতি (ঋণাত্মক `net_profit`)।
- `report_comparison_enabled` বন্ধ থাকলে `compare` কাজ করে না।

**Deliverable**
API, UI

**Sub-tasks**
movement, group উপমোট, compare, basis check, API, UI, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC3.2 (P&L অংশ), AC3.5, AC3.6 সবুজ।
- `RPT_BASIS_UNSUPPORTED` `AccountingError` enum-এ (code ও 422 status সহ)।
- P&L service পুনঃব্যবহারযোগ্য (`P3-06`, `P3-13` ডাকবে)।

**Architecture constraints**
- Accrual ধরে লেখা; `cash` basis চুপচাপ accrual হিসেবে চালানো যাবে না, স্পষ্ট error।
- সব হিসাব `LedgerQueryService` দিয়ে।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P3-06 — Balance Sheet

Project
ERPFlow
Module
Accounting Module
Type
Feature (api + ui)
Title
Balance Sheet — Backend ও Frontend
Branch Name
`feature/accounting-report-balance-sheet`
View Order
43
Description
**Summary**
নির্দিষ্ট তারিখে Asset, Liability, Equity দেখে Asset = Liability + Equity সমীকরণ মিলছে কি না বোঝা যাবে।

**Task serial**
`ACC-P3-06 — Balance Sheet` · Feature

**Depends on**
- `ACC-P3-01`
- `ACC-P3-05`
- OD-1 (accrual)
- OD-20 (বছর-শেষ closing: ধরা হয়েছে (ক) গতিশীল হিসাব, বছর-শেষে আলাদা closing journal ছাড়া)

**Relation: আগের task**
- `P3-01`: `balancesAsOf` (asset/liability/equity) আর `netProfit(from, to)`।
- `P3-05`: P&L-এর নিট লাভ logic (Equity-র (২) ও (৩) উপাদান)।
- `P1-08`: `acc_periods`, `PeriodResolver::forDate` ও `fiscal_year`: `as_of` কোন আর্থিক বছরে পড়ে তা বের করতে।
- `P1-03`: `fiscal_year_start` (আর্থিক বছরের শুরু, যেমন জুলাই-শুরু) ও `base_currency`।
- `P1-06`: CoA গাছ।
- `P2-14`: Opening journal সাধারণ Post হয়ে Equity/Asset/Liability-র শুরুর balance বসায়।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P3-08` (Cash Flow Statement): শেষের নগদ Balance Sheet-এর নগদ-সমতুল্য account-এর যোগফলের সাথে মেলাতে হয় (RC4); Indirect পদ্ধতি `balancesAsOf` ও P&L-এর ওপর।
- `P3-13`: RC3 (Asset = Liability + Equity) এবং RC4-এর অংশ।
- `P3-10`: `balance-sheet` export।
- `P3-15`: accountant একাধিক আর্থিক বছরের Equity হাতে মেলাবে।
- `P5-02` (Cutover): ধাপ ৭, Balance Sheet মেলানো।
- **খোলা প্রশ্ন OD-20:** বছর-শেষের closing journal না থাকায় আগের বছরের লাভ প্রতিবার হিসাব হয়। Retained Earnings account-এ (যেটা system account `retained_earnings`) হাতে করা journal থাকলে তা Equity-র (১)-এ আসে আর (২)-এর সাথে যোগ হয়। এটা double count কি না তা accountant দেখবে। সিদ্ধান্ত লিখিত হতে হবে `P3-15`-এর আগে।
- **খোলা প্রশ্ন OP-7:** `netProfit(inception, FY শুরু − 1)`-এ `inception` Pack-এ সংজ্ঞায়িত নয়। ধরা হলো: নিচের সীমা ছাড়া (company-র প্রথম journal থেকে, বা যেকোনো `from` যা সব ledger ধরে)। `LedgerQueryService::netProfit`-এর signature-এ এটা সমর্থন করতে হবে।
- **খোলা প্রশ্ন OP-8:** `as_of` কোনো period-এ না পড়লে আচরণ Pack-এ নেই। `PeriodResolver::forDate` (`P1-08`) `JRN_DATE_INVALID` দেয়; সেটাই ধরা হলো (অনুমান)।

**Related tables / schema**
কোনো নতুন table নেই।

**Business rules (R3.6, RC3)**
Equity-র গঠন:
1. Equity type-এর account-এর balance।
2. **আগের বছরগুলোর জমানো নিট লাভ:** এই আর্থিক বছর শুরুর আগের দিন পর্যন্ত সব Revenue ও Expense-এর পার্থক্য: `netProfit(inception, FY শুরু − 1)`।
3. **চলতি বছরের নিট লাভ:** `netProfit(FY শুরু, as_of)`।

আর্থিক বছরের শুরু = `as_of` যে period-এ পড়ে সেই period-এর `fiscal_year`-এর প্রথম period-এর `start_date`।

- Equity = (১) + (২) + (৩)।
- Asset-এ Credit balance বা Liability-তে Debit balance ঋণাত্মক অঙ্কে দেখানো হয়, অন্য অংশে সরানো হয় না।
- **RC3:** মোট Asset = মোট Liability + মোট Equity। না মিললে লাল ব্যানার, পার্থক্য সহ (report আটকায় না)।
- বছর-শেষের closing journal না থাকায় আগের বছরের লাভ প্রতিবার হিসাব হয় (OD-20 (ক))।

**DB flow**
1. Snapshot।
2. `balancesAsOf(as_of)` শুধু `asset`, `liability`, `equity` type।
3. `netProfit` দুটো: আগের বছরগুলোর, আর চলতি বছরের।
4. Equity = (১) + (২) + (৩)।
5. RC3: মোট Asset = মোট Liability + মোট Equity।
6. Audit `accounting.report.run`।

**Task scope**
1. Asset-Liability-Equity হিসাব।
2. আগের বছরের লাভ (`netProfit(inception, FY শুরু − 1)`)।
3. চলতি লাভ (`netProfit(FY শুরু, as_of)`)।
4. আর্থিক বছরের শুরু বের করা (`PeriodResolver` ও `fiscal_year`)।
5. RC3 ও `balanced` flag।
6. তুলনা (আগের বছরের শেষ)।
7. API ও UI।
8. Test।

**Out of scope**
- বছর-শেষের closing journal (OD-20 (ক) অনুযায়ী নেই)।
- Export (`P3-10`)।
- RC3 সংরক্ষণ ও রাতের চালানো (`P3-13`)।

**Contract**
`GET /accounting/reports/balance-sheet?as_of=&compare=`

Response: Asset, Liability, Equity অংশ, প্রতিটায় group উপমোট; মোট Asset, মোট Liability, মোট Equity; `balanced` flag (RC3)।

UI:
- তারিখ বাছাই।
- তিন অংশ।
- Equity-তে তিন উপাদান আলাদা সারিতে।
- `balanced` চিহ্ন।
- তুলনা (আগের বছরের শেষ)।
- প্রতিটা account-এ click → GL।

**Permission**
`accounting.report.view`

**Error**
`ACC_FORBIDDEN`, `ACC_COMPANY_FORBIDDEN`; `RPT_RECON_FAILED` শুধু তথ্য (RC3)।

**Audit**
`accounting.report.run`

**Acceptance**
AC3.2, AC3.3, AC3.5, AC3.6

**Test**
- নমুনা data-য় হাতে হিসাব।
- একাধিক আর্থিক বছর (আগের বছরের লাভ Equity-তে)।
- আর্থিক বছর জুলাই-শুরু।
- RC3 সবসময় মেলে সুষম data-য়।
- ইচ্ছা করে ভাঙা data-য় লাল।
- Asset-এ Credit balance ঋণাত্মক অঙ্কে।

**Deliverable**
API, UI

**Sub-tasks**
Asset-Liability-Equity হিসাব, আগের বছরের লাভ, চলতি লাভ, RC3, API, UI, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC3.2, AC3.3, AC3.5, AC3.6 সবুজ।
- `BalanceSheetEquationTest` (RC3) পাশ।
- BS service পুনঃব্যবহারযোগ্য (`P3-08`, `P3-13` ডাকবে)।

**Architecture constraints**
- সব হিসাব `LedgerQueryService` দিয়ে।
- ঋণাত্মক balance অন্য অংশে সরানো যাবে না।
- Equity-র তিন উপাদান আলাদা রাখা, একত্র করে লুকানো নয়।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Feature (service + api + ui)
Title
Cash Flow mapping UI ও যাচাই — Backend ও Frontend
Branch Name
`feature/accounting-cashflow-mapping`
View Order
44
Description
**Summary**
Admin নিজেই ঠিক করবে কোন account নগদ-সমতুল্য, বাকিগুলো Operating / Investing / Financing-এর কোন ভাগে — Cash Flow কোনো অনুমানে চলবে না।

**Task serial**
`ACC-P3-07 — Cash Flow mapping UI ও যাচাই` · Feature

**Depends on**
- `ACC-P1-07` (CoA API ও UI)
- `ACC-P1-04` (Settings UI ও দ্বিতীয় অনুমোদন)
- OD-2 (কোন account নগদ-সমতুল্য, আর Cash Flow-এর পদ্ধতি)

**Relation: আগের task**
- `P1-06`: `acc_accounts.cashflow_class` (`operating`/`investing`/`financing`, nullable, `ck_acc_accounts_cf`) আর `is_cash_equivalent` (BOOLEAN, default false) column। এই task নতুন column বানায় না, বিদ্যমানগুলো ভরার জায়গা দেয়।
- `P1-07`: account তালিকা/গাছ UI আর `PUT /accounts/{id}` (`version` সহ) API; এই task-এর UI সেই গাছের ওপর।
- `P1-03`: `SettingsService` ও `cash_equivalent_accounts` setting (account id-র তালিকা, default খালি, lock নেই কিন্তু audit সহ)। `sensitive_settings` default তালিকায় `cash_equivalent_accounts` আছে।
- `P1-04`: সংবেদনশীল setting বদলাতে দ্বিতীয় অনুমোদন (change request): `POST /accounting/settings/{key}/change-requests`, অনুমোদনকারী ভিন্ন ব্যক্তি, `SET_STALE_REQUEST`।
- `P1-12`: error, audit।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P3-08` (Cash Flow Statement): `CashflowMappingService::missing` খালি না হলে Cash Flow চলে না (`RPT_MAPPING_INCOMPLETE`)। এই task শেষ না হলে `P3-08` শুরুই হয় না।
- `P3-13` RC4: CF-এর শেষের নগদ BS-এর নগদ-সমতুল্য account-এর যোগফলের সাথে।
- `P3-14`: AC3.11 (`CashflowMappingTest`)।
- `P3-15`: accountant এই mapping সই করে।
- `P1-05`-এর ধাপ `S11_cashflow_mapping` ("নগদ account ও `cashflow_class` ঠিক", Cash Flow চালু হলে বাধ্যতামূলক): একই শর্ত।
- **খোলা প্রশ্ন OP-9:** `S11`-এর checker `CashflowMappingService::missing` ডাকবে কি না Pack-এ স্পষ্ট লেখা নেই (শর্ত একই বলা আছে)। ধরা হলো ডাকবে; `P1-05`-এর checker-কে এই service-এ সংযুক্ত করা এই task-এর scope-এ ছোট একটা কাজ হিসেবে ধরা যায়, tech lead নিশ্চিত করবে।
- **খোলা প্রশ্ন OP-10:** Pack এই card-এ শুধু service method ও UI-র কথা বলে, API route-এর নাম/আকার লেখে না (অন্য card-এর মতো)। Route নাম implementation-এ ঠিক হবে, PR-এ লিখতে হবে।
- **খোলা প্রশ্ন OD-2:** নগদ-সমতুল্য কোন account, আর পদ্ধতি (`direct`/`indirect`/দুটোই (গ))। `cashflow_method` setting এখনো বাধ্যতামূলক ("Cash Flow চালু করতে")। এই task সরাসরি পদ্ধতির ওপর নির্ভর করে না, কিন্তু OD বন্ধ না হলে `Blocked`।

**Related tables / schema**
কোনো নতুন table নেই।
- `acc_accounts.cashflow_class`, `acc_accounts.is_cash_equivalent` (`P1-06`)।
- **একটাই উৎস:** `cash_equivalent_accounts` setting (সংবেদনশীল, দ্বিতীয় অনুমোদন লাগে)। `is_cash_equivalent` শুধু তার **প্রতিফলন**, যা `SettingsService` সেই একই transaction-এ আপডেট করে। কেউ সরাসরি এই flag বদলাতে পারে না।

**Business rules**
`CashflowMappingService::missing` যাচাই:
1. নগদ-সমতুল্য account-গুলো `asset` type আর leaf হতে হবে।
2. নগদ নয় এমন প্রতিটা `asset`, `liability`, `equity` account (leaf, active অথবা balance আছে) এর `cashflow_class` থাকতে হবে।
3. `revenue` ও `expense` account-এর `cashflow_class` ফাঁকা থাকলে ধরা হয় `operating`; আলাদা মান দিলে (যেমন সম্পদ বিক্রির লাভ `investing`) সেটাই।

অসম্পূর্ণ হলে ফেরত দেয় কোন কোন account বাকি।

- `cashflow_class` বদল: কোনো lock নেই, Posted data থাকলেও বদলানো যায় (এতে report-এর ফল বদলায়, তাই UI আগে সতর্ক করে আর audit-এ আগে ও পরে)।
- Group account-এ ভাগ বেছে নিলে সব descendant-এ প্রযোজ্য।
- নগদ-সমতুল্য চিহ্ন বদলাতে গেলে setting-এর দ্বিতীয় অনুমোদনের পথে যায় (`P1-04`), সরাসরি লেখা যায় না।
- `is_cash_equivalent`-এর একমাত্র লেখক `SettingsService`। `P1-06`-এর `AccountService::update` এবং `P1-07`-এর `PUT /accounts/{id}` দিয়ে এই flag বদলানো আটকাতে হবে।

**DB flow: `cashflow_class` বদল**
1. transaction।
2. account row `FOR UPDATE`।
3. `version` মেলানো (না মিললে `JRN_CONFLICT`)।
4. `UPDATE` (`version + 1`)।
5. Audit `accounting.cashflow.mapping_changed` (আগে ও পরে সহ)।
6. commit।

**DB flow: নগদ-সমতুল্য চিহ্ন বদল**
`cash_equivalent_accounts` setting বদলের অনুরোধ (`P1-04`) → অন্য ব্যক্তির অনুমোদন → `SettingsService` একই transaction-এ setting আর `is_cash_equivalent` আপডেট করে। Non-asset বা group account হলে `SET_INVALID`।

**Task scope**
1. `CashflowMappingService::missing`।
2. সুরক্ষা: `is_cash_equivalent`-এর একমাত্র লেখক `SettingsService`।
3. `cashflow_class` update (lock, version, audit)।
4. Group-এ প্রয়োগ (descendant)।
5. UI: সব account-এর তালিকা, দুই কলাম, "বাকি আছে: N টা account" গণনা ও ফিল্টার।
6. API।
7. Test।

**Out of scope**
- Cash Flow Statement নিজে (`P3-08`)।
- `cashflow_method` setting বদলানো (সাধারণ settings পাতা, `P1-04`)।
- Setup Checklist পাতা (`P1-05`)।

**Contract**
- Service: `CashflowMappingService::missing(companyId)` → বাকি account-এর তালিকা।
- API route-এর নাম Pack-এ নির্ধারিত নেই (OP-10)।
- `cash_equivalent_accounts` বদল `P1-04`-এর change-request endpoint দিয়ে।

UI:
- সব account-এর তালিকা, দুটো কলাম: "নগদ-সমতুল্য" (টিক, বদলাতে গেলে setting-এর দ্বিতীয় অনুমোদনের পথে যায়) আর "Cash Flow ভাগ" (ড্রপডাউন)।
- Group account-এ ভাগ বেছে নিলে সব descendant-এ প্রযোজ্য।
- উপরে "বাকি আছে: ১২টা account" গণনা আর ফিল্টার।
- ভাগ বদলের আগে সতর্কতা (report-এর ফল বদলাবে)।

**Permission**
`accounting.cashflow.manage` (ভাগ বদল)। নগদ-সমতুল্য চিহ্নের অনুরোধ ও অনুমোদন `accounting.settings.manage`/`accounting.settings.approve` (`P1-04`)।

**Error**
`RPT_MAPPING_INCOMPLETE` (422), `SET_INVALID` (422); পথে `JRN_CONFLICT`, `SET_STALE_REQUEST`, `JRN_SOD_VIOLATION` (`P1-04` থেকে)।

**Audit**
`accounting.cashflow.mapping_changed` (mapping বা নগদ চিহ্ন বদল); setting পথে `accounting.setting.change_requested/.approved/.rejected`, `accounting.setting.changed` (`P1-03`, `P1-04`)।

**Acceptance**
AC3.11

**Test**
- অসম্পূর্ণ তালিকা ঠিক ধরা।
- নগদ-সমতুল্য non-asset account-এ আপত্তি।
- দ্বিতীয় অনুমোদন ছাড়া নগদ চিহ্ন বদলানো যায় না।
- Group-এ প্রয়োগ।
- `is_cash_equivalent` সরাসরি account API দিয়ে বদলানো যায় না।
- Revenue/Expense-এর ফাঁকা `cashflow_class` Operating ধরা।
- দুজন একসাথে `cashflow_class` বদলালে `JRN_CONFLICT`।

**Deliverable**
Service, API, UI

**Sub-tasks**
`missing`, সুরক্ষা (flag-এর একমাত্র লেখক `SettingsService`), `cashflow_class` update, UI, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC3.11 সবুজ (`CashflowMappingTest`)।
- `is_cash_equivalent` ও `cash_equivalent_accounts` কখনো অমিল হতে পারে না (test প্রমাণ)।
- `missing` ফল `P3-08` সরাসরি ব্যবহার করতে পারে।

**Architecture constraints**
- `cash_equivalent_accounts` একটাই উৎস; `is_cash_equivalent` শুধু প্রতিফলন।
- কোনো account-এর ভাগ অনুমান করা যাবে না (R3.8 এর ভিত্তি)।
- Mapping বদল সবসময় audit-এ, আগে ও পরের মান সহ।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P3-09 — অন্য report (Account Summary, Opening Balance, Audit, Register-এর পূর্ণ রূপ)

Project
ERPFlow
Module
Accounting Module
Type
Feature (api + ui)
Title
অন্য report — Backend ও Frontend
Branch Name
`feature/accounting-report-others`
View Order
45
Description
**Summary**
মূল পাঁচটা report ছাড়াও Account Summary, Opening Balance, Audit Report আর Journal/Approval Register-এর পূর্ণ রূপ লাগবে — দৈনন্দিন কাজ আর নিরীক্ষায় ফাঁক থাকবে না।

**Task serial**
`ACC-P3-09 — অন্য report` · Feature

**Depends on**
- `ACC-P3-01`
- `ACC-P2-15` (Queue ও Register)

**Relation: আগের task**
- `P3-01`: Account Summary-র `movements` ও `balancesAsOf`।
- `P2-15`: Journal Register ও Approval Register (filter: অবস্থা, ধরন, তারিখ পরিসর, voucher নম্বর, reference, account, amount পরিসর, Maker, Approver; `id` ধরে cursor pagination)। এই task তাদের **export-সহ পূর্ণ রূপ** দেয়, নতুন করে লেখে না।
- `P1-09`/`P1-11`/`P2-14`: opening journal (`journal_type = 'opening'`) ও তার line, Draft/Submitted/Posted অবস্থা।
- `P1-12`: `activity_logs`-এ লেখা সব `accounting.*` event (`AccountingAudit` দিয়ে)।
- `P1-02`: `accounting.audit.view` permission (Phase 1 থেকে), `accounting.report.view`।
- `P2-05`/`P2-08`: `acc_approval_requests`, `acc_approval_actions` (Approval Register-এর উৎস)।
- `P0-02`: `activity_logs`-এর index বড় table-এ যথেষ্ট কি না, সেই নিশ্চিতকরণ।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P3-10`: পাঁচটা report-ই export-যোগ্য (`account-summary` ইত্যাদি)।
- `P3-13`: এই task অবরুদ্ধ করে (build sequence-এ)। সরাসরি RC নেই, কিন্তু সব report service একসাথে `P3-13`-এর আগে তৈরি।
- `P3-14`/`P3-15`: AC3.6, AC3.9।
- `P5-03` (monitoring): Audit Report-এর মতো `activity_logs` ব্যবহার করে (`post_failures`, `locked_post_attempts` alert)।
- `P5-02`: Opening Balance Report cutover মহড়ায় দরকার।
- **খোলা প্রশ্ন OP-11:** `P3-13`-এর `নির্ভর করে`-তে `P3-09` নেই (শুধু `P3-03` থেকে `P3-06`, `P3-08`) কিন্তু `P3-09`-এর `অবরুদ্ধ করে`-তে `P3-13` আছে। নিয়ম: dependency দুই দিকের ফারাক, build sequence (`P3-09` ক্রম ২৬, `P3-13` ক্রম ২৮) অনুসরণ করা হলো।
- **খোলা প্রশ্ন OP-12:** Account Summary-র `group` parameter-এর অর্থ (account group ধরে ভাগ করা?) Pack-এ বিস্তারিত নেই; ধরা হলো `account_group_id`-র মতো descendant ধরে যোগ।

**Related tables / schema**
কোনো নতুন table নেই। পড়া: `acc_journals`, `acc_journal_lines`, `acc_approval_requests`, `acc_approval_actions`, `activity_logs`।

| 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`) |
| Journal Register (পূর্ণ) | `P2-15`-এর filter-সহ, এখন export-সহ | সব journal | `acc_journals` |
| Approval Register (পূর্ণ) | `P2-15` + export | Pending, Approved, Rejected, Posted | `acc_approval_requests`, `acc_approval_actions` |

**Business rules**
- Opening Balance Report শুধু opening ধরনের journal দেখায় (Draft সহ)।
- Audit Report শুধু `accounting.audit.view` permission-ওয়ালাকে।
- সব report company-scoped (Audit Report-এ অন্য company-র event আসে না)।
- Audit Report-এ বড় table-এ `activity_logs`-এর index যথেষ্ট কি না `P0-02`-এ দেখা; নইলে index যোগ।
- Account Summary শুধু ledger-এর অবস্থা (`posted`, `reversed`) ধরে।

**DB flow**
- Account Summary: snapshot → `movements(from, to)` ও `balancesAsOf(to)` → account (বা `group`) ধরে Debit, Credit, শেষের balance।
- Opening Balance Report: `acc_journals WHERE company_id = ? AND journal_type = 'opening'` → line, অবস্থা, মোট Debit/Credit ও পার্থক্য।
- Audit Report: `activity_logs WHERE company_id = ? AND event LIKE 'accounting.%'` + filter (`actor`, `event`, `from`, `to`, `journal`)।
- Register: `P2-15`-এর query, শুধু export সংযোগ।
- প্রতিটায় audit `accounting.report.run`।

**Task scope**
1. Account Summary API ও UI।
2. Opening Balance Report API ও UI।
3. Audit Report API ও UI, `activity_logs` index যাচাই (দরকারে index migration)।
4. Journal Register ও Approval Register-এ export সংযোগ।
5. Test।

**Out of scope**
- Export ফাইল তৈরি (`P3-10`)।
- নতুন audit event বানানো।
- Dimension filter (Phase 6)।

**Contract**
- `GET /accounting/reports/account-summary?from=&to=&group=`
- `GET /accounting/reports/opening-balance`
- `GET /accounting/reports/audit?actor=&event=&from=&to=&journal=`
- Journal Register, Approval Register: `P2-15`-এর endpoint (`GET /accounting/journals?...`, `GET /accounting/approvals/register?...`) এখন export-সহ।

**Permission**
`accounting.report.view`; Audit Report-এ `accounting.audit.view`।

**Error**
`ACC_FORBIDDEN`, `ACC_COMPANY_FORBIDDEN`

**Audit**
`accounting.report.run`

**Acceptance**
AC3.6, AC3.9

**Test**
- প্রতিটা report নমুনা data-য় ঠিক।
- Draft opening-ও দেখায়।
- Audit Report-এ অন্য company-র event আসে না।
- `accounting.audit.view` ছাড়া Audit Report-এ `ACC_FORBIDDEN`।
- Account Summary `reversed` ধরে।

**Deliverable**
চারটা API, UI

**Sub-tasks**
প্রতিটা report আলাদা API ও UI, Audit Report-এর index যাচাই, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC3.6, AC3.9 (এই report-গুলোর অংশ) সবুজ।
- `activity_logs` index যাচাইয়ের ফল PR-এ লেখা (লাগলে migration)।

**Architecture constraints**
- Audit Report কখনো অন্য company-র event দেখাবে না।
- Account Summary `LedgerQueryService` ছাড়া নিজের SQL নয়।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P3-08 — Cash Flow Statement

Project
ERPFlow
Module
Accounting Module
Type
Feature (service + api + ui)
Title
Cash Flow Statement — Backend ও Frontend
Branch Name
`feature/accounting-report-cash-flow`
View Order
46
Description
**Summary**
নির্বাচিত সময়ে নগদ কীভাবে এলো-গেল Operating, Investing, Financing ভাগে দেখা যাবে; শুরু ও শেষের নগদ মিলছে কি নাও পরিষ্কার হবে।

**Task serial**
`ACC-P3-08 — Cash Flow Statement` · Feature

**Depends on**
- `ACC-P3-01`
- `ACC-P3-07`
- `ACC-P3-06`
- OD-1, OD-2 (দুটো বন্ধ না হলে এই card শুরু হয় না: acceptance-এ আটকাবে, stub চলবে না)

**Relation: আগের task**
- `P3-01`: `balancesAsOf` (নগদ account-এ শুরু ও শেষের নগদ), `movements`, snapshot।
- `P3-07`: `CashflowMappingService::missing` (খালি না হলে report চলে না) আর প্রতিটা account-এর `cashflow_class` ও নগদ-সমতুল্য চিহ্ন।
- `P3-06`: Balance Sheet-এর `balancesAsOf` ও নগদ-সমতুল্য যোগফল (RC4 এর অংশ)।
- `P3-05`: P&L-এর সময়ের নিট লাভ (Indirect পদ্ধতির Operating-এর শুরু), যদিও card-এ নির্ভরতা `P3-06` মারফত।
- `P1-03`: `cashflow_method` (`direct` বা `indirect`), `cash_equivalent_accounts`।
- `P1-12`: error `RPT_MAPPING_INCOMPLETE`।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P3-13`: RC4: CF-এর শেষের নগদ = Balance Sheet-এর নগদ-সমতুল্য account-এর যোগফল, আর তিন ভাগের মোট = নিট পরিবর্তন।
- `P3-10`: `cash-flow` export।
- `P3-14`: AC3.2, AC3.4 (`CashFlowReconciliationTest`), AC3.11।
- `P3-15`: accountant তাঁর হাতে করা Cash Flow-এর সাথে মেলান (নমুনা company)।
- **খোলা প্রশ্ন OD-2:** Deliverable "OD-2 যেটা চায় (গ) হলে দুটোই": কোন পদ্ধতি বা দুটোই, তা OD-2 বন্ধের সময় ঠিক হবে। `cashflow_method` setting-এ থাকবে; দুটো করার সিদ্ধান্ত হলে দুই পদ্ধতিই এই card-এর scope।
- **খোলা প্রশ্ন OD-1:** accounting basis; Cash Flow `accounting_basis` অনুযায়ী আচরণ বদলায় কি না Pack-এ বলা নেই, শুধু OD-1 বন্ধ থাকতে হবে।
- **খোলা প্রশ্ন OP-13:** `P3-08` build sequence-এ `P3-05` সরাসরি নির্ভরতা হিসেবে নেই, কিন্তু Indirect নিট লাভ `P3-05`-এর সময়ের নিট লাভ ধরে। `P3-06` মারফত (যার `P3-05` লাগে) পরোক্ষ নির্ভরতা।

**Related tables / 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`)। **দুই পদ্ধতির মোট একই হবে।**

**Business rules (R3.8, R3.9)**
- কোনো account-এর ভাগ অনুমান করা হয় না। ভাগহীন account থাকলে `RPT_MAPPING_INCOMPLETE` (তালিকা সহ), report চলে না।
- তিন ভাগের মোট = নিট পরিবর্তন কি না (RC4-এর অংশ)। না মিললে লাল সতর্কতা আর পার্থক্য (report আটকায় না, R3.9)।
- পদ্ধতি `cashflow_method` setting থেকে; এই report-এর UI-তে বদলানো যায় না।
- মিশ্র journal (এক journal-এ একাধিক ভাগ) প্রতিটা অ-নগদ line আলাদা ভাগে যায়।

**DB flow**
1. Snapshot।
2. `CashflowMappingService::missing` খালি না হলে `RPT_MAPPING_INCOMPLETE` (তালিকা সহ), report চলে না।
3. পদ্ধতি অনুযায়ী উপরের ধাপ।
4. শুরুর ও শেষের নগদ।
5. তিন ভাগের মোট = নিট পরিবর্তন কি না (RC4-এর অংশ); না মিললে লাল সতর্কতা আর পার্থক্য।
6. Audit `accounting.report.run`।

**Direct-এ query:** একটা query দিয়ে সংশ্লিষ্ট journal id:

```sql
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 পড়ে হিসাব।

**Task scope**
1. নগদ হিসাব (শুরু, শেষ, নিট পরিবর্তন)।
2. Direct পদ্ধতি।
3. Indirect পদ্ধতি।
4. মিল পরীক্ষা (তিন ভাগ = নিট পরিবর্তন)।
5. API।
6. UI ও drill-down।
7. Test।

**Out of scope**
- Mapping UI (`P3-07`)।
- `cashflow_method` setting বদলানো।
- Cash Flow Statement-এর Cash basis রূপ।
- RC4 সংরক্ষণ ও রাতের চালানো (`P3-13`)।

**Contract**
`GET /accounting/reports/cash-flow?from=&to=` (পদ্ধতি `cashflow_method` setting থেকে)

UI:
- সময় বাছাই।
- পদ্ধতির নাম দেখানো (setting থেকে, এখানে বদলানো যায় না)।
- তিন ভাগ, শুরু ও শেষের নগদ, `নিট পরিবর্তন`।
- মিল সবুজ বা লাল।
- প্রতিটা লাইনে click → Direct-এ journal-এর তালিকা, Indirect-এ সংশ্লিষ্ট account-এর GL।

**Permission**
`accounting.report.view`

**Error**
`RPT_MAPPING_INCOMPLETE` (422); `RPT_RECON_FAILED` শুধু তথ্য।

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

**Definition of done**
- সাধারণ DoD পূরণ।
- AC3.2, AC3.4, AC3.11 সবুজ।
- OD-1 ও OD-2 লিখিতভাবে বন্ধ (stub দিয়ে এগোনো যাবে না)।
- দুই পদ্ধতির মোট একই, test প্রমাণ।

**Architecture constraints**
- কোনো account-এর ভাগ অনুমান নয়।
- সব সংখ্যা `LedgerQueryService` থেকে।
- OD-1 ও OD-2 বন্ধ না হলে stub দিয়ে চালানো যাবে না।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P3-13 — Reconciliation framework

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service + job + api + ui)
Title
Reconciliation framework — Backend ও Frontend
Branch Name
`feature/accounting-reconciliation`
View Order
47
Description
**Summary**
TB, GL, P&L, BS, CF, raw SQL, cache, hash — হিসাবের ভেতরের সব মিল স্বয়ংক্রিয়ভাবে বারবার চেক হবে, ফল সংরক্ষিত থাকবে; কোনো ভুল চুপচাপ পড়ে থাকতে পারবে না।

**Task serial**
`ACC-P3-13 — Reconciliation framework` · Feature

**Depends on**
- `ACC-P3-03`
- `ACC-P3-04`
- `ACC-P3-05`
- `ACC-P3-06`
- `ACC-P3-08`

**Relation: আগের task**
- `P3-03` (GL), `P3-04` (TB), `P3-05` (P&L), `P3-06` (BS), `P3-08` (CF) প্রত্যেকে পুনঃব্যবহারযোগ্য service দিয়েছে। RC1 থেকে RC5 সেই service-গুলোর **ফল তুলনা** করে। এজন্য ওই service-গুলো controller-এ নয়, আলাদা class-এ থাকা জরুরি।
- `P3-02` cache আর `balance_version`: RC7 বৈধ cache row-কে raw SQL-এর সাথে মেলায়। (build sequence-এ `P3-13`-এর `নির্ভর করে`-তে `P3-02` নেই, কিন্তু RC7 তার ওপর, পরোক্ষে `P3-04` মারফত।)
- `P1-10`: `Schema::appendOnly('acc_reconciliation_runs')` helper (table-এ UPDATE/DELETE trigger দিয়ে আটকায়) আর `acc_journals.posted_hash`: RC9 এটা মেলায়। P2-12 post-এর সময় hash বসায় (journal-এর নির্দিষ্ট column আর line `line_no` ক্রমে canonical JSON-এর SHA-256)।
- `P1-12`: `AccountingJob` (company context), `AccountingAudit`, error enum, correlation id।
- `P1-09`: `acc_journals`/`acc_journal_lines` যা RC6 ইচ্ছা করে raw SQL-এ পড়ে।
- `P3-01`: RC6-র তুলনার অপর প্রান্ত।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P3-11` (Period বন্ধ): `recon_pass` যাচাই (RC1 থেকে RC7, intake চালু থাকলে RC8, আর RC9) এই framework ডাকে, `triggered_by = 'close'`।
- `P3-14`: AC3.1, AC3.3, AC3.4, AC3.10-এর test।
- `P4-10` (RC8): একই framework-এর আরেকটা check class। একই রাতের job ও dashboard-এ।
- `P5-03` (Monitoring): `ReconciliationFailed` event আর `acc_reconciliation_runs`-এ `fail` দেখে `recon_failed` alert (যেকোনো একটা `fail`, সঙ্গে সঙ্গে)।
- `P6-13`/`P6-15`: নতুন RC sub-check (`RC1d`, `RC1m`, `RC1n`) এই framework গ্রহণ করবে। তাই check class-গুলো registry-ভিত্তিক হওয়া দরকার, hard-coded `RC1..RC9` তালিকা নয়।
- `P5-01` UAT (U08, U12): reconciliation dashboard সবুজ, ইচ্ছা করে ভাঙলে লাল।
- **খোলা প্রশ্ন OP-14:** "একটা run-এ ৯টা row, একই `started_at`" কিন্তু Phase 3-এ RC8 নেই (Phase 4-এ)। ধরা হলো: RC8-এর row `result = 'skipped'` হিসেবে লেখা হবে `P4-10` আসা পর্যন্ত (enum-এ `skipped` আছে), আর `P3-11`-এর `recon_pass` সেই `skipped`-কে ব্যর্থ ধরবে না। Tech lead নিশ্চিত করবে।
- **খোলা প্রশ্ন OP-15:** Deliverable-এ "আটটা check class (RC1..RC7, RC9)" কিন্তু schema-তে `check_code` `RC1`–`RC9` (নয়টা)। উপরের OP-14-এর মতোই।

**Related tables / 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 | হ্যাঁ | Composite FK `(company_id, period_id)` |
| `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)`।
Table `Schema::appendOnly` (`P1-10`) দিয়ে বানানো: UPDATE/DELETE কখনো নয় (`ACC_APPEND_ONLY`)।

**Check তালিকা:**

| 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 করে, সবসময় সব নয় |

**Business rules (R3.9)**
- প্রতিটা check একটা আলাদা class, নিজের snapshot transaction-এ।
- `error` মানে check নিজেই চলেনি (exception), `fail` মানে অমিল পাওয়া গেছে, `skipped` মানে প্রযোজ্য নয় (যেমন RC8 intake বন্ধ থাকলে)।
- **RC9 কৌশল:** Posted journal অনেক হতে পারে, তাই রাতে একটা period করে ঘোরানো (সব period ক্রমে, সপ্তাহে একবার সব)। চলতি আর আগের period প্রতিদিন। অমিল পাওয়া journal-এর id `detail`-এ।
- কোনো check ব্যর্থ হলে `ReconciliationFailed` event (`P5-03` alert-এ বাঁধা)। **Report আটকায় না** (R3.9; `RPT_RECON_FAILED` তথ্য, error নয়)।
- RC6 ইচ্ছা করে `LedgerQueryService`-এর ওপর নির্ভর করে না, যাতে service-এর ভুল ধরা পড়ে।
- প্রতিটা report চালানোর সময় সংশ্লিষ্ট check-গুলো (RC1 থেকে RC5) শুধু ফলের পাশে দেখানোর জন্য চলে, **সংরক্ষণ ছাড়া**।

**DB flow**
1. Run শুরু: একটা `started_at` ঠিক করা (এক run-এর সব row-এ একই)।
2. প্রতিটা check class নিজের snapshot transaction-এ চলে (`P3-01`-এর wrapper)।
3. ফল `acc_reconciliation_runs`-এ `INSERT` (একটা run-এ ৯টা row, একই `started_at`)।
4. `fail` হলে `ReconciliationFailed` event।
5. Audit `accounting.recon.run` (ফল সহ)।
6. কখনো row `UPDATE`/`DELETE` নয়।

**কখন চলে**
1. হাতে: `POST /accounting/reconciliation/run` (`triggered_by = 'manual'`)।
2. Period বন্ধের অনুরোধ ও অনুমোদনের সময় (`P3-11`, `triggered_by = 'close'`)।
3. রাতে সব সক্রিয় company-র জন্য (job, company context সহ, `triggered_by = 'schedule'`)।
4. প্রতিটা report চালানোর সময় সংশ্লিষ্ট RC1 থেকে RC5 (সংরক্ষণ ছাড়া)।

**Task scope**
1. Migration (`acc_reconciliation_runs`, `Schema::appendOnly` সহ)।
2. Check class RC1 থেকে RC7 আর RC9 (RC8 Phase 4-এ)।
3. Check registry (নতুন check নিবন্ধনের জায়গা)।
4. Runner (run-এর সব check এক `started_at`-এ, `error` ধরা)।
5. রাতের job (সব সক্রিয় company)।
6. `ReconciliationFailed` event।
7. API ও dashboard UI।
8. Test।

**Out of scope**
- RC8 check class (`P4-10`)।
- Alert পাঠানো (`P5-03`)।
- Period বন্ধ নিজে (`P3-11`)।
- RC1d/RC1m/RC1n (Phase 6)।

**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`

**Error**
`RPT_RECON_FAILED` (তথ্য হিসেবে, error নয়), `ACC_FORBIDDEN`, `ACC_COMPANY_FORBIDDEN`, `ACC_APPEND_ONLY` (সরাসরি SQL-এ বদল চেষ্টা)

**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` পথ (check-এ exception)।
- রাতের job (company context সহ)।
- RC9-এর batch (একটা period করে ঘোরানো)।
- Run-এ সব row-এর `started_at` এক।
- `acc_reconciliation_runs`-এ UPDATE/DELETE ব্যর্থ।

**Deliverable**
Migration, আটটা check class (RC1 থেকে RC7, RC9; RC8 Phase 4-এ), job, API, dashboard

**Sub-tasks**
migration, RC1 থেকে RC7, RC9, job, API, dashboard, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC3.1, AC3.3, AC3.4, AC3.10 সবুজ।
- DB guard বন্ধ করা শুধু test পরিবেশে, production কোডে এমন পথ নেই।
- নতুন check (RC8, Phase 6-এর RC1d ইত্যাদি) নিবন্ধন করা যায়।

**Architecture constraints**
- RC6 কখনো `LedgerQueryService` ডাকবে না।
- Reconciliation report আটকায় না।
- `acc_reconciliation_runs` append-only।
- প্রতিটা check নিজের snapshot transaction-এ।

Owner
Dev B
Estimate Hours
6
Status
Todo
QA Status
-
QA Comment

---

## ACC-P3-10 — Export (Excel ও PDF)

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + job + api)
Title
Export (Excel ও PDF) — Backend ও Frontend
Branch Name
`feature/accounting-report-export`
View Order
48
Description
**Summary**
Screen-এ যা দেখছেন হুবহু Excel বা PDF-এ পাবেন; ফাইল queue-তে তৈরি হবে, একই অনুরোধে দুবার আলাদা ফাইল হবে না।

**Task serial**
`ACC-P3-10 — Export (Excel ও PDF)` · Feature

**Depends on**
- `ACC-P3-03` থেকে `ACC-P3-09` সব (GL, TB, P&L, BS, Cash Flow mapping-সহ CF, Cash Flow, অন্য report)

**Relation: আগের task**
- `P3-03` থেকে `P3-09`: প্রতিটা report একটা service-class, যা screen আর export **দুজনেই** ডাকে (R3.10: screen যে service ও query ব্যবহার করে হুবহু সেটাই)। তাই export নিজের হিসাব লেখে না।
- `P3-09`: Journal Register ও Approval Register-এর export সংযোগ (পূর্ণ রূপ)।
- `P1-12`: `AccountingJob` (company context), `AccountingAudit`, error, correlation id।
- `P1-02`: `accounting.report.export` slug।
- `P3-13`: Reconciliation ব্যর্থ থাকলে ফাইলে "মেলেনি" লেখার জন্য (R3.9)। build sequence-এ `P3-13` (ক্রম ২৮) `P3-10`-এর (ক্রম ২৯) আগে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P3-13`-এর `নির্ভর করে` তালিকায় `P3-10` নেই, কিন্তু Pack `P3-10`-এর `অবরুদ্ধ করে`-তে `P3-13` লিখেছে। (ক্রমের ফারাক, নিচে OP-16।)
- `P3-14`: AC3.9 (`ExportParityTest`) আর ১ লক্ষ row export-এর সময়ের পরিমাপ।
- `P3-15`: accountant ফাইলের সংখ্যাও মেলান।
- `P5-03`: `export_failures` alert (`acc_report_exports`-এ `failed`, যেকোনো একটা)।
- `P5-01` UAT U10: Excel ও PDF-এর সংখ্যা মেলে।
- `P1-10`-এর `Schema::appendOnly` এখানে নয়; `acc_report_exports` ইতিহাস, কিন্তু অবস্থা বদলায় (`queued → done`)।
- **খোলা প্রশ্ন OP-16:** `P3-10` build sequence-এ (ক্রম ২৯) এবং `P3-13`-এর `নির্ভর করে`-তে নেই, তবুও `P3-10`-এর `অবরুদ্ধ করে`-তে `P3-13`। ধরা হলো: `P3-13` আগে বানানো হয় (ক্রম ২৮), export-এর "মেলেনি" নোট `P3-13` ফল পড়ে।
- **খোলা প্রশ্ন OP-17:** `report` column-এর সব মান Pack-এ দেওয়া হয়নি (`general-ledger`, `trial-balance` "ইত্যাদি")। প্রতিটা report-এর slug route-এর নামের সাথে মিলিয়ে ঠিক করবে (`profit-loss`, `balance-sheet`, `cash-flow`, `account-summary`, `opening-balance`, `audit`, journal/approval register)।
- **খোলা প্রশ্ন OP-18:** টাকার অঙ্ক "নির্ধারিত দশমিক সংখ্যায়" কোন সংখ্যা: ধরা হলো `currency_precision` setting। Pack শুধু "নির্ধারিত" বলে।

**Related tables / 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 | না |  |

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

**Business rules (R3.10)**
- Screen যে service ও query ব্যবহার করে **হুবহু সেটাই** export ব্যবহার করে; সংখ্যা এক থাকা বাধ্যতামূলক।
- টাকার অঙ্ক নির্ধারিত দশমিক সংখ্যায় **সংখ্যা** হিসেবে (float-এর rounding নয়)।
- ফাইল নির্দিষ্ট পথে `exports/{company}/{id}.{ext}`, আবার চললে একই পথ ওভাররাইট, দুটো ফাইল হয় না।
- Reconciliation ব্যর্থ থাকলে ফাইলে "মেলেনি" লেখা (R3.9)।
- `accounting.report.export` আলাদা permission। প্রতিটা export audit-এ।
- ব্যর্থতা: তিনবার আবার চেষ্টা (বাড়তে থাকা বিরতি), তারপর `failed`। সর্বোচ্চ row সীমা (config, প্রস্তাব ১ লক্ষ); ছাড়ালে `failed` ও বার্তা, ছোট পরিসরে ভাগ করতে বলা।
- ফাইল রাখার সময় `expires_at` পর্যন্ত (30 দিন); তারপর রাতের job ফাইল মোছে ও row-এর `file_path` খালি করে (row থাকে)।
- ডাউনলোড: company ও permission যাচাই, মেয়াদী লিংক।

**DB flow: অনুরোধ**
1. `INSERT` row `queued` (`request_hash`, `expires_at` সহ)।
2. Unique ভাঙলে চলমান row-ই ফেরত।
3. Job dispatch।
4. Audit।

**DB flow: job**
1. company context (`P1-12`)।
2. row `FOR UPDATE`, `queued` হলে `processing` (`started_at`)।
3. Screen যে service ও query ব্যবহার করে হুবহু সেটাই (R3.10)।
4. Excel বা PDF তৈরি, টাকার অঙ্ক নির্ধারিত দশমিক সংখ্যায় সংখ্যা হিসেবে।
5. ফাইল `exports/{company}/{id}.{ext}`-এ লেখা, আবার চললে একই পথ ওভাররাইট।
6. Reconciliation ব্যর্থ থাকলে ফাইলে "মেলেনি" লেখা (R3.9)।
7. `done`, `row_count`, `finished_at`।
8. Audit `accounting.report.exported`।

**Task scope**
1. Migration (`acc_report_exports`, generated column `active_key`, unique)।
2. API: অনুরোধ, অবস্থা, ডাউনলোড।
3. Job (retry, row সীমা)।
4. Excel renderer।
5. PDF renderer।
6. মেয়াদ job (রাতে ফাইল মোছা)।
7. ডাউনলোড UI।
8. Test।

**Out of scope**
- Report হিসাব নিজে (`P3-03` থেকে `P3-09`)।
- Alert পাঠানো (`P5-03`)।
- ফাইলের সংরক্ষণ-নীতি (storage backend) ERPFlow-এর বিদ্যমান ব্যবস্থা (`P0-02`)।

**Contract**
- `POST /accounting/reports/{report}/export` (`filters`, `format`)
- `GET /accounting/exports/{id}` (অবস্থা)
- `GET /accounting/exports/{id}/download` (company ও permission যাচাই, মেয়াদী লিংক)

**Permission**
`accounting.report.export`

**Error**
`ACC_FORBIDDEN`, `ACC_COMPANY_FORBIDDEN` (অন্য company-র export ডাউনলোড), `ACC_MODULE_DISABLED`; `failed` অবস্থায় `error` column-এ বার্তা (সীমা ছাড়ানো ইত্যাদি)।

**Audit**
`accounting.report.exported`

**Acceptance**
AC3.9

**Test**
- Screen ও ফাইলের সংখ্যা এক (কয়েকটা report)।
- একই অনুরোধ দুইবার (একই row)।
- Job মাঝপথে ভাঙা ও আবার চালানো (একটাই ফাইল)।
- অন্য company-র export ডাউনলোড (নিষেধ)।
- মেয়াদ পেরোনো।
- সীমার বেশি row।
- Reconciliation `fail` থাকলে ফাইলে "মেলেনি"।

**Deliverable**
Migration, job, renderer (Excel ও PDF), API, ডাউনলোড UI

**Sub-tasks**
migration, API, job, Excel renderer, PDF renderer, মেয়াদ job, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC3.9 সবুজ (`ExportParityTest`)।
- Float ব্যবহার নেই; সংখ্যা ফাইলে সংখ্যা-কোষ (text নয়)।
- একই অনুরোধে দুটো ফাইল হয় না।

**Architecture constraints**
- Export screen-এর service ছাড়া আলাদা হিসাব করবে না।
- ফাইল পথ নির্ধারিত (`exports/{company}/{id}.{ext}`)।
- Download সবসময় company ও permission যাচাই করে।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service + api + ui)
Title
Period বন্ধ ও আবার খোলা — Backend ও Frontend
Branch Name
`feature/accounting-period-close-reopen`
View Order
49
Description
**Summary**
Period যাচাই দিয়ে বন্ধ করা যাবে; আবার খুলতে হলে কারণ লিখে দুজনের সম্মতি লাগবে — বন্ধ হিসাবে কেউ চুপচাপ হাত দিতে পারবে না।

**Task serial**
`ACC-P3-11 — Period বন্ধ ও আবার খোলা` · Feature

**Depends on**
- `ACC-P1-08`
- `ACC-P3-13` (reconciliation)
- `ACC-P2-10` (SoD)
- `ACC-P3-01`
- OD-5 (period lock-এর কঠোরতা: `period_lock_mode`, `period_close_checks`)
- OD-15 (বন্ধ period-এ reversal-এর তারিখ)

**Relation: আগের task**
- `P1-08`: `acc_periods` (`status` `open`/`soft_closed`/`closed`, `closed_at`, `closed_by`, `reopened_at`, `reopened_by`, `version`: এই কয়টা column Phase 1-এ বানানো হয়েছিল "Phase 3-এ ব্যবহার" বলে), `PeriodResolver`, `uq_acc_periods_company_id` (event table-এর composite FK-র লক্ষ্য)।
- `P3-13`: `recon_pass` যাচাইয়ের জন্য RC1 থেকে RC7 (আর RC8 intake চালু থাকলে, RC9)। `triggered_by = 'close'`।
- `P2-10`: `SodGuard::assert` যার একটা নিয়ম `sod_poster_cannot_sole_close` (default `true`)। "যে journal post করেছে সে ওই period বন্ধের একমাত্র অনুমোদনকারী নয়"।
- `P3-01`: TB-এর RC1 (period-শেষ তারিখে `tb_balanced`)।
- `P3-02`: `acc_periods.balance_version` (পরে commit-এর পরে বাড়ে) আর `JournalPosted` listener। এই task-এর বন্ধ-প্রক্রিয়া সেটাই ব্যবহার করে (Pack ৯.৫)।
- `P2-12`: Post period row `FOR SHARE` নেয় (লক-ক্রম ৪); `posted_at`, `posted_by` যা এই task পড়ে।
- `P1-10`: `Schema::appendOnly` helper এবং `acc_period_events` সম্পর্কে নোট (নিচে OP-19)।
- `P1-12`: error/audit/company context।

**Relation: পরের task (কে অপেক্ষা করছে)**
- **`P3-12` (PeriodGuard):** এই task-এর `closed`/`soft_closed` অবস্থাই `period_lock_mode` ম্যাট্রিক্সের ইনপুট। এটা না থাকলে কোনো period বন্ধ হয় না, তাই `strict` mode পরীক্ষা করা যায় না।
- **`P5-02` (Cutover মহড়া):** ধাপগুলো বন্ধের সময় এই flow ব্যবহার করে।
- `P3-14`: AC3.7 (`PeriodCloseTest`), AC3.8-এর ভিত্তি।
- `P3-15`: accountant নমুনা company-তে period বন্ধ করে দেখে।
- `P5-01` UAT U09: Period বন্ধ, বন্ধ period-এ Post, আবার খোলা।
- `P5-03`: `locked_post_attempts` alert (সম্পর্কিত, `P3-12`-এর audit থেকে)।
- **খোলা প্রশ্ন OP-19:** `P1-10` `acc_period_events`-কে "পরের Phase-এ append-only table (সিদ্ধান্তের পর)" হিসেবে তালিকাভুক্ত করেছে, কিন্তু এই card-এর trigger শর্তসাপেক্ষ: `pending` অবস্থায় row বদলানো যায় (সিদ্ধান্ত লেখা হয়), শুধু `OLD.status <> 'pending'` হলে নিষেধ। তাই সরল `Schema::appendOnly` এখানে খাটে না। ধরা হলো: এই card নিজের শর্তসাপেক্ষ trigger বানায় (`P1-10`-এর একই `SQLSTATE 45000` ও error mapping ধরে), আর `P1-10`-এর তালিকার "সিদ্ধান্তের পর" শব্দটা এটাই বোঝায়। Tech lead নিশ্চিত করবে।
- **খোলা প্রশ্ন OP-20:** `period_close_sequential` (নতুন setting) Pack-এ "client চাইলে" হিসেবে প্রস্তাবিত, registry-তে (Pack ২.৪) নেই। এখন বাধ্যতামূলক নয়, তাই এই card-এ বানানো হয় না। Client চাইলে নতুন setting registry-তে যোগ হবে।
- **খোলা প্রশ্ন OP-21:** Pack-এ `soft_closed` থেকে `open`-এ ফেরার পথ (soft close তোলা) লেখা নেই; শুধু `open → soft_closed` (soft close), আর `closed → open` (reopen) আছে। `soft_closed → closed` close অনুরোধ ও অনুমোদনে (অনুমতি আছে, কারণ close অনুরোধ `open` বা `soft_closed` থেকে)। `soft_closed → open` কীভাবে হবে সিদ্ধান্তের অপেক্ষায়।
- **খোলা প্রশ্ন OP-22:** `PRD_REOPEN_DENIED` (403) "খোলার শর্ত বা অনুমোদন নেই" বলা আছে, কিন্তু কোন শর্ত ভাঙলে তা enumerated নয় (কারণ ছাড়া reopen কি এই code, নাকি validation?)। ধরা হলো: permission/অনুমোদন সংক্রান্ত না-পারা `PRD_REOPEN_DENIED`; `reason` না দেওয়া `422` validation ধরনের (field সহ)। নিশ্চিত করতে হবে।
- **খোলা প্রশ্ন OP-23:** `sod_poster_cannot_sole_close`-এ ওই period-এ কোনো posted journal না থাকলে নিয়ম প্রযোজ্য কি না (সব `posted_by` সেট খালি) Pack-এ স্পষ্ট নেই। ধরা হলো প্রযোজ্য নয় (লঙ্ঘন নেই)।
- **খোলা প্রশ্ন OP-24 (নকশায় দ্বন্দ্ব):** এই card-এর DB flow-এর ধাপ (৫) বলে অনুমোদনের সময় "period `FOR UPDATE`, যাচাই আবার চালানো"। Pack ৯.৫ বলে ভারী যাচাই (RC1–RC9) lock-এর **বাইরে**, ভেতরে শুধু `balance_version` ও `MAX(posted_at)` তুলনা। Pack নিজেই বলে "`P3-11`-এর 'যাচাই আবার চালানো' এই নকশাতেই করা হবে" তাই ৯.৫ নকশাটি নেওয়া হলো (নিচে DB flow: অনুমোদন)।
- **খোলা প্রশ্ন OD-5, OD-15:** `period_lock_mode` ও `period_close_checks` default, আর বন্ধ period-এ reversal-এর তারিখ। সিদ্ধান্ত আসা পর্যন্ত card `Blocked`।

**Related tables / schema**

**`acc_period_events`**

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

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

**ব্যবহৃত `acc_periods` column (`P1-08`):** `status`, `closed_at`, `closed_by`, `reopened_at`, `reopened_by`, `version`, `balance_version` (`P3-02`)।

**Business rules (R3.13, R3.14)**
- **Soft close:** অনুমোদন ছাড়া, `accounting.period.manage` permission-এ। Period `open` থেকে `soft_closed`। `kind = 'soft'`, সঙ্গে সঙ্গে `approved`।
- **Close অনুরোধ:** অবস্থা `open` বা `soft_closed` না হলে `JRN_STATE_INVALID`। `period_close_checks` setting-এর (default তিনটাই) যাচাই: 
  - `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, কোন পার্থক্য); **অনুরোধ তৈরিই হয় না**।
- **অনুমোদন:** `decided_by <> requested_by` আর `accounting.period.approve` permission, নইলে `JRN_SOD_VIOLATION`। `sod_poster_cannot_sole_close` চালু থাকলে: ওই period-এর posted journal-এর **সব** `posted_by` সেই একই ব্যক্তি হলে সে একমাত্র অনুমোদনকারী হতে পারে না (অনুরোধকারী ভিন্ন হলেও)। এটা `SodGuard` ডাকে।
- অনুরোধ আর অনুমোদনের মাঝে নতুন journal এসে থাকতে পারে, তাই অনুমোদনে যাচাই **আবার** চলে; ব্যর্থ হলে event `rejected` (স্বয়ংক্রিয়, কারণ সহ)।
- **আবার খোলা:** অনুরোধ (`reason` বাধ্যতামূলক, `accounting.period.manage`) ও অনুমোদন (`accounting.period.approve`, অন্য ব্যক্তি) একই কাঠামোয়। অনুমোদনে period `closed` থেকে `open`, `reopened_at`, `reopened_by`। আংশিক খোলা (শুধু নির্দিষ্ট তারিখ) এই version-এ নেই, পুরো period-ই। সংশোধনের পর আবার বন্ধ করতে হয় (নতুন close অনুরোধ)।
- আগের period বন্ধ না হলে পরেরটা বন্ধ করা যাবে কি না তা এখন বাধ্যতামূলক নয় (OP-20)। আগের period আবার খোলা হলে পরের closed period-এর `checks` বাতিল হয় না, কিন্তু সতর্কতা দেখায়।
- একটা period-এ একটাই `pending` অনুরোধ (DB unique)।
- সিদ্ধান্ত হয়ে গেলে event row অপরিবর্তনীয় (trigger)।

**DB flow: soft close**
1. `POST /accounting/periods/{id}/soft-close`।
2. transaction; period `FOR UPDATE`; অবস্থা `open` না হলে `JRN_STATE_INVALID`।
3. period `soft_closed` (`version + 1`), event `kind='soft'`, `status='approved'`।
4. Audit।
5. commit।

**DB flow: বন্ধের অনুরোধ**
1. transaction।
2. period `FOR UPDATE`।
3. অবস্থা `open` বা `soft_closed`, নইলে `JRN_STATE_INVALID`।
4. `period_close_checks` setting-এর যাচাই চালানো (উপরের তিনটা)।
5. যেকোনো একটা ব্যর্থ হলে `PRD_CLOSE_BLOCKED` আর `checks` সহ; অনুরোধ তৈরিই হয় না।
6. সবুজ হলে `acc_period_events` `INSERT` (`pending`, `checks` সংরক্ষিত)।
7. commit।

(ভারী যাচাই চালানোর সময় period lock ধরে রাখা হয় না; নিচের "বন্ধের নকশা" দেখুন।)

**DB flow: অনুমোদন (Pack ৯.৫ নকশা)**
1. event `FOR UPDATE`, `pending` কি না।
2. `decided_by <> requested_by`, `accounting.period.approve`, না হলে `JRN_SOD_VIOLATION`।
3. `sod_poster_cannot_sole_close` চালু থাকলে `SodGuard`-এর পরীক্ষা।
4. `acc_periods.balance_version` পড়া (`v`), **lock ছাড়া**।
5. ভারী যাচাই: RC1 থেকে RC9, `no_pending_journals`, `tb_balanced` ইত্যাদি, **lock ছাড়া** (snapshot transaction)।
6. যাচাই ব্যর্থ হলে event `rejected` (স্বয়ংক্রিয়, কারণ সহ), শেষ।
7. নতুন ছোট transaction শুরু: period `FOR UPDATE` (লক-ক্রম ৪; চলমান Post-এর `FOR SHARE`-এর সাথে বিরোধ, তাই চলমান Post শেষ হওয়া পর্যন্ত অপেক্ষা, নতুন Post আটকে)।
8. `balance_version` আবার পড়া।
9. `balance_version = v` হলে মাঝে কোনো Post হয়নি, তাই ধাপ ৫-এর ফল এখনো সত্য। সঙ্গে সস্তা যাচাই `no_pending_journals` ও `MAX(posted_at)` তুলনা (period-র journal-এর সর্বশেষ `posted_at` ধাপ ৫-এর সময়ের পরে হলে ব্যর্থ)। এগুলো commit-এর পরে `balance_version` বাড়ার সংক্ষিপ্ত ফাঁকে বন্ধ ফসকে যাওয়া ঠেকায়।
10. সব সবুজ হলে period `closed`, `closed_at`, `closed_by`, `version + 1`।
11. event `approved` (`decided_by`, `decided_at`)।
12. Audit।
13. commit।
14. `balance_version ≠ v` হলে lock ছেড়ে ধাপ ৪ থেকে আবার (সর্বোচ্চ তিনবার)। তারপরও না মিললে `PRD_CLOSE_BLOCKED` ("চলতি Post-এর কারণে স্থির হচ্ছে না, আবার চেষ্টা করুন")।

**DB flow: আবার খোলা**
অনুরোধ (`reason` বাধ্যতামূলক) ও অনুমোদন একই কাঠামোয়। অনুমোদনে period `closed` থেকে `open`, `reopened_at`, `reopened_by`, `version + 1`। কোনো ভারী যাচাই নেই।

**Task scope**
1. Migration ও trigger (`acc_period_events`, `pending_key` generated column, `uq_acc_pe_pending`)।
2. Soft close।
3. Close অনুরোধ (যাচাই সহ)।
4. Close অনুমোদন (পুনঃযাচাই, `balance_version` নকশা, `SodGuard`)।
5. Reopen (অনুরোধ ও অনুমোদন)।
6. API।
7. UI।
8. Test।

**Out of scope**
- `period_lock_mode`-এর প্রয়োগ (Post/Submit/Reversal আটকানো) → `P3-12`।
- আংশিক period খোলা।
- `period_close_sequential` (OP-20)।
- Reconciliation check নিজেরা (`P3-13`)।

**Contract**
- `POST /accounting/periods/{id}/soft-close` (`accounting.period.manage`)
- Close/reopen অনুরোধ ও অনুমোদনের API route-এর নাম Pack-এ নির্ধারিত নেই (শুধু soft-close দেওয়া): implementation-এ ঠিক করে PR-এ লিখতে হবে।

UI:
- Period-এর তালিকা (অবস্থা, কে কখন বন্ধ করেছে)।
- "বন্ধ করুন" যাচাইয়ের ফল সহ (কোনটা সবুজ, কোনটা লাল আর কেন)।
- অনুমোদনের অপেক্ষায় তালিকা।
- "আবার খুলুন" কারণের ঘর সহ।
- Soft close বাটন।
- আগের period reopen হলে পরের closed period-এর পাশে সতর্কতা।

**Permission**
`accounting.period.manage` (soft close, close/reopen অনুরোধ); `accounting.period.approve` (অনুমোদন); দেখা `accounting.period.view`।

**Error**
`PRD_CLOSE_BLOCKED` (422), `PRD_REOPEN_DENIED` (403), `JRN_SOD_VIOLATION` (403), `JRN_STATE_INVALID` (409)

**Audit**
`accounting.period.close_requested`, `.closed`, `.reopen_requested`, `.reopened`; `SodGuard` লঙ্ঘনে `accounting.guard.denied`।

**Acceptance**
AC3.7

**Test**
- যাচাই ব্যর্থ হলে বন্ধ হয় না (অনুরোধই তৈরি হয় না)।
- অনুরোধ ও অনুমোদনের মাঝে নতুন journal এলে স্বয়ংক্রিয় reject।
- নিজের অনুরোধ নিজে অনুমোদন (নিষেধ)।
- SoD-poster (একমাত্র poster অনুমোদনকারী হতে পারে না)।
- কারণ ছাড়া reopen।
- দুজন একসাথে একই period-এ অনুরোধ (DB unique আটকায়)।
- Soft close।
- অনুমোদনের সময় Post চলমান থাকলে `balance_version` নকশা কাজ করে (তিনবার পুনঃচেষ্টা, তারপর `PRD_CLOSE_BLOCKED`)।
- সিদ্ধান্ত হওয়া event row সরাসরি SQL-এ বদলানো যায় না।
- `draft`/`rejected` journal শুধু সতর্কতা।

**Deliverable**
Migration ও trigger, service, API, UI

**Sub-tasks**
migration, soft close, close অনুরোধ, close অনুমোদন (পুনঃযাচাই), reopen, UI, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC3.7 সবুজ (`PeriodCloseTest`)।
- OD-5, OD-15 লিখিতভাবে বন্ধ।
- Lock-ক্রম (Pack ৯.১) মানা: period `FOR UPDATE` লক-ক্রম ৪, ভারী যাচাই lock-এর বাইরে।
- সিদ্ধান্ত হওয়া event অপরিবর্তনীয় (trigger test)।

**Architecture constraints**
- ভারী যাচাই চলার পুরো সময় period lock ধরে রাখা যাবে না।
- Period বন্ধ ও আবার খোলা কখনো একজনের সিদ্ধান্তে নয়।
- Posted ledger row এই task কখনো বদলায় না।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Feature (service)
Title
Period lock-এর কঠোর রূপ (PeriodGuard) — Backend
Branch Name
`feature/accounting-period-guard`
View Order
50
Description
**Summary**
`period_lock_mode` ঠিক করবে বন্ধ period-এ Submit, Post, Reversal-এ কী হবে; তিন জায়গায় একই নিয়ম, যাতে বন্ধ হিসাবে কিছু চুপচাপ ঢোকে না।

**Task serial**
`ACC-P3-12 — Period lock-এর কঠোর রূপ (PeriodGuard)` · Feature

**Depends on**
- `ACC-P3-11`
- `ACC-P2-12`
- OD-5, OD-15

**Relation: আগের task**
- `P2-12` (Posting service) `PeriodGuard::check` hook বানিয়ে রেখেছে। Phase 2-এ period সবসময় `open` ছিল, তাই শুধু `period_lock_mode`-এর `warn` পথ চলত: `closed` + `strict` হলে `JRN_PERIOD_LOCKED`, `warn` হলে সতর্কতা আর `accounting.period.locked_post_attempt` audit। এই task সেই hook-কে পূর্ণ রূপ দেয়।
- `P3-11` period-কে `soft_closed`/`closed` করতে শুরু করেছে। তাই এখন থেকে matrix-এর সব সারি প্রমাণযোগ্য।
- `P1-08`: `PeriodResolver::forDate`।
- `P2-03`: `SubmitValidator` (এখানে নতুন hook যোগ হয়)।
- `P2-13`: Reversal-এর তারিখ ঠিক করা (`reversal_default_date`: `today`, `original`, `ask`)।
- `P1-03`: `period_lock_mode` (`off`, `warn`, `strict`, default `warn`), `reversal_default_date`।
- `P1-12`: error `JRN_PERIOD_LOCKED`, audit।
- Pack ৯.১ (লক-ক্রম): Post-এ period `FOR SHARE` (ক্রম ৪)।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P3-14`: AC3.8 (`PeriodGuardMatrixTest`, নয়টা জোড়া)।
- `P3-15` (সই) ও `P5-01` UAT U09 (বন্ধ period-এ Post)।
- `P5-03`: `locked_post_attempts` alert (`monitor_closed_period_post_attempts` দিনে) `activity_logs`-এর `accounting.period.locked_post_attempt` event গোনে।
- `P4-06` (Event থেকে Draft) পরে একই hook ব্যবহার করবে (Post-এর পথ একই)।
- **খোলা প্রশ্ন OP-25:** matrix-এর `soft_closed` + `strict` ঘরে "শুধু `journal.post` permission-ওয়ালার জন্য, নইলে `JRN_PERIOD_LOCKED`"। কিন্তু Post নিজেই `journal.post` ছাড়া অসম্ভব (`P2-12` ধাপ ৬)। অর্থাৎ শর্তটা কেবল Submit (Maker) ও Reversal-তারিখের ক্ষেত্রে অর্থপূর্ণ। ধরা হলো: Submit-এ `soft_closed` + `strict`-এ Maker যদি `journal.post` না রাখে তাহলে `JRN_PERIOD_LOCKED`। Accountant নিশ্চিত করবে।
- **খোলা প্রশ্ন OP-26:** `period_lock_mode = off` ঘরে `closed` period-এ "অনুমোদিত" (কোনো সতর্কতা, audit নেই?)। Matrix-এ `off` সারিতে শুধু "অনুমোদিত"। কিন্তু Audit-এ "সব চেষ্টা" বলা আছে। ধরা হলো: `off` মোডেও `accounting.period.locked_post_attempt` লেখা হয় না (সতর্কতা/audit শব্দটা শুধু `warn`/`strict` সারির সাথে লেখা); নিশ্চিত করা দরকার।

**Related tables / schema**
কোনো নতুন table নেই। পড়া: `acc_periods` (`status`, `start_date`, `end_date`)।

Matrix:

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

**Business rules (R3.15)**
- `period_lock_mode` বদলালে আগে Post হওয়া কিছু প্রভাবিত হয় না।
- Hook তিন জায়গায়:
  1. **Submit validator (`P2-03`):** `strict` মোডে `closed` period হলে Submit-এই `JRN_PERIOD_LOCKED`, যাতে Maker দেরিতে না জানে।
  2. **Post (`P2-12` ধাপ ৯, Pack ৯.২-এর ধাপ ৪):** আবার, কারণ Submit-এর পরে period বন্ধ হয়ে থাকতে পারে।
  3. **Reversal-এর তারিখ ঠিক করার সময় (`P2-13`):** `reversal_default_date` অনুযায়ী পাওয়া তারিখ `strict` ও `closed` period-এ পড়লে `JRN_PERIOD_LOCKED`; তখন Maker খোলা period-এর তারিখ বেছে নেয় (বা Admin period আবার খোলে)।
- Post-এ period row `FOR SHARE`: বন্ধ করার transaction (`FOR UPDATE`, `P3-11`) আর এই Post একসাথে চললে একজন অপেক্ষা করে, ফলে বন্ধ হওয়ার ঠিক মুহূর্তে Post ফসকে ঢোকা অসম্ভব।
- Audit সব চেষ্টার জন্য: সফল (সতর্কতা সহ) বা ব্যর্থ।

**DB flow**
1. `PeriodResolver::forDate(companyId, date)` → period-এর `status` পড়া (কোনো period না পেলে `JRN_DATE_INVALID`, `P1-08`)।
2. Post-এর ক্ষেত্রে period row `FOR SHARE` (লক-ক্রম ৪)।
3. matrix অনুযায়ী সিদ্ধান্ত: অনুমোদিত / অনুমোদিত + সতর্কতা + audit / `JRN_PERIOD_LOCKED`।
4. Audit `accounting.period.locked_post_attempt` (সফল সতর্কতা সহ বা ব্যর্থ)।

**Task scope**
1. `PeriodGuard` নিয়ম (matrix, তিন mode × তিন অবস্থা)।
2. Submit hook (`SubmitValidator`-এ)।
3. Post-এর lock (`FOR SHARE`) ও hook (`PostingService`-এ)।
4. Reversal-তারিখ hook।
5. Audit event।
6. Test।

**Out of scope**
- Period বন্ধ/খোলা (`P3-11`)।
- `period_lock_mode` setting বদলানোর UI (`P1-04`-এর settings পাতা)।
- আংশিক period lock।

**Contract**
`PeriodGuard::check(company, journal_date)` (`P2-12`-এ বানানো hook-এর একই নাম ও আকার)। ফল: অনুমোদিত (সতর্কতা সহ বা ছাড়া) বা `JRN_PERIOD_LOCKED`। Submit, Post ও Reversal-তারিখ তিন জায়গা থেকেই এই একটাই method ডাকা হয়; কোনো public HTTP endpoint নেই। (Matrix-এর `journal.post` শর্তের জন্য actor-এর permission কীভাবে পৌঁছাবে, তা implementation-এ ঠিক হবে, OP-25।)

**Permission**
নতুন slug নেই। `accounting.journal.post` (matrix-এর `soft_closed` + `strict` ঘরে)।

**Error**
`JRN_PERIOD_LOCKED` (409)

**Audit**
`accounting.period.locked_post_attempt` (সফল, সতর্কতা সহ বা ব্যর্থ, সব চেষ্টা)

**Acceptance**
AC3.8

**Test**
- তিন mode × তিন অবস্থা (নয়টা জোড়া)।
- Post আর period বন্ধের একসাথে চলা (কোনো journal বন্ধ period-এ ঢোকে না)।
- Submit-এ আগাম আটকানো (`strict` + `closed`)।
- Reversal-এর তারিখ বন্ধ period-এ।
- `period_lock_mode` বদলালে আগে Post হওয়া অপ্রভাবিত।
- প্রতিটা চেষ্টার audit।

**Deliverable**
`PeriodGuard` পূর্ণ, test

**Sub-tasks**
guard নিয়ম, Submit hook, Post-এর lock, reversal hook, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC3.8 সবুজ (`PeriodGuardMatrixTest`)।
- OD-5, OD-15 বন্ধ।
- Concurrency test MySQL-এ একাধিক connection দিয়ে।
- `P2-12`-এর বিদ্যমান test ভাঙেনি (hook-এর পুরোনো `warn` আচরণ matrix-এর অংশ)।

**Architecture constraints**
- Lock-ক্রম (Pack ৯.১) ভাঙা যাবে না: period `FOR SHARE` ক্রম ৪, journal ও request ও account-এর পরে।
- Guard একটাই class, Submit/Post/Reversal তিন জায়গা থেকে ডাকা হয়; নিয়ম আলাদা আলাদা কপি নয়।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Test (`test` + `ops`)
Title
Performance ও Phase 3-এর test suite
Branch Name
`feature/accounting-phase3-tests`
View Order
51
Description
**Summary**
Phase 3-এর সব report-এর Acceptance Criteria স্বয়ংক্রিয় test-ে প্রমাণিত হবে; বড় data-তেও report-এর গতি গ্রহণযোগ্য থাকবে — না হলে contingency (যেমন `entry_date` বা OD-14 Option B) ঠিক হবে।

**Task serial**
`ACC-P3-14 — Performance ও Phase 3-এর test suite` · Test

**Depends on**
- `ACC-P3-01` থেকে `ACC-P3-13` সব

**Relation: আগের task**
- প্রতিটা Phase 3 task নিজের test লিখেছে। এই task সেগুলোকে AC ধরে এক জায়গায় আনে, ফাঁক ধরে, আর বড় data-য় গতি মাপে।
- `P3-01`: AC3.1, AC3.5, AC3.12-এর ভিত্তি আর contingency-র সিদ্ধান্তের বিন্দু (লাইনে `entry_date` কপি, বা OD-14 Option B)।
- `P3-02`: AC3.10 (cache)।
- `P3-03` থেকে `P3-06`, `P3-08`, `P3-09`: AC3.1, AC3.2, AC3.3, AC3.4, AC3.6।
- `P3-07`: AC3.11।
- `P3-10`: AC3.9।
- `P3-11`, `P3-12`: AC3.7, AC3.8।
- `P3-13`: RC1 থেকে RC9, AC3.1, AC3.3, AC3.4, AC3.10।
- `P2-16`: Phase 2-এর test suite এবং helper (failure-injection, সমান্তরাল runner) যা এই suite পুনঃব্যবহার করতে পারে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P3-15` (accountant-এর সই): এই suite সবুজ না হলে accountant-এর হাতে report যায় না।
- **Phase 4 শুরু:** Phase 3 পুরো সবুজ হওয়া Phase 4-এর শর্ত।
- `P5-05` (শেষ security ও performance): এই task-এর ফল (সীমা ও পরিমাপ) staging-এ আবার চালায়।
- **খোলা প্রশ্ন OP-27:** নিচের performance সংখ্যাগুলো **প্রস্তাব**; Phase শুরুতে client ও tech lead নিশ্চিত করবে (AC3.12-এ "নির্ধারিত আকারের data" বলা আছে)। "কয়েক সেকেন্ড", "এক সেকেন্ডের কাছাকাছি", "কয়েক মিনিট" এর সংখ্যাগত মান ঠিক হয়নি।
- **খোলা প্রশ্ন OP-28:** Golden suite-এর সংজ্ঞা Blueprint ১২.২-এ (এই Pack-এ নেই); শুধু বলা আছে "৭টা পর্যন্ত এই Phase-এ সবুজ"। সাতটার তালিকা Blueprint থেকে নিতে হবে।
- **খোলা প্রশ্ন OP-29:** Seeder command-এর নাম Pack-এ নেই, শুধু "একটা seeder command"। নাম implementation-এ ঠিক হবে।

**Related tables / schema**
নতুন table নেই।

**Performance পরিমাপ।** Test পরিবেশে (কখনো production নয়) একটা seeder command বাস্তবসম্মত বড় 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**

| 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 |

**Acceptance**
AC3.1 থেকে AC3.13 (সব Phase 3 AC; AC3.12 আলাদা চালানো)

**Business rules**
- **Golden suite** (Blueprint ১২.২)-এর ৭টা পর্যন্ত এই Phase-এ সবুজ।
- **সব report test MySQL-এ**।
- `ReportPerformanceTest` CI-র সাধারণ run-এ নয়, আলাদা চালানো হয় (বড় data লাগে)।
- Performance পরিমাপ কখনো production-এ নয়।
- Seeder শুধু test/staging পরিবেশে চলে।

**Task scope**
1. Seeder (বড় data: ৫ বছর, ৫০০ account, ১০ লক্ষ line, ৬০ period)।
2. `EXPLAIN` যাচাই (প্রধান query-গুলোর index ব্যবহার)।
3. প্রতিটা test class আলাদা (উপরের তালিকা)।
4. Golden suite-এর (৭টা পর্যন্ত) সংযোগ।
5. Performance রিপোর্ট ও সিদ্ধান্ত (সীমা ছাড়ালে contingency)।
6. বিদ্যমান ERPFlow regression (AC3.13)।

**Out of scope**
- Production-এ পরিমাপ।
- নতুন feature বা report।
- `P5-05`-এর শেষ security/performance যাচাই।

**Deliverable**
Seeder, performance রিপোর্ট ও সিদ্ধান্ত, সব test class

**Sub-tasks**
seeder, EXPLAIN যাচাই, প্রতিটা test class, performance রিপোর্ট

**Definition of done**
- AC3.1 থেকে AC3.13 সবুজ (AC3.12 আলাদা চালানো, রিপোর্ট আছে)।
- Golden suite-এর ৭টা পর্যন্ত সবুজ।
- বিদ্যমান ERPFlow suite সবুজ।
- MySQL CI job ব্যবহার হয়েছে।
- Performance সীমা ছাড়ালে tech lead-এর লিখিত সিদ্ধান্ত।

**Architecture constraints**
- Concurrency, snapshot, generated column, trigger-নির্ভর test SQLite-এ চালানো যাবে না।
- Seeder production-এ চলবে না।
- Contingency (entry_date বা Option B) এই task নিজে বাস্তবায়ন করে না, শুধু সিদ্ধান্তের জন্য তোলে।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Governance (`gov`)
Title
Accountant-এর নমুনা report pack ও সই
Branch Name
`feature/accounting-phase3-sample-report-pack` (শুধু নমুনা company-র data/seeder ও সই করা নথির জন্য, product code বদলায় না)
View Order
52
Description
**Summary**
Client-এর accountant নমুনা company-তে পুরো এক আর্থিক বছরের data দিয়ে সব report নিজে হাতে মিলিয়ে মেনে নেবেন — তখনই Phase 3 report-এ লিখিত আস্থা থাকবে, Phase 4 শুরু হবে।

**Task serial**
`ACC-P3-15 — Accountant-এর নমুনা report pack ও সই` · Governance

**Depends on**
- `ACC-P3-14`
- OD-1, OD-2, OD-5, OD-14, OD-15, OD-16, OD-20, OD-23 লিখিতভাবে বন্ধ

**Relation: আগের task**
- `P3-14` সব test সবুজ করেছে এবং performance সীমা প্রমাণ করেছে। তাই accountant-এর হাতে যে report যায় সেগুলো যান্ত্রিকভাবে নির্ভুল।
- `P3-03`, `P3-04`, `P3-05`, `P3-06`, `P3-08`, `P3-09`: যে report-গুলো accountant মেলান (TB, GL, P&L, BS, Cash Flow, Account Summary)।
- `P3-07`: Cash Flow mapping accountant-ই সই করে (OD-2)।
- `P3-11`: period বন্ধের প্রবাহ একই নমুনায় প্রদর্শন।
- `P2-13`: reversal ও adjustment যা নমুনা data-য় থাকে।
- OD-গুলোর বিস্তারিত অর্থ: OD-1 (accounting basis), OD-2 (নগদ-সমতুল্য account ও Cash Flow পদ্ধতি), OD-5 (period lock), OD-14 (ledger-এর উৎস), OD-15 (বন্ধ period-এ reversal-এর তারিখ), OD-16 (cash basis P&L, (ক) হলে `P3-05b`), OD-20 (Balance Sheet-এর বছর-শেষ closing, Retained Earnings-এর double count), OD-23 (opening-এ Revenue/Expense)।

**Relation: পরের task (কে অপেক্ষা করছে)**
- **Phase 4 শুরু** (`P4-01`: Change request অনুমোদন ও সিদ্ধান্ত বন্ধ)। `P3-15` সই না হলে কোনো Phase 4 task শুরু হবে না।
- `P5-01` (UAT pack ও সই): `P3-15`-র নমুনা pack থেকে UAT scenario ও ফল।
- `P5-02` (Cutover মহড়া): এই task-এর সই করা নিশ্চয়তা ধরে staging-এ মহড়া।
- **খোলা প্রশ্ন OP-30:** "সই করা নমুনা pack"-এর বিন্যাস (PDF, নথি, spreadsheet) Pack-এ নির্ধারিত নেই। Client-এর accountant ও Finance contact-এর সাথে ঠিক হবে।
- **খোলা প্রশ্ন OP-31:** "বাস্তবসম্মত journal" বানানোর দায়িত্ব কার (accountant নিজে, না dev seeder), Pack-এ স্পষ্ট নয়। কাজ (১) "একটা নমুনা company তৈরি, বাস্তবসম্মত journal" বলে; ধরা হলো dev/QA বানায়, accountant বিষয়বস্তু অনুমোদন করে।

**Related tables / schema**
কোনো নতুন table নেই। নমুনা company-র data (journal, line, CoA, period) ব্যবহার।

**Business rules**
- OD-1, OD-2, OD-5, OD-14, OD-15, OD-16, OD-20, OD-23 লিখিতভাবে বন্ধ না হলে এই task শুরু বা শেষ হবে না।
- Accountant নিজের হাতে বা অন্য tool-এ একই data-র হিসাব করে মেলাবেন। System-এর হিসাব মেনে নেওয়া নয়, স্বাধীন হিসাব।
- অমিল পেলে কারণ খোঁজা, ঠিক করা, আবার। অমিল চুপচাপ গ্রহণ নয়।
- Phase 3 exit criteria (Blueprint ৮.১০) পূরণ।

**Task scope**
1. একটা নমুনা company তৈরি: একটা পূর্ণ আর্থিক বছরের বাস্তবসম্মত journal (আয়, খরচ, বেতন, ভাড়া, সম্পদ কেনা, ঋণ, reversal, adjustment সহ)।
2. সব report চালানো: TB, GL, P&L, BS, Cash Flow, Account Summary।
3. Accountant তাঁর নিজের হাতে বা অন্য tool-এ একই data-র হিসাব করে মেলানো।
4. অমিল পেলে কারণ খোঁজা, ঠিক করা, আবার।
5. সই করা নমুনা pack।

**Out of scope**
- নতুন feature বা report যোগ।
- Production data।
- Phase 4-এর কোনো কাজ।
- OD বন্ধ করার আলোচনা নিজে (সেটা আগেই লিখিতভাবে হওয়ার কথা; এই task শুধু বন্ধ হয়েছে কি না যাচাই করে)।

**Deliverable**
- সই করা নমুনা report pack
- বন্ধ হওয়া OD-র তালিকা

**Permission**
প্রযোজ্য নয় (নথি-ভিত্তিক)। নমুনা company-তে report চালাতে `accounting.report.view`।

**Audit**
Report চালানো `accounting.report.run` স্বাভাবিক নিয়মে audit-এ যায়।

**Acceptance**
AC3.2, Phase 3 exit criteria (Blueprint ৮.১০)

**Test**
প্রযোজ্য নয় (গভর্নেন্স)। প্রমাণ = accountant-এর স্বাধীন হিসাবের সাথে মিল।

**Sub-tasks**
নমুনা company ও journal, report চালানো, accountant-এর হিসাব মেলানো, অমিল ঠিক, সই, OD তালিকা

**Definition of done**
- সই করা নমুনা report pack আছে।
- আটটা OD (1, 2, 5, 14, 15, 16, 20, 23) লিখিতভাবে বন্ধ, তালিকা সংযুক্ত।
- অমিল থাকলে কারণ ও সমাধান নথিভুক্ত।
- Phase 3 exit criteria পূরণ।
- Phase 4-এর (`P4-01`) ও `P5-01`-এর নির্ভরতা খুলে দেওয়া হয়েছে।

**Architecture constraints**
- Accountant-এর সই ছাড়া Phase 4 শুরু নয়।
- অমিল "পরে দেখব" বলে এড়ানো যাবে না।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

<!-- ===== END: ACC-Phase-3-Tasks.md ===== -->


<!-- ===== BEGIN: ACC-Phase-4-Tasks.md ===== -->

# Accounting Module — Phase 4 Tasks (Finance Event Intake ও Asset Pilot)

> এখানকার প্রতিটি task আলাদা করে একজন কাজ করতে পারবে।
> Phase 4-এর শেষে Asset module-এ write-off event হলে Accounting-এ একটা Draft Journal নিজে থেকে তৈরি হয় — কিন্তু কিছুই নিজে নিজে post হয় না; Submit, approve, post আগের মতোই মানুষের হাতে।
> ক্রম (master sequence অনুযায়ী): অনুমোদন ও সিদ্ধান্ত → inbox → Asset reader → consumer → mapping → Draft তৈরি → তিন স্তরের flag → Asset-এ ফেরত → approval ও SoD → ops screen → RC8 → test → runbook।
> Estimate, Owner, Sprint ইচ্ছে করে খালি রাখা হয়েছে — Phase 3 exit ও `ACC-P4-01` শেষ হলে ভর্তি হবে।

## সব Phase 4 task-এ প্রযোজ্য সাধারণ নিয়ম (প্রতিটা task নিজের DoD-তে এগুলো ধরে নেয়)

- Phase 4 শুরু হয় শুধু Phase 3 পুরো সবুজ (`ACC-P3-14`), accountant-এর সই করা নমুনা report pack (`ACC-P3-15`) আর client-এর লিখিত change request অনুমোদনের (`ACC-P4-01`, OD-18) পরে। এর আগে কোনো Phase 4 code লেখা যাবে না।
- Database: MySQL, InnoDB, `utf8mb4`। Table prefix `acc_`। Primary key ধরন `ACC-P0-02`-এর Convention Sheet অনুযায়ী (অনুমান: bigint unsigned বা ULID)। সব `*_id` FK একই ধরনের।
- প্রতিটা business table-এ `company_id NOT NULL`, `companies(id)`-এর FK, আর প্রতিটা composite index-এ `company_id` সবার আগে।
- টাকা `DECIMAL(20,4)`। Enum `VARCHAR(n)` + `CHECK`, MySQL enum নয়। বদলানো যায় এমন row-এ `version INT UNSIGNED NOT NULL DEFAULT 1`; update-এ `WHERE version = ?` আর `version = version + 1`।
- `ON DELETE RESTRICT` ডিফল্ট; লেজারের table-এ `CASCADE` নয়। Posted বা লেজারের row কখনো মোছা হয় না।
- **account_id, journal_id বা inbox_id রাখা table-এ composite FK** `(company_id, account_id)` / `(company_id, journal_id)` / `(company_id, inbox_id)`, একা `*_id`-র FK নয়।
- Migration: প্রতি table-এ আলাদা, প্রয়োগ হওয়া migration সম্পাদনা নয়, পিছনে সামঞ্জস্যপূর্ণ (আগে যোগ, পরে বদল, শেষে মোছা), `down()` নিরাপদ। Append-only table (`acc_finance_event_actions` ইত্যাদি) `Schema::appendOnly('table')` helper (`ACC-P1-10`) দিয়ে।
- **Phase 4-এ কিছুই নিজে post হয় না (AC4.4):** Consumer বা `DraftBuilder` থেকে `PostingService`-এ কোনো code-পথ থাকবে না। Event থেকে শুধু Draft তৈরি হয়; Submit, approve, post সাধারণ মানুষের পথে (`ACC-P2-08`, `ACC-P2-12`)।
- **Asset module-এর code Phase 4-এ অপরিবর্তিত**, একমাত্র ব্যতিক্রম `ACC-P4-07`-এর ছোট পরিবর্তন (`FinanceEventAcknowledger`-এর Asset-দিকের বাস্তবায়ন, আর OD-21 (ক) হলে একটা migration ও writer-এর পরিবর্তন)। Accounting Asset-এর table শুধু **পড়ে** (`ACC-P4-04`), লেখে না, শুধু Asset-এর নিজের acknowledger দিয়ে `status` বদলায়।
- Lock-এর সর্বজনীন ক্রম (Pack Section ৯.১): `acc_journals` → `acc_approval_requests` → `acc_accounts` (`FOR SHARE`, `id` ছোট থেকে বড়) → `acc_periods` → `acc_voucher_counters` → দ্বিতীয় journal। Phase 4-এর নতুন code এই ক্রম ভাঙবে না।
- তিন স্তরের flag (`accounting.enabled`, `event_intake.enabled`, `event_intake.sources.asset_write_off`) সব company-তে **Default বন্ধ**। Flag বন্ধ রেখে merge নিরাপদ; PR-এ "flag Default বন্ধ প্রমাণিত" টিক।
- Phase 4-এ যোগ হওয়া নতুন error code: `EVT_STATE_INVALID` (409), `EVT_PROCESSING_ERROR` (তথ্য, inbox `failed`), `SET_STALE_REQUEST` (409, `ACC-P1-04`-এর সাথে ভাগ করা)। সব code `AccountingError` enum-এ (`ACC-P1-12`), অন্য কোথাও নয়।
- **DoD-র সাধারণ অংশ:** Service–Repository architecture (interface সহ), Form Request, API Resource; `company_id` tenant scope + IDOR test; Feature test (happy path, validation, unauthorized/forbidden); audit event `AccountingAudit` দিয়ে; error `AccountingError` enum-এর code দিয়ে; Job হলে `AccountingJob` base (company context, `ACC-P1-12`); concurrency ও constraint test **MySQL-এ** (SQLite-এ নয়); বিদ্যমান ERPFlow regression সবুজ; Project/Payroll/Asset/Commissioning-এর code অপরিবর্তিত (`ACC-P4-07` ছাড়া)।
- Blueprint-এর `R4.x`, `R5.x`, `AC4.x`, `AC5.x` নম্বর Pack-এ যেভাবে উল্লেখ আছে ঠিক সেভাবে রাখা হয়েছে; Blueprint নিজে এই task-এর সাথে নেই, তাই AC-র অর্থ `ACC-P4-12`-এর AC তালিকা থেকে নেওয়া।

**Phase 4 নির্ভরতা-ছবি:**

```
P3-14 + P3-15 ─► P4-01 ─┬─► P4-02 ─┬─► P4-03 ─┐        (P4-04, P1-12, P1-01-ও লাগে)
                        │          └─► P4-05 ─┼─► P4-06 ─┬─► P4-07   (P4-01-ও লাগে)
                        └─► P4-04 ─► P4-03    │          ├─► P4-08   (P2-08, P2-10-ও লাগে)
                                              │          ├─► P4-09   (P4-02, P4-03, P4-05-ও লাগে)
                                              │          └─► P4-10   (P3-13-ও লাগে)
        P4-05 + P1-01 + P1-04 + P1-05 ─► P4-11     (P4-03 চালানোর শর্ত)
        P4-02 থেকে P4-11 সব ─► P4-12 ─► P4-13 ─► Phase 5
```

**Build order (master sequence ক্রম 33 থেকে 40):** P4-01 → P4-02, P4-04 → P4-03, P4-05 → P4-06, P4-11 → P4-07, P4-08, P4-09 → P4-10 → P4-12 → P4-13। একই ক্রম-গ্রুপের card একসাথে চলতে পারে।

---

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

Project
ERPFlow
Module
Accounting Module
Type
Governance (`gov`)
Title
Change request অনুমোদন ও সিদ্ধান্ত বন্ধ
Branch Name
কোনো branch নেই (নথি-ভিত্তিক কাজ)
View Order
53
Description
**Summary**
Asset write-off থেকে Draft Journal বানানোর pilot-এর আগে client-এর লিখিত change request আর ছয়টা সিদ্ধান্ত (OD-18, OD-3, OD-10, OD-11, OD-21, OD-22) নথিতে বন্ধ করে নাও। অনুমোদন ছাড়া Phase 4-এর কোনো code লেখা বা merge করা যাবে না।

**Task serial**
`ACC-P4-01 — Change request অনুমোদন ও সিদ্ধান্ত বন্ধ` · Governance

**Depends on**
- `ACC-P3-15` (accountant-এর সই করা নমুনা report pack; Phase 3 exit criteria পূরণ)
- OD: `OD-18`, `OD-3`, `OD-10`, `OD-11`, `OD-21`, `OD-22` (এই task নিজেই এগুলো বন্ধ করে)

**Relation: আগের task**
- `P3-15` দিয়েছে: সই করা নমুনা report pack আর বন্ধ হওয়া OD-র তালিকা (OD-1, 2, 5, 14, 15, 16, 20, 23)। অর্থাৎ ledger আর report-এর হিসাব accountant মেনে নিয়েছে। এই ভিত্তি ছাড়া "event থেকে journal" যোগ করা অর্থহীন, কারণ event-এর ফল শেষ পর্যন্ত ওই report-এই দেখা যাবে।
- `P3-14` দিয়েছে Phase 3-এর সম্পূর্ণ test সবুজ প্রমাণ (`P3-15` এর মাধ্যমে Phase 4 শুরুর শর্ত)।
- `P0-04` আগেই client-কে Setup Checklist ও decision list পাঠিয়েছিল এবং **Asset pilot-এর change request (OD-18)-এর খসড়া আলাদা পাতায়** রেখেছিল। সেই খসড়াই এই task-এ চূড়ান্ত করে সই করানো হয়।

**Relation: পরের task (কে অপেক্ষা করছে)**
- **পুরো Phase 4** এই task বন্ধ না হলে শুরু হয় না (Pack: "অবরুদ্ধ করে: পুরো Phase 4")।
- সরাসরি `depends on` লেখা card: `P4-02` (inbox), `P4-04` (Asset reader, OD-21), `P4-07` (Asset-এ ফেরত, OD-11 ও OD-21)।
- সিদ্ধান্তগুলো যে card-এ কাজে লাগে: OD-10 -> `P4-05` (mapping-এর মান) ও `P4-06` (line-নিয়ম); OD-3 -> `P4-06` (currency); OD-11 -> `P4-07`; OD-21 -> `P4-04` ও `P4-07`; OD-22 -> `P4-07` ও Asset-এর ভবিষ্যৎ কাজ।
- `P4-11` ও `P5-08`: `event_intake.sources.asset_write_off` চালুর শর্ত "OD-18 অনুমোদিত" এই task-এর সই করা নথি। System এটা যাচাই করে না, নথি দেখে মানুষ নিশ্চিত করে।

**Related tables / schema**
কোনো table বা migration নেই। এই task শুধু নথি ও সিদ্ধান্ত।

**Business rules**
- মূল scope-এ Fixed Assets আলাদা workflow, আর অন্য module থেকে automatic posting দুটোই বাদ ছিল। এই pilot দুটোকেই ছুঁয়েছে, তাই client-এর লিখিত অনুমোদন ছাড়া কোনো code লেখা যাবে না।
- **যা বদলাচ্ছে:** Asset-এর write-off থেকে Draft Journal নিজে তৈরি হবে (post নয়)।
- **যা বদলাচ্ছে না:** Asset-এর নিজের নিয়ম, বিদ্যমান invoice ও payment।
- ছয়টা সিদ্ধান্তই লিখিতভাবে বন্ধ না হলে `ACC-P4-02` থেকে কোনো card শুরু হবে না।

**Task scope**
নিচের ছয়টা কাজ। প্রতিটার একজন owner আর একটা লিখিত deliverable।

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

**সিদ্ধান্ত অনুযায়ী কোন card কীভাবে বদলায় (প্রভাব):**

| সিদ্ধান্ত | বিকল্প | প্রভাব |
| --- | --- | --- |
| OD-21 | (ক) Asset-এর outbox-এ `effective_date`, `schema_version` যোগ | `P4-07` Asset-এ একটা ছোট migration ও writer-এর পরিবর্তন করে; `P4-04` ওই column থেকে পড়ে |
| OD-21 | (খ) Accounting-এর adapter নিজে পড়বে | `P4-04` Asset-এর `asset_write_offs` table থেকে পড়ে (শুধু `SELECT`); Asset অপরিবর্তিত; `schema_version = 1` adapter-এ ধ্রুবক |
| OD-11 | (ক) `accepted` = Draft তৈরি | `P4-07` Draft commit-এর পর (`afterCommit`) `AcknowledgeEventJob` চালায় |
| OD-11 | (খ) `accepted` = Post | `P4-07` Asset-এর status বদল Post-এর পরে (`JournalPosted`) করে; Draft অনেকক্ষণ আটকে থাকলে Asset-এ event `pending` দেখায় |
| OD-22 | (খ) বা (গ) | Asset-এ আরও কাজ লাগে, **এই Pack-এ নেই** |

**Out of scope**
কোনো code, migration বা test নয়। Disposal ও acquisition event-এর সিদ্ধান্ত (সেগুলো Phase 4-এর বাইরে, `P4-13`-এ শুধু তালিকা হয়)।

**Acceptance**
ছয়টাই লিখিতভাবে বন্ধ।

**Test**
কোড test নেই। যাচাই: ছয়টা সিদ্ধান্তের প্রতিটার সই বা লিখিত নিশ্চয়তা নথিতে আছে; প্রতিটা সিদ্ধান্ত কোন বিকল্প নেওয়া হলো স্পষ্ট লেখা; OD-10-এর মান `P4-05`-এর mapping তালিকার সাথে মিলিয়ে দেখা হয়েছে।

**Deliverable**
সই করা change request নথি, ছয়টা সিদ্ধান্ত নথি, OD-10-এর account mapping-এর মান, বন্ধ হওয়া OD-র তালিকা।

**Sub-tasks**
change request সই (OD-18), OD-10, OD-3, OD-11, OD-21, OD-22

**Definition of done**
- ছয়টা সিদ্ধান্তের প্রতিটা লিখিতভাবে বন্ধ এবং owner-এর নাম সহ।
- সই করা নথি প্রকল্পের নথি-ভান্ডারে রাখা, `P4-02` থেকে সবার কাছে লিংক সহ উপলব্ধ।
- কোন বিকল্প (OD-11 ক/খ, OD-21 ক/খ) বেছে নেওয়া হলো তা পরের card-গুলোর team জানে।

**Architecture constraints**
- Client-এর লিখিত অনুমোদন ছাড়া Phase 4-এর কোনো code merge নয়।
- Asset-এর business rule, invoice বা payment বদলানোর কোনো প্রতিশ্রুতি এই নথিতে নেই।

**Open points (Pack-এ অস্পষ্ট)**
- OD-3-এর বিকল্পের তালিকা Pack-এ নেই। Pack শুধু ধরে নেয় `event_currency_policy` default `hold` (`P4-06`, `P4-03`)। সিদ্ধান্তে `hold`/`ignore` ছাড়া তৃতীয় কিছু এলে `P4-06`/`P4-03` বদলাবে।
- OD-22-এর বিকল্প (ক) Pack-এ স্পষ্ট লেখা নেই; শুধু বলা আছে (খ) বা (গ) হলে Asset-এ আরও কাজ, এই Pack-এ নেই। বিকল্পগুলো নথিতে নাম ধরে লিখতে হবে।
- OD-10: "`amount` কি একমাত্র amount" প্রশ্নের উত্তর "না" হলে (accumulated depreciation আলাদা line) `P4-06`-এর line-নিয়ম বদলাবে; Pack-এর line-নিয়ম শুধু দুই line (Debit `debit_account`, Credit `credit_account`, দুটোতেই `amount`)।

Owner
All (TL+PO)
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P4-02 — Finance Event Inbox

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service)
Title
Finance Event Inbox — schema, গ্রহণ ও অবস্থা-বদলের service
Branch Name
`feature/accounting-event-inbox`
View Order
54
Description
**Summary**
অন্য module থেকে আসা finance event-এর জন্য একটা source-নিরপেক্ষ inbox বানাতে হবে — একই event যতবারই আসুক একটাই row, আর status বদল শুধু একটা service (`InboxTransitionService`) দিয়ে। বাকি Phase 4-এর সব কাজ এই ভিত্তির ওপর দাঁড়াবে।

**Task serial**
`ACC-P4-02 — Finance Event Inbox` · Feature

**Depends on**
- `ACC-P4-01` (সই করা change request, সিদ্ধান্ত বন্ধ)
- `ACC-P1-12` (`AccountingAudit`, `AccountingError` enum, company context)
- `ACC-P2-12` (Posting service, Phase 2 সম্পূর্ণ; Pack কারণ আলাদা করে বলেনি, নিচের open point দেখুন)

**Relation: আগের task**
- `P4-01` দিয়েছে অনুমতি: OD-18 সই, আর OD-11/OD-21 সিদ্ধান্ত যা inbox-এর `effective_date`, `schema_version` ও পরে `acknowledged_at` ব্যবহারের ধরন ঠিক করে।
- `P1-12` দিয়েছে: audit লেখার `AccountingAudit::log(...)`, error-এর একটাই উৎস `AccountingError` enum (এখানে নতুন `EVT_*` code যোগ হবে), আর `X-Correlation-Id`।
- `P2-12` দিয়েছে: কাজের Posting পথ ও Phase 2-এর journal lifecycle, যার ওপর পরে `DraftBuilder` (`P4-06`) দাঁড়ায়।

**Relation: পরের task (কে অপেক্ষা করছে)**
Pack বলে এই task `P4-03`, `P4-05`, `P4-06`, `P4-09`-কে অবরুদ্ধ করে। এছাড়া দেখা যায়:
- `P4-03` (Consumer job): `InboxService::receive` দিয়ে event তোলে; `status = 'received'` row `FOR UPDATE SKIP LOCKED` দিয়ে দখল করে; `InboxTransitionService` দিয়ে অবস্থা বদলায়; `idx_acc_fei_claim` index ব্যবহার করে।
- `P4-05` (mapping): inbox-এর `source_type`/`event_type` ধরে mapping; আটকে থাকা (`held_mapping`) event গোনে।
- `P4-06` (`DraftBuilder`): inbox row পড়ে Draft বানায়, `received` -> `draft_created`; `acc_finance_event_journal_links.inbox_id` composite FK `(company_id, inbox_id)` এই task-এর `uq_acc_fei_company_id`-কে লক্ষ্য করে (তাই এই unique বাদ দেওয়া যাবে না)।
- `P4-07`: expand migration দিয়ে এই table-এ `acknowledged_at`, `ack_attempts`, `ack_error` যোগ করে।
- `P4-09` (ops screen): release, retry, ignore, conflict সমাধান সব এই task-এর transition table মেনে চলে; `acc_finance_event_actions.inbox_id` composite FK।
- `P4-10` (RC8), `P5-03` (monitoring: `held_events`, `failed_events`, `ack_failing`) inbox-এর অবস্থা পড়ে।
- `P4-12` পুরো suite (AC4.1) আর `P5-*` ops।

**Related tables / 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)`: অন্য table-এর composite FK-র লক্ষ্য।
- `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)`।

**Business rules**
- R4.1 থেকে R4.3 (Blueprint)। একই identity `(company_id, source_type, source_id, event_type)` মানে একই event: যত বারই আসুক একটাই row।
- মূল `payload` একবার লিখলে আর বদলায় না। একমাত্র ব্যতিক্রম: `conflict` সমাধানে মানুষ নতুন payload গ্রহণ (`accept`) করলে `payload`-এ `conflict_payload` বসে, audit সহ (`P4-09`)।
- অবস্থা বদল **শুধু একটা service দিয়ে**: `InboxTransitionService` (Journal state machine `P2-01`-এর মতো)। অন্য কোনো code সরাসরি `status` update করবে না; একটা static test এটা ধরবে।
- `EVT_*` code সাধারণত inbox-এর অবস্থা হিসেবে থাকে, user-এর request-এর error নয়। শুধু ops screen থেকে নিজে retry করলে HTTP response আসে (`EVT_STATE_INVALID` 409)।

**অবস্থা বদলের নিয়ম (`InboxTransitionService`):**

| থেকে | যায় | কে | কখন |
| --- | --- | --- | --- |
| (নতুন) | `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)।

**DB flow: গ্রহণ (`InboxService::receive(SourceEvent)`)**
1. transaction শুরু।
2. `INSERT ... ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id)`; নতুন হলে `status = 'received'`।
3. নতুন না হলে row `SELECT ... FOR UPDATE`: `payload_hash` সমান হলে কিছু না বদলে আগের অবস্থা ফেরত (`EVT_DUPLICATE`, 200)। ভিন্ন হলে `status = 'conflict'` আর `conflict_payload` বসানো, মূল `payload` অক্ষত (`EVT_CONFLICT`)।
4. Audit (`accounting.event.received` বা `.conflict`)।
5. commit।

**Task scope**
1. Migration `acc_finance_event_inbox` (সব constraint ও index সহ)।
2. Model ও repository (interface সহ)।
3. `InboxService::receive(SourceEvent)`: ওপরের DB flow, `payload_hash` = `payload`-এর canonical (key-ক্রম স্থির করা) JSON-এর SHA-256।
4. `InboxTransitionService`: উপরের transition table কোডে একটাই উৎস হিসেবে; `apply(inboxId, toStatus, actor|system, ctx)`; অবৈধ জোড়ায় `EVT_STATE_INVALID`; প্রতিটা বদলে `version + 1`, `WHERE version = ?`।
5. Error: `EVT_DUPLICATE`, `EVT_CONFLICT`, `EVT_STATE_INVALID` `AccountingError`-এ।
6. Static test: `acc_finance_event_inbox.status` set করা শুধু `InboxTransitionService`-এর মধ্যে (এবং `receive`-এর `INSERT`/conflict পথে)।

**Out of scope**
Consumer job (`P4-03`), Asset থেকে পড়া (`P4-04`), Draft তৈরি (`P4-06`), ops API ও UI (`P4-09`), `acknowledged_at`/`ack_*` column (`P4-07`)।

**Contract**
- `InboxService::receive(SourceEvent): InboxReceiveResult` (নতুন, duplicate, conflict জানায়)।
- `InboxTransitionService::apply(...)`।
- কোনো public HTTP endpoint নেই এই task-এ।

**Error**
`EVT_DUPLICATE` (200, আগের ফল), `EVT_CONFLICT` (409), `EVT_STATE_INVALID` (409, নতুন)

**Audit**
`accounting.event.received`, `accounting.event.conflict`

**Acceptance**
AC4.1

**Test**
- একই event ১০ বার পাঠালে একটাই row, আগের ফল ফেরত।
- একই identity ভিন্ন payload: `status = 'conflict'`, `conflict_payload` বসে, মূল `payload` অক্ষত।
- প্রতিটা বৈধ ও অবৈধ অবস্থা-জোড়া (সব অবস্থা × সব অবস্থা matrix)।
- দুই connection থেকে একসাথে গ্রহণ (MySQL-এ concurrency test)।

**Deliverable**
Migration, `InboxService`, `InboxTransitionService`, model, error case, test।

**Sub-tasks**
migration, receive, transition table, conflict, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC4.1 সবুজ।
- DB-তে `uq_acc_fei_identity` সরাসরি SQL দিয়ে ভাঙার চেষ্টা ব্যর্থ (test প্রমাণ)।

**Architecture constraints**
- অবস্থা বদলের একমাত্র পথ `InboxTransitionService`।
- মূল `payload` কোনো সাধারণ code বদলাবে না।
- `uq_acc_fei_company_id` বাদ দেওয়া যাবে না (`P4-06`, `P4-09` এর composite FK-র লক্ষ্য)।

**Open points (Pack-এ অস্পষ্ট)**
- Pack বলেনি `P2-12`-এর ওপর এই task কেন নির্ভর করে। ধরে নেওয়া হয়েছে Phase 2 (journal ও posting) পুরো সবুজ থাকা Phase 4 শুরুর শর্ত; tech lead নিশ্চিত করবেন।
- Transition table-এ `failed`, `ignored` বা `conflict` অবস্থায় থাকা identity-তে **আবার ভিন্ন payload** এলে কী হবে বলা নেই (`conflict`-এ যাওয়ার সারিতে শুধু `received`, `draft_created`, `held_*` আছে)। একই payload এলে `EVT_DUPLICATE`। ভিন্ন payload এই তিন অবস্থায় এলে প্রস্তাব: `conflict_payload` হালনাগাদ বা প্রত্যাখ্যান, সিদ্ধান্ত লাগবে।
- `payload_hash`-এর canonical রূপ (key sorting, number format) Pack-এ সংজ্ঞায়িত নয়, `P2-12`-এর `posted_hash` canonical JSON-এর সাথে একই পদ্ধতি নেওয়াই যুক্তিসঙ্গত, কিন্তু নিশ্চিত করতে হবে।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P4-04 — Asset event reader (adapter)

Project
ERPFlow
Module
Accounting Module
Type
Feature (service)
Title
Asset event reader (adapter) — শুধু-পড়া
Branch Name
`feature/accounting-asset-event-reader`
View Order
55
Description
**Summary**
Asset-এর outbox (`asset_finance_events`) থেকে write-off event শুধু পড়ার একটা adapter লাগবে। Accounting সেখানে কিছুই লেখবে না; consumer ও Draft builder `FinanceEventSource` interface দিয়ে event পাবে।

**Task serial**
`ACC-P4-04 — Asset event reader (adapter)` · Feature

**Depends on**
- `ACC-P4-01` (OD-21: `effective_date` ও `schema_version` কোথা থেকে আসবে)

**Relation: আগের task**
- `P4-01` OD-21-এর সিদ্ধান্ত দিয়েছে। (ক) হলে Asset-এর outbox-এ নতুন `effective_date` ও `schema_version` column থাকবে (যোগ করবে `P4-07`) আর এই adapter সেখান থেকে পড়বে। (খ) হলে এই adapter Asset-এর `asset_write_offs` table থেকে তারিখ পড়বে আর `schema_version = 1` ধ্রুবক।
- বিদ্যমান `asset_finance_events` outbox (Asset module-এর, Pack-এর table index-এ "বিদ্যমান") যা Asset নিজের transaction-এ `pending` row দিয়ে লেখে। Accounting এর অস্তিত্ব জানে না।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P4-03` (Consumer job): `FinanceEventSource::fetchPending(companyId, intake_batch_size)` ডাকে। Pack-এ এই task `P4-03`-কে অবরুদ্ধ করে।
- `P4-10` (RC8): এই task-এর reader class-এ আলাদা read-only method `fetchAcceptedIds` যোগ করে RC8.5-এর জন্য (Asset-এ `accepted` অথচ Accounting-এ inbox row নেই কি না দেখতে)। সেই method-ও একই "শুধু-পড়া" static test মানবে।
- `P4-07` (Asset-এ ফেরত): Asset-এর দিকের `FinanceEventAcknowledger` আলাদা class; এই reader শুধু পড়ে। OD-21 (ক) হলে `P4-07`-এর migration-এর column এই reader পড়ে।
- `P4-12`: AC4.8 (Asset-এর আচরণ অপরিবর্তিত) ও static test।

**Related tables / schema**
কোনো নতুন table নেই। পড়া হয় বিদ্যমান `asset_finance_events`: শুধু `event = 'write_off'`, `status = 'pending'` row। Column যা Pack-এ উল্লেখিত: `id`, `company_id`, `event`, `status`, `serialized_asset_id`, `reference_id`। বাকি column payload-এ হুবহু যায়।

কোনো লেখা নেই। OD-21 (খ)-তে Asset-এর write-off source record (`asset_write_offs`) থেকে শুধু `SELECT`।

**Business rules**
- Accounting Asset-এর outbox শুধু **পড়ে**; কখনো `insert`, `update`, `delete` বা model `save` নয়।
- শুধু committed row দেখা: `fetchPending` একটা নির্দিষ্ট isolation level (REPEATABLE READ বা READ COMMITTED, নির্দিষ্ট করে লেখা) ব্যবহার করে। Asset-এর নিজের transaction শেষ না হওয়া event আসে না।
- শুধু `write_off` event আসে। `disposal`, `acquisition` ইত্যাদি আসে না।
- অন্য company-র row আসে না (`WHERE company_id = ?`)।
- অবৈধ তথ্যের জন্য কোনো business error নেই; সব `integrity_flags`-এ।
- R4.4: event-এর `company_id` আর সংশ্লিষ্ট asset ও write-off source-এর `company_id` মিলতে হবে।

**DB flow**
1. `AssetFinanceEventSource::fetchPending(companyId, limit)`:
   `SELECT * FROM asset_finance_events WHERE company_id = ? AND event = 'write_off' AND status = 'pending' ORDER BY id LIMIT ?`।
2. প্রতিটা row `SourceEvent`-এ রূপান্তর: `source_type = 'asset_finance_event'`, `source_id` = ওই row-এর `id`, `schema_version = 1` (adapter-এ ধ্রুবক, OD-21 (খ)-তে), `payload` = row-এর সব column হুবহু।
3. `effective_date`: OD-21 (ক) হলে Asset-এর যোগ করা column থেকে। (খ) হলে `reference_id` ধরে Asset-এর write-off source record থেকে তারিখ পড়া (শুধু `SELECT`)। দুটোই না পেলে `effective_date = null` (ফলে পরে `EVT_PAYLOAD_INVALID`)।
4. Company যাচাই (R4.4): event-এর `serialized_asset_id`-র asset আর write-off source-এর `company_id` পড়ে event-এর `company_id`-র সাথে মেলানো। না মিললে `integrity_flags.company_mismatch = true`।
5. `fetchOne(sourceId)`: একটা id ধরে একই রূপান্তর।

**Task scope**
1. Interface `FinanceEventSource`: `sourceType(): string`, `fetchPending(companyId, limit): iterable<SourceEvent>`, `fetchOne(sourceId): ?SourceEvent`।
2. 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`)।
3. `AssetFinanceEventSource` (ওপরের DB flow)।
4. তারিখ নির্ণয় (OD-21 অনুযায়ী দুই রূপ)।
5. Company যাচাই।
6. **শুধু-পড়া সুরক্ষা:** এই class-এ কোনো `insert`, `update`, `delete` বা model `save` নেই। একটা static test এই class-এর source-এ এসব নিষিদ্ধ শব্দ খোঁজে। বিদ্যমান Asset code অপরিবর্তিত।
7. Source-এর container binding: `source_type` ধরে সঠিক implementation পাওয়া যায় (পরে `P4-07`-এর acknowledger-এর জন্যও একই কৌশল)।

**Out of scope**
Asset-এ কোনো লেখা বা পরিবর্তন (`P4-07`); inbox-এ ঢোকানো (`P4-03`, `P4-02`); `fetchAcceptedIds` (`P4-10` যোগ করে)।

**Contract**
Interface ও DTO (ওপরে)। কোনো HTTP endpoint নেই।

**Permission**
প্রযোজ্য নয় (ভেতরের service)।

**Error**
কোনো business error নেই। অবৈধ তথ্য `integrity_flags`-এ।

**Audit**
কোনো নিজস্ব audit নেই (event-এর audit inbox-এ, `P4-02`)।

**Acceptance**
AC4.8

**Test**
- Pending ও non-pending row: শুধু `pending` আসে।
- শুধু `write_off` আসে (`disposal`, `acquisition` নয়)।
- অন্য company-র row আসে না।
- Company অমিল ধরা (`integrity_flags.company_mismatch`)।
- তারিখ-পাওয়া ও না-পাওয়া (OD-21-এর দুই রূপ)।
- Static test: class-এ নিষিদ্ধ শব্দ নেই।
- Asset-এর নিজের transaction শেষ না হওয়া row দেখা যায় না।

**Deliverable**
Interface, DTO, `AssetFinanceEventSource`, test।

**Sub-tasks**
interface ও DTO, adapter, তারিখ নির্ণয়, company যাচাই, static test, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC4.8 (এই অংশ) সবুজ।
- বিদ্যমান Asset code-এর diff খালি।

**Architecture constraints**
- Adapter শুধু পড়ে; লেখার কোনো পথ নেই।
- Source-নিরপেক্ষ interface: `P4-03` শুধু `FinanceEventSource` চেনে, Asset-এর class নয়।

**Open points (Pack-এ অস্পষ্ট)**
- Isolation level: REPEATABLE READ নাকি READ COMMITTED, Pack "নির্দিষ্ট করে" বলেছে কিন্তু কোনটা বলেনি। বাছাই implementation-এ, PR-এ লিখিত থাকতে হবে।
- `dimensions` (cost center) এবং `amount`, `currency`, `occurred_at` কোন column থেকে আসে তা `asset_finance_events`-এর column তালিকা ছাড়া জানা যায় না (Pack-এ নেই)। `P0-02`/Asset code দেখে নিশ্চিত করতে হবে।
- `payload` = "row-এর সব column" বলা আছে, OD-21 (খ)-তে আনা write-off source-এর তারিখ payload-এ যোগ হবে কি না স্পষ্ট নয়।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P4-03 — Consumer job

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + job)
Title
Consumer job — event তোলা, প্রক্রিয়া করা, heartbeat
Branch Name
`feature/accounting-event-consumer-job`
View Order
56
Description
**Summary**
একটা scheduled consumer job source থেকে event তুলে inbox-এ আনবে, প্রতিটি `received` row আলাদা transaction-এ process করবে, আর process kill বা crash-এর পরও নিরাপদে resume হবে — event হারানো বা duplicate journal-এর পথ থাকবে না।

**Task serial**
`ACC-P4-03 — Consumer job` · Feature

**Depends on**
- `ACC-P4-02` (inbox ও transition service)
- `ACC-P4-04` (`FinanceEventSource` ও Asset adapter)
- `ACC-P1-12` (`AccountingJob` base, company context)
- `ACC-P1-01` (`AccountingFeatureService`, flag)

**Relation: আগের task**
- `P4-02` দিয়েছে: `acc_finance_event_inbox`, `InboxService::receive` (event গ্রহণ, `EVT_DUPLICATE`), `InboxTransitionService`, আর `idx_acc_fei_claim` index যার ওপর দখল-query চলে।
- `P4-04` দিয়েছে: `FinanceEventSource::fetchPending(companyId, limit)` এবং `SourceEvent` DTO।
- `P1-12` দিয়েছে: `AccountingJob` base (constructor-এ `companyId` বাধ্যতামূলক, `TenantContext` বসানো ও `finally`-তে পরিষ্কার)।
- `P1-01` দিয়েছে: `AccountingFeatureService`-এর `sourceEnabled(companyId, source)` (flag `Setting` model-এ, default `false`)।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P4-06` (`DraftBuilder`): Pack অনুযায়ী এই task `P4-06`-কে অবরুদ্ধ করে। Consumer প্রতিটা দখল করা row-এর জন্য `DraftBuilder::build(row)` ডাকে।
- `P4-12`: AC4.5, AC4.6, AC4.10-এর test।
- `P4-09` (ops screen): `acc_intake_heartbeats` পড়ে "Consumer-এর স্বাস্থ্য-বক্স" দেখায় (সর্বশেষ কখন চলেছে, কত পিছিয়ে)।
- `P5-03` (monitoring): `consumer_lag` alert `acc_intake_heartbeats` পড়ে, `monitor_consumer_lag_minutes`-এর সাথে তুলনা করে।
- **`P4-11`:** Pack-এর ব্যতিক্রম: এই task-এর code আগে লেখা যায়, কিন্তু **চালানো যায় শুধু `P4-11` (তিন স্তরের flag) শেষ হলে**। Flag চালু করার মাধ্যম `P4-11` দেয়।

**Related tables / schema**

**`acc_intake_heartbeats`** (company প্রতি একটা row)

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

বিদ্যমান: `acc_finance_event_inbox` (`P4-02`, পড়া ও লেখা)। পড়া: `asset_finance_events` (`P4-04`-এর মাধ্যমে), `acc_settings` (settings নিচে)।

**Settings যা এই task পড়ে:** `intake_batch_size` (default 50), `intake_retry_backoff_seconds` (default 60, 300, 900, 3600), `intake_max_attempts` (default 5), `event_currency_policy` (`hold` বা `ignore`, default `hold`), flag `event_intake.enabled`, `event_intake.sources.asset_write_off`।

**Business rules**
- R4.11, R4.12 (Blueprint)।
- একই company-তে একসাথে দুটো consumer চলে না।
- প্রতিটা row নিজের আলাদা transaction-এ প্রক্রিয়া হয়; একটার ব্যর্থতা বাকিদের থামায় না।
- Source থেকে তোলা আবার ঘটলে (যেমন Asset-এ ফেরত জানানো ব্যর্থ হয়ে `pending` থেকে গেলে) `receive` শুধু `EVT_DUPLICATE` দেয়, ক্ষতি নেই।
- একটা run সর্বোচ্চ ৫০ সেকেন্ড (schedule প্রতি মিনিটে, তাই overlap এড়াতে)।
- Flag run চলার মধ্যেও প্রতিটা loop-এর শুরুতে আবার যাচাই; বন্ধ হলে চলমান run পরের loop-এ থামে।
- এই job কখনো `PostingService` ডাকে না (AC4.4)।

**ফলের শ্রেণিবিভাগ (Draft তৈরির ফল অনুযায়ী inbox অবস্থা):**

| ফল | inbox অবস্থা |
| --- | --- |
| Draft তৈরি সফল | `draft_created` (`DraftBuilder`, `P4-06`) |
| `EVT_MAPPING_MISSING` | `held_mapping` |
| `EVT_CURRENCY_MISMATCH` | `event_currency_policy` অনুযায়ী `held_currency` (`hold`) বা `ignored` (`ignore`) |
| `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` |

**DB flow: এক run**
1. company context (`P1-12`)।
2. Flag আবার পড়া (`sourceEnabled`), বন্ধ হলে থামা।
3. `acc_intake_heartbeats`-এ শুরু (`last_started_at`)।
4. **তোলা:** `FinanceEventSource::fetchPending(companyId, intake_batch_size)` (`P4-04`) থেকে প্রতিটা event `InboxService::receive` (`P4-02`)।
5. **প্রক্রিয়া, row দখল:** `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`।
6. **প্রতিটা row নিজের আলাদা transaction-এ** `DraftBuilder::build(row)` (`P4-06`)। একটা row-এর ব্যর্থতা বাকিদের থামায় না।
7. প্রতিটা loop-এর শুরুতে flag আর সময়সীমা (৫০ সেকেন্ড) যাচাই।
8. শেষে heartbeat-এ ফল (`last_finished_at`, `processed_count`, `held_count`, `failed_count`, `last_error`)।

**Task scope**
1. Migration `acc_intake_heartbeats`।
2. Artisan command `accounting:consume-events`, schedule **প্রতি মিনিটে**: তিনটা flag চালু (`P4-11`) এমন প্রতিটা company-র জন্য `ConsumeFinanceEventsJob(companyId)` dispatch।
3. **একই company-তে একসাথে দুটো নয়:** job-এর শুরুতে `GET_LOCK('acc_intake_{companyId}', 0)`; না পেলে চুপচাপ বেরিয়ে যায়; শেষে `RELEASE_LOCK` (`finally`-তে)।
4. তোলা (ওপরের step 4)।
5. দখল ও প্রক্রিয়া (step 5-6), `DraftBuilder` ডাকা `P4-06`-এর contract ধরে।
6. ফল শ্রেণিবিভাগ (ওপরের table) আর `InboxTransitionService` দিয়ে অবস্থা বদল।
7. Backoff হিসাব ও `EVT_PROCESSING_ERROR`।
8. Heartbeat লেখা।
9. Test।

**Out of scope**
Draft তৈরির ভেতরের যুক্তি (`P4-06`); flag চালু-বন্ধের API ও UI (`P4-11`); ops screen (`P4-09`); alert (`P5-03`); Asset-এ ফেরত জানানো (`P4-07`)।

**Contract**
`php artisan accounting:consume-events`; `ConsumeFinanceEventsJob(companyId)`। কোনো HTTP endpoint নেই। Heartbeat পড়ার API `P4-09`-এর অংশ।

**Permission**
প্রযোজ্য নয় (background job)।

**Error**
`EVT_PROCESSING_ERROR` (নতুন, তথ্য হিসেবে inbox `failed`)। বাকি `EVT_*` `DraftBuilder` থেকে আসে ও এখানে শ্রেণিবদ্ধ হয়।

**Audit**
`accounting.event.*` (`P4-02`, `P4-06` লেখে; এই job `.failed`, `.held` বদলের সময় একই `AccountingAudit` ব্যবহার করে)

**Acceptance**
AC4.5, AC4.6, AC4.10

**Test**
- দুটো job একসাথে: দ্বিতীয়টা চুপচাপ বেরিয়ে যায়।
- Flag চলতে চলতে বন্ধ: run পরের loop-এ থামে।
- একটা row ভাঙলে বাকিরা চলে।
- মাঝপথে process kill করে আবার চালানো: row `received` থেকে যায়, একটাই Draft হয়।
- Backoff-এর ক্রম (`attempts`-তম মান), `intake_max_attempts`-এ `failed` ও `EVT_PROCESSING_ERROR`।
- দুই company-র event মেশে না (company context)।
- MySQL-এ `SKIP LOCKED` ও `GET_LOCK` সত্যিকারের আচরণ।

**Deliverable**
Migration, command, job, heartbeat।

**Sub-tasks**
migration, command ও schedule, advisory lock, তোলা, দখল ও প্রক্রিয়া, শ্রেণিবিভাগ, heartbeat, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC4.5, AC4.6, AC4.10 (এই job-সংক্রান্ত অংশ) সবুজ।
- Flag বন্ধ থাকলে scheduler কোনো job dispatch করে না (প্রমাণিত)।

**Architecture constraints**
- এই job কখনো `PostingService` ডাকে না।
- সব job `AccountingJob` base-এর মধ্য দিয়ে company context-এ চলে।
- প্রতিটা row আলাদা transaction; একটা বড় transaction-এ পুরো batch নয়।

**Open points (Pack-এ অস্পষ্ট)**
- **Build order চক্র:** এই task `DraftBuilder` ডাকে, কিন্তু `DraftBuilder` (`P4-06`) এই task শেষ হওয়ার পর বানানো হয় (`P4-06` নির্ভর করে `P4-03`-এর ওপর)। প্রস্তাব: এই task-এ একটা `DraftBuilderContract` interface ও stub (সবসময় `held_mapping` বা no-op) রাখা, `P4-06` আসল implementation বসায়। Pack এটা বলেনি।
- `attempts`-তম মান বলতে `intake_retry_backoff_seconds` তালিকার দৈর্ঘ্য (৪) ছাড়িয়ে `attempts` গেলে (max 5) কোন মান, বলা নেই (প্রস্তাব: শেষ মান)।
- "amount শূন্য বা ঋণাত্মক" -> `failed`; কোন error code (প্রস্তাব `EVT_PAYLOAD_INVALID`) Pack স্পষ্ট বলেনি।
- `GET_LOCK` MySQL connection-ভিত্তিক; queue worker-এর connection pool/reuse-এ lock কীভাবে ছাড়ে তা implementation-এ যাচাই করতে হবে।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service + api + ui)
Title
Account mapping — schema, API ও UI
Branch Name
`feature/accounting-event-account-mapping`
View Order
57
Description
**Summary**
Admin বা finance manager UI/API দিয়ে ঠিক করবেন কোন event-এর কোন অংশ কোন account-এ যাবে; সংবেদনশীল বদলে দ্বিতীয় জনের approval লাগবে (`ACC-P1-04` পথ)। Draft তৈরির account hard-code থাকবে না — সব `acc_account_mappings`-এ।

**Task serial**
`ACC-P4-05 — Account mapping (schema, API, UI)` · Feature

**Depends on**
- `ACC-P4-02` (inbox: আটকে থাকা event গোনা, `source_type`/`event_type` ধরন)
- `ACC-P1-06` (`acc_accounts`, composite FK লক্ষ্য `uq_acc_accounts_company_id`)
- `ACC-P1-04` (settings change request ও দ্বিতীয় অনুমোদন)
- `ACC-P2-10` (`SodGuard`, SoD service)
- OD: `OD-10` (`ACC-P4-01` বন্ধ করে)

**Relation: আগের task**
- `P4-02` দিয়েছে inbox, যেখানে `held_mapping` অবস্থার event আছে। এই task-এর UI সেই সংখ্যা দেখায় ও ছাড়ার প্রস্তাব দেয়।
- `P1-06` দিয়েছে `acc_accounts` ও তার composite unique `(company_id, id)`; `acc_account_mappings.account_id`-র composite FK এর লক্ষ্য। Account-এর `is_group`, `is_active`, `type` নিয়ম যাচাইয়ের উৎস।
- `P1-04` দিয়েছে `acc_setting_change_requests` (status `pending/approved/rejected/cancelled`), `settings_require_second_approval` চালু থাকলে সংবেদনশীল key-র সরাসরি লেখা বন্ধ করার পদ্ধতি, `SET_STALE_REQUEST` এবং "অনুরোধকারী নিজে অনুমোদন করতে পারে না" নিয়ম। Pack-এর `sensitive_settings` default-এ `account_mappings` আছে। এই task `mapping:` prefix-এর জন্য একটা applier নিবন্ধন করে।
- `P2-10` দিয়েছে `SodGuard` ও setting `sod_mapping_editor_cannot_approve`; এই task যে `updated_by` রাখে সেটাই SoD-র ভিত্তি (`P4-08` ব্যবহার করে)।
- `P4-01` OD-10 বন্ধ করেছে: কোন account (Debit/Credit) আর `amount` একমাত্র কি না। সেই মান এখানে প্রথম mapping-এর মান।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P4-06` (`DraftBuilder`): Pack অনুযায়ী এই task `P4-06`-কে অবরুদ্ধ করে। `MappingRequirements` পড়ে দুটো mapping আনে ও `mapping_snapshot` বানায়।
- `P4-11` (flag): Setup Checklist `I01_mappings` ("প্রয়োজনীয় সব mapping বৈধ") `MappingRequirements` ও `acc_account_mappings` ব্যবহার করে `event_intake.enabled` চালুর শর্ত যাচাই করে।
- `P4-09` (ops screen): `POST /accounting/intake/release-held` (`mapping_key` ধরে) mapping বসানোর পর এই task-এর UI থেকেও ডাকা হয়; তাই এই UI-র "আটকে থাকা event ছাড়ার প্রস্তাব" `P4-09` শেষ হলে পূর্ণ কাজ করে।
- `P4-08` (SoD): `acc_account_mappings.updated_by` Draft তৈরির সময় snapshot-এ যায়।
- `P4-12`: AC4.2, AC4.11; `P5-03` ও `P5-08`: mapping না থাকলে `held_events`।

**Related tables / 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)`।

পড়া/ব্যবহার: `acc_accounts` (`P1-06`), `acc_setting_change_requests` (`P1-04`), `acc_settings` (`sensitive_settings`, `settings_require_second_approval`), `acc_finance_event_inbox` (আটকে থাকা event গোনা)।

**Business rules**
- **প্রয়োজনীয় mapping-এর তালিকা** কোডে `MappingRequirements`: `asset_finance_event` + `write_off` -> `debit_account`, `credit_account`। এটাই Setup Checklist-এর `I01`-এর আর `DraftBuilder`-এর ভিত্তি।
- **যাচাই:** Account ওই company-র, active, leaf (`group` নয়) -> নইলে `JRN_ACCOUNT_INVALID`।
- **সতর্কতা (error নয়):** `debit_account` Expense বা Asset-এর বাইরে হলে; `credit_account` Asset-এর বাইরে হলে।
- **বদলের প্রভাব (R4.14):** নতুন event নতুন mapping পায়। আগে তৈরি Draft অপরিবর্তিত। `held_mapping` অবস্থার event আপনা-আপনি ছাড়া পায় না: UI সংরক্ষণের পর জানায় "৩টা event আটকে আছে, এখন ছাড়বেন?" (`P4-09`-এর release)।
- **SoD:** যে mapping বদলেছে তার নাম `acc_account_mappings.updated_by`-এ। Draft তৈরির সময় সেই মান journal-এর সাথে জোড়া `mapping_snapshot`-এ সংরক্ষণ (`P4-06`), যাতে পরে mapping আবার বদলালেও SoD-র তথ্য ঠিক থাকে।
- Mapping বদল সংবেদনশীল: `settings_require_second_approval` (default true) চালু থাকলে সরাসরি লেখা নয়।

**DB flow: বসানো বা বদলানো**
1. `account_mappings` `sensitive_settings` তালিকায় আছে আর `settings_require_second_approval` চালু: সরাসরি লেখা নয়। `ACC-P1-04`-এর change request তৈরি, key `mapping:asset_finance_event.write_off.debit_account` ধরনের, `proposed_value` = account id। P1-04-এর service-এ একটা **`ChangeApplier` রেজিস্ট্রি**, যেখানে এই card `mapping:` prefix-এর জন্য একটা applier নিবন্ধন করে (আলাদা approval table লাগে না)।
2. অনুমোদনে (অন্য একজন, `accounting.settings.approve`): transaction, mapping row `FOR UPDATE`, `version` মেলানো (না মিললে `SET_STALE_REQUEST`), `UPDATE` বা `INSERT`, audit আগে ও পরে সহ (`accounting.mapping.changed`)।
3. `settings_require_second_approval` বন্ধ থাকলে একই লেখা সরাসরি, একই transaction-কাঠামোয়।
4. Deactivate: `is_active = false`, একই পথ।

**Task scope**
1. Migration `acc_account_mappings` (composite FK সহ)।
2. `MappingRequirements` (কোডে, `asset_finance_event` + `write_off`)।
3. Validator (account যাচাই + সতর্কতা)।
4. `ChangeApplier` রেজিস্ট্রি (যদি `P1-04` আগে না রেখে থাকে সেখানেই) + `mapping:` applier।
5. API (ওপরের Contract)।
6. UI (নিচে)।
7. Test।

**Out of scope**
Draft তৈরি (`P4-06`); আটকে থাকা event ছাড়ার কাজ নিজে (`P4-09`-এর `release-held`); SoD-র approve-এ প্রয়োগ (`P4-08`); `flag:` applier (`P4-11`)।

**Contract**
- `GET /accounting/mappings`
- `PUT /accounting/mappings/{source}/{event}/{key}` (`account_id`, `version`, `reason`)
- `POST /accounting/mappings/{source}/{event}/{key}/deactivate` (Pack-এ `POST .../deactivate` লেখা)

**UI**
1. প্রয়োজনীয় mapping-এর তালিকা (source ও event ধরে)।
2. প্রতিটার অবস্থা (বসানো, বাকি)।
3. Account picker (leaf ও active)।
4. সতর্কতা (Debit/Credit account type-এর অসামঞ্জস্য)।
5. History drawer।
6. অপেক্ষমাণ অনুমোদনের অনুরোধ।
7. আটকে থাকা event ছাড়ার প্রস্তাব ("৩টা event আটকে আছে, এখন ছাড়বেন?")।

**Permission**
`accounting.mapping.manage`। অনুমোদনের জন্য `accounting.settings.approve` (`P1-04`-এর পথ)।

**Error**
`JRN_ACCOUNT_INVALID`, `JRN_CONFLICT`, `SET_STALE_REQUEST`

**Audit**
`accounting.mapping.changed` (তৈরি, বদল, বন্ধ; আগে ও পরে সহ)

**Acceptance**
AC4.2, AC4.11

**Test**
- বৈধ ও অবৈধ account (অন্য company, inactive, group)।
- দ্বিতীয় অনুমোদন ছাড়া বদল হয় না।
- নিজে অনুমোদন (নিষেধ, `JRN_SOD_VIOLATION`)।
- বদলের পর পুরোনো Draft অপরিবর্তিত।
- আটকে থাকা event-এর সংখ্যা ঠিক।
- Stale অনুরোধ (`SET_STALE_REQUEST`); দুজন একসাথে `version` ভাঙা (`JRN_CONFLICT`)।
- সরাসরি SQL-এ অন্য company-র account বসানো DB FK আটকায়।

**Deliverable**
Migration, service, `ChangeApplier` রেজিস্ট্রি, API, UI।

**Sub-tasks**
migration, requirements, validator, applier, API, UI, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC4.2, AC4.11 (mapping-অংশ) সবুজ।
- সব তালিকা ও বাটন `can()` ও permission দেখে।

**Architecture constraints**
- Mapping কোডে বাঁধা নয়; `MappingRequirements` শুধু প্রয়োজনীয় তালিকা, account নয়।
- আলাদা approval table বানানো নয়; `P1-04`-এর change request পুনর্ব্যবহার।
- UI-তে business rule নকল নয়; server-ই আসল বাধা।

**Open points (Pack-এ অস্পষ্ট)**
- `ChangeApplier` রেজিস্ট্রি `P1-04`-এর স্পেক-এ নেই (Pack বলে "P1-04-এর service-এ একটা রেজিস্ট্রি, এই card নিবন্ধন করে")। ধরে নেওয়া: রেজিস্ট্রি না থাকলে এই task-ই ছোট skeleton যোগ করে; `P4-11` ও `P5-06` একই রেজিস্ট্রিতে `flag:` applier দেয়।
- `P1-04`-এর অনুমোদন-flow (step 6) `acc_settings` row-এর `version` মিলিয়ে stale ধরে; mapping-এর stale-check `acc_account_mappings.version`-এ করতে হবে। applier-এর contract-এ "stale-check কোথায়" ঠিক করা দরকার।
- `P4-05` (ক্রম 35) `P4-09` (ক্রম 37)-এর আগে; UI-র "ছাড়ার প্রস্তাব" বোতাম `release-held` API পাওয়া পর্যন্ত লুকিয়ে বা disabled রাখতে হবে।
- ডিফল্ট role-ভিত্তিক `accounting.mapping.manage`-এর বরাদ্দ Pack-এ (Blueprint ৪.৫-এ) আছে, এখানে নেই।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P4-06 — Event থেকে Draft Journal

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service)
Title
Event থেকে Draft Journal — `DraftBuilder` ও event-journal link
Branch Name
`feature/accounting-event-draft-builder`
View Order
58
Description
**Summary**
`received` inbox row থেকে `DraftBuilder` সম্পূর্ণ, balanced, validation-pass করা Draft Journal বানাবে। কিছু incomplete থাকলে journal তৈরি হবে না — event `held_*` বা `failed`-এ আটকে থাকবে। Accountant শুধু Draft দেখে submit করবেন; auto-post নেই।

**Task serial**
`ACC-P4-06 — Event থেকে Draft Journal` · Feature

**Depends on**
- `ACC-P4-03` (consumer job, যা row দখল করে `DraftBuilder::build` ডাকে)
- `ACC-P4-05` (account mapping, `MappingRequirements`)
- `ACC-P2-02` (Draft তৈরি, `JournalService::createDraft` ও Draft সম্পাদনার নিয়ম)
- `ACC-P2-03` (`SaveValidator`)
- OD: `OD-10` (লাইনের নিয়ম), `OD-3` (currency)

**Relation: আগের task**
- `P4-03` দিয়েছে: দখল করা inbox row (`FOR UPDATE SKIP LOCKED`), প্রতি row আলাদা transaction, আর ফল শ্রেণিবিভাগ (এই task থেকে ছোঁড়া `EVT_*` error গুলো consumer inbox অবস্থায় রূপ দেয়)।
- `P4-05` দিয়েছে: `acc_account_mappings` ও `MappingRequirements`; `DraftBuilder` প্রতিটা প্রয়োজনীয় key-র mapping পড়ে ও snapshot রাখে। `updated_by` যায় snapshot-এ (SoD-র জন্য)।
- `P2-02` দিয়েছে: journal তৈরির প্রোগ্রামেটিক পথ (`createDraft`), এবং Draft সম্পাদনার সংশোধিত নিয়ম: `journal_type = 'event_generated'` Draft-এ `created_by` null, তাই যেকোনো `accounting.journal.create`-ওয়ালা সম্পাদনা করতে পারে, SoD-র Maker = `submitted_by`।
- `P2-03` দিয়েছে: `SaveValidator` (account একই company-র ও active leaf, Debit/Credit নিয়ম, currency = base currency)। Event-journal বানাতে `createDraft` এটা চালায়।
- `P1-09` দিয়েছে: `acc_journals`-এর `journal_type = 'event_generated'`, `source_type/source_id/source_event` column, generated column `source_active` ও `uq_acc_journals_source_active (company_id, source_active)` (cancelled বাদে, R4.10), আর `acc_journal_lines`।
- `P4-01` দিয়েছে OD-10 (line-নিয়ম) ও OD-3 (currency)।

**Relation: পরের task (কে অপেক্ষা করছে)**
Pack: এই task `P4-07`, `P4-08`, `P4-10`-কে অবরুদ্ধ করে। এছাড়া `P4-09` (link ও journal-লিংক ops screen-এ) এবং `P4-12`।
- `P4-07` (Asset-এ ফেরত): Draft commit-এর পর (`afterCommit`, OD-11 (ক)) `AcknowledgeEventJob` dispatch করে।
- `P4-08` (approval ও SoD): `acc_finance_event_journal_links.mapping_snapshot.updated_by` পড়ে।
- `P4-10` (RC8): inbox `draft_created` row ↔ সক্রিয় link ↔ journal মিলায়; পোস্টকৃত event-journal-এর `total_debit` ↔ inbox `amount`।
- `P4-09` (ops screen): বিস্তারিত পাতায় "লিঙ্কযুক্ত journal (লিংক)"।
- `P4-12`: AC4.2, AC4.3, AC4.4, AC4.7।

**Related tables / 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-এর)।

ব্যবহার/লেখা: `acc_journals`, `acc_journal_lines`, `acc_journal_status_history` (Section ৯.৪ ধাপ 8-এ), `acc_finance_event_inbox` (`draft_created`)। পড়া: `acc_account_mappings`, `acc_accounts`, `acc_settings` (`base_currency`)।

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

**Business rules**
- R4.1 থেকে R4.10, R4.14 (Blueprint)।
- **অসম্পূর্ণ journal কখনো নয়।** কিছু বাকি থাকলে event `held_*` বা `failed`, journal বানানো হয় না।
- **নিজে post কখনো নয় (AC4.4)।** Draft কখনো `posted` নয়।
- `journal_date = effective_date` (তৈরির তারিখ নয়, R4.8)।
- দুই worker একই row-এ এলে একটাই Draft হয় (`uq_acc_journals_source_active` + link-এর `uq_acc_fejl_live`)।
- Event-তৈরি Draft সংশোধন: যেকোনো `journal.create`-ওয়ালা সংশোধন করতে পারে। amount বদলালে audit-এ আগে ও পরে, আর UI-তে "event-এর amount থেকে আলাদা" চিহ্ন। Post হওয়ার পর RC8 (`P4-10`) পার্থক্য ধরে।
- Draft `cancelled` হলে (R4.10) link `is_live = false` আর inbox `draft_created` -> `received`, ফলে event আবার প্রক্রিয়া হয় ও নতুন Draft বানায়। চাইলে মানুষ আগে `ignore` করতে পারে (`P4-09`)।

**DB flow: `DraftBuilder::build(inboxRow)`** (একটাই transaction, `P4-03` দখল করা row-এ)
1. row `FOR UPDATE`; `status = 'received'` না হলে বেরিয়ে যাওয়া।
2. Payload যাচাই: `effective_date` আছে, `amount > 0`, `currency` আছে, `integrity_flags` ফাঁকা। নইলে `EVT_PAYLOAD_INVALID` বা `EVT_COMPANY_MISMATCH` -> `failed`।
3. `currency = base_currency` না হলে `EVT_CURRENCY_MISMATCH`।
4. `MappingRequirements` অনুযায়ী দুটো mapping পড়া, active ও বৈধ। না থাকলে `EVT_MAPPING_MISSING` (কোন key বাকি তা `error_message`-এ) -> `held_mapping`।
5. লাইন বানানো (উপরের line-নিয়ম)।
6. `JournalService::createDraft`: `journal_type = 'event_generated'`, `journal_date = effective_date`, `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`।
7. `INSERT` journal-এ `uq_acc_journals_source_active` ভাঙলে (দুই worker একসাথে): আগের journal পড়ে সেটাই ব্যবহার, নতুন কিছু না বানিয়ে।
8. `INSERT` link (`mapping_snapshot` সহ); `acc_journal_status_history` (from null -> `draft`, Section ৯.৪)।
9. `InboxTransitionService`: `received` -> `draft_created`।
10. Audit।
11. commit।

**Draft বাতিল হলে:** Journal `cancelled` হলে একটা listener (`JournalCancelled`) transaction-এ link `is_live = false` আর inbox `draft_created` -> `received`।

**Task scope**
1. Migration `acc_finance_event_journal_links` (generated column, দুই unique, composite FK)।
2. `DraftBuilder::build(inboxRow)` (ওপরের ১১ ধাপ)।
3. Payload যাচাই, currency যাচাই, mapping পড়া, লাইন বানানো।
4. `createDraft` ডাকা ও concurrency পথ (step 7)।
5. Link ও `mapping_snapshot`।
6. `JournalCancelled` listener।
7. Test।

**Out of scope**
Consumer job (`P4-03`); mapping CRUD (`P4-05`); Asset-এ ফেরত (`P4-07`); approval/SoD-র শাখা (`P4-08`); Submit, approve, post (Phase 2-এর পথ)।

**Contract**
`DraftBuilder::build(InboxRow): DraftBuildResult` (সফল / held / failed + কারণ)। `JournalCancelled` listener। কোনো HTTP endpoint নেই।

**Permission**
প্রযোজ্য নয় (system job)। Draft সম্পাদনা `accounting.journal.create` (`P2-02`)।

**Error**
`EVT_PAYLOAD_INVALID`, `EVT_COMPANY_MISMATCH`, `EVT_CURRENCY_MISMATCH`, `EVT_MAPPING_MISSING` (সবই inbox অবস্থা হিসেবে, কোনো HTTP error নয়)

**Audit**
`accounting.event.draft_created`, `accounting.event.held`, `accounting.event.failed`

**Acceptance**
AC4.2, AC4.3, AC4.4, AC4.7

**Test**
- স্বাভাবিক Draft (দুই line, balanced, `journal_date = effective_date`, cost center)।
- প্রতিটা বাদ-যাওয়া mapping (`debit_account`, `credit_account`)।
- Inactive account (mapping-এর account এখন inactive) -> `held_mapping`।
- Currency অমিল -> `EVT_CURRENCY_MISMATCH`।
- দুই worker একই row-এ: একটাই Draft, দ্বিতীয়টা আগেরটাই ব্যবহার করে।
- Draft বাতিলের পর আবার Draft (link `is_live = false`, নতুন link)।
- Amount শূন্য।
- Link ও `mapping_snapshot`-এর সঠিকতা (`account_id`, `version`, `updated_by`)।
- Draft কখনো `posted` নয় (static: `DraftBuilder` থেকে `PostingService`-এ পথ নেই)।

**Deliverable**
Migration, `DraftBuilder`, listener।

**Sub-tasks**
migration, payload যাচাই, currency, mapping, লাইন, `createDraft`, concurrency পথ, link, `JournalCancelled` listener, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC4.2, AC4.3, AC4.4, AC4.7 সবুজ।
- সরাসরি SQL-এ দ্বিতীয় সক্রিয় link/journal বসাতে গেলে DB constraint আটকায় (test প্রমাণ)।

**Architecture constraints**
- `DraftBuilder` থেকে `PostingService`-এ কোনো পথ নেই।
- একটা row = একটা transaction; Draft, link ও inbox অবস্থা একসাথে commit বা কিছুই না।
- Lock ক্রম Pack ৯.১ মানে।

**Open points (Pack-এ অস্পষ্ট)**
- `source_event` column-এর মান কী হবে (প্রস্তাব: inbox-এর `event_type`, `source_type`/`source_id` inbox-এর সমান) Pack-এ সরাসরি লেখা নেই।
- `JournalCancelled` event কোন card dispatch করে (`P2-01` transition service বা `P2-02` cancel) Pack-এ বলা নেই; নিশ্চিত করতে হবে `P2-01`/`P2-02` এটা ছুঁড়ছে।
- `P4-06`-এর লেখা তালিকায় `acc_journal_status_history` আলাদা করে নেই, কিন্তু Section ৯.৪ ধাপ 8 তে আছে; এখানে ৯.৪ অনুসরণ করা হয়েছে।
- OD-3 অন্য সিদ্ধান্তে গেলে (শুধু `hold` নয়) step 3 ও `P4-03`-এর শ্রেণিবিভাগ বদলাবে।
- `dimensions` (cost center) event-এ না থাকলে line-এ null যাবে কি না Pack বলেনি।

Owner
Dev A
Estimate Hours
6
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Feature (service + api + ui)
Title
তিন স্তরের flag ও প্রথমবার চালু
Branch Name
`feature/accounting-intake-flags`
View Order
59
Description
**Summary**
Event intake তিন স্তরের flag (`accounting.enabled`, `event_intake.enabled`, `event_intake.sources.asset_write_off`) দিয়ে নিরাপদে on/off করতে হবে; প্রথমবার চালাতে দুজনের সম্মতি। Consumer শুধু তখনই চলবে যখন checklist ও flag সব ঠিক।

**Task serial**
`ACC-P4-11 — তিন স্তরের flag ও প্রথমবার চালু` · Feature

**Depends on**
- `ACC-P1-01` (`AccountingFeatureService`, `Setting`-ভিত্তিক flag)
- `ACC-P1-05` (Setup Checklist, `S01` থেকে `S10`, `I01` থেকে `I03`)
- `ACC-P1-04` (change request ও দ্বিতীয় অনুমোদন, `ChangeApplier` রেজিস্ট্রি)
- `ACC-P4-05` (mapping ও `MappingRequirements`, `I01`-এর ভিত্তি)

**Relation: আগের task**
- `P1-01` দিয়েছে: `accounting.enabled`, `event_intake.enabled`, `event_intake.sources.*` flag যা `acc_settings`-এ নয়, ERPFlow-এর `Setting` model-এ থাকে; `AccountingFeatureService::enabled/intakeEnabled/sourceEnabled` (সব default `false`) এবং middleware `ACC_MODULE_DISABLED`। এই task সেই service-এ চালু-বন্ধের পূর্ণ নিয়ম ও API বসায়।
- `P1-05` দিয়েছে: `ChecklistService::evaluate`, ধাপ `S01`-`S10` (core), `S11`, আর `I01_mappings`, `I02_currency_policy`, `I03_source_flags` (intake group), এবং `canEnable()`। Pack-এ `S01` থেকে `S10` সবুজ না হলে `accounting.enabled` চালু যায় না (`SET_INCOMPLETE`)।
- `P1-04` দিয়েছে: change request (`acc_setting_change_requests`), "অনুরোধকারী নিজে অনুমোদন নয়" নিয়ম, `accounting.settings.approve`, এবং `ChangeApplier` রেজিস্ট্রি যেখানে `P4-05` `mapping:` applier দিয়েছে। এই task `flag:` prefix-এর applier যোগ করে।
- `P4-05` দিয়েছে: mapping ও `MappingRequirements` যা ছাড়া `I01` সবুজ হয় না।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P4-03` (Consumer job): Pack বলে এই task `P4-03`-কে **চালানোর জন্য** অবরুদ্ধ করে। Consumer শুধু `sourceEnabled` দেখে।
- `P4-12`: AC4.5 (`IntakeFlagMatrixTest`)।
- `P5-06` (Release gate): `accounting.enabled` চালু করে (দুজন, `flag:accounting.enabled` key) ও এই task-এর `flag:` applier ও `ChecklistService` নিয়ম পুনর্ব্যবহার করে। P5-06 এই task-এর flag service-এর ওপর নির্ভর করে।
- `P5-08` (Pilot চালু): ধাপ 5 এ `event_intake.enabled` ও `sources.asset_write_off` চালু হয় এই task-এর "প্রথমবারের নিয়ম" মেনে।

**Related tables / schema**
কোনো নতুন table নেই। Flag `Setting` model-এ (group `accounting`/`event_intake`), `acc_setting_change_requests` (`P1-04`), `acc_setup_status` (`P1-05`), `activity_logs`।

**তিন flag ও চালুর শর্ত:**

| 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 যাচাই করে না) |

**Business rules**
- `AccountingFeatureService::intakeEnabled(company) = enabled && intake.enabled`; `::sourceEnabled(company, source) = intakeEnabled && sources.x`। Consumer শুধু `sourceEnabled` দেখে (`P4-03`)।
- **চালু করা:** `accounting.enable.manage` permission, Checklist শর্ত, **প্রথমবার** দ্বিতীয় জনের অনুমোদন।
- **প্রথমবার** = এই company-র এই flag কখনো চালু ছিল না। সরাসরি লেখা নয়, change request (key `flag:event_intake.enabled`), অন্য একজন (`accounting.settings.approve`) অনুমোদন করলে তবে চালু। পরের বার (বন্ধ করার পর আবার) একজনেই চালাতে পারে।
- **বন্ধ করা:** সঙ্গে সঙ্গে, অনুমোদন ছাড়া (জরুরি নিরাপত্তা), `reason` বাধ্যতামূলক। Consumer-এর বর্তমান run পরের loop-এ থামে। আগের Draft, inbox কিছু মোছা হয় না।
- Flag Default সবসময় `false`। Flag বন্ধ করলে data অক্ষত থাকে।

**DB flow: চালু**
`POST /accounting/flags/{flag}` (`enabled = true`, `reason`):
1. `accounting.enable.manage` permission যাচাই।
2. Checklist শর্ত (`ChecklistService`)।
3. প্রথমবার হলে সরাসরি লেখা নয়: `P1-04`-এর change request (key `flag:event_intake.enabled`), অন্য একজন (`accounting.settings.approve`) অনুমোদন করলে তবে চালু (`ChangeApplier` `flag:` prefix)। পরের বার একজনেই।
4. `Setting` লেখা।
5. Audit `accounting.flag.enabled`।

**DB flow: বন্ধ**
`POST /accounting/flags/{flag}` (`enabled = false`, `reason` বাধ্যতামূলক): permission, `Setting` লেখা, audit `accounting.flag.disabled`; অনুমোদন ছাড়া।

**Task scope**
1. `AccountingFeatureService`-এ `intakeEnabled`, `sourceEnabled`-এর চূড়ান্ত নিয়ম (ওপরে)।
2. চালুর শর্ত যাচাই (`ChecklistService` ব্যবহার করে)।
3. প্রথমবারের অনুমোদন: `flag:` applier (`ChangeApplier` রেজিস্ট্রিতে)।
4. বন্ধ করা।
5. API `POST /accounting/flags/{flag}`।
6. UI।
7. Test।

**Out of scope**
`accounting.enabled` চালুর Release gate ও তিন গেট (`P5-06`); consumer নিজে (`P4-03`); alert (`P5-03`)।

**Contract**
`POST /accounting/flags/{flag}` (`enabled`, `reason`)। Service `AccountingFeatureService`।

**UI**
Settings-এর "Event intake" অংশ: তিন flag-এর অবস্থা, প্রতিটার শর্ত (সবুজ বা লাল তালিকা), "চালু" বাটন শর্ত পূরণে সক্রিয়, "বন্ধ" সবসময় সক্রিয়।

**Permission**
`accounting.enable.manage` (চালু বা বন্ধ), `accounting.settings.approve` (প্রথমবারের অনুমোদন)

**Error**
`SET_INCOMPLETE`, `ACC_FORBIDDEN`, `JRN_SOD_VIOLATION`

**Audit**
`accounting.flag.enabled`, `accounting.flag.disabled`

**Acceptance**
AC4.5

**Test**
- তিন flag-এর সব ২³ সমন্বয়ে consumer কী করে (`IntakeFlagMatrixTest`)।
- শর্ত পূরণ না হলে চালু হয় না (`SET_INCOMPLETE`, কোন ধাপ বাকি তার তালিকা সহ)।
- প্রথমবার নিজে অনুমোদন (নিষেধ, `JRN_SOD_VIOLATION`)।
- বন্ধ করলে চলমান run থামে।
- বন্ধের পর data অক্ষত (Draft, inbox, audit)।
- পরের বার একজনেই চালু করা যায়।

**Deliverable**
Service, API, UI।

**Sub-tasks**
service, চালুর শর্ত, প্রথমবারের অনুমোদন (applier), বন্ধ, UI, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC4.5 সবুজ।
- কোনো flag কোনো code-পথে Default `true` নয়।

**Architecture constraints**
- Flag `acc_settings`-এ নয়, `Setting` model-এ (`P1-01`-এর সিদ্ধান্ত)।
- বন্ধ করা কখনো অনুমোদনের পেছনে আটকে থাকে না।
- আলাদা approval table নয়; change request পুনর্ব্যবহার।

**Open points (Pack-এ অস্পষ্ট)**
- "প্রথমবার" নির্ধারণের উৎস Pack বলেনি। `acc_enablement_log` Phase 5-এ (`P5-06`) তৈরি হয়, তাই Phase 4-এ উৎস হতে পারে `activity_logs`-এর `accounting.flag.enabled` event বা `Setting` ইতিহাস। সিদ্ধান্ত লাগবে।
- `P5-06` `acc_enablement_log`-এ intake flag-ও রাখে (`flag` column-এ `event_intake.enabled` ইত্যাদি), কিন্তু এই task আগে বানানো হয়; P5-06-এর সময় এই task-এর flag service-কে ওই log-এ লেখাতে হবে কি না Pack বলেনি।
- `ChecklistService` Pack-এ "নতুন" লেখা, যদিও `P1-05` আগেই `ChecklistService::evaluate` ও `I01`-`I03` checker (`checker ×14`) বানায়; এই task শুধু "flag-চালুর শর্ত যাচাই" method যোগ করে ধরা হয়েছে।
- `P1-05` `POST /accounting/enable` endpoint-এর কথা বলে (পূর্ণ রূপ `P5-06`), এখানে `POST /accounting/flags/{flag}`; দুটো একই কাজ করলে একটা অন্যটার alias কি না ঠিক করতে হবে।
- `P1-04`-এর stale-check `acc_settings.version` ভিত্তিক, flag `Setting` model-এ; `flag:` applier-এর stale-check কীভাবে হবে তা নিশ্চিত করতে হবে।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Feature (service + job, Accounting ও Asset দুই দিকে)
Title
Asset-এ ফেরত জানানো (`accepted`) ও Asset-এর ছোট পরিবর্তন
Branch Name
`feature/accounting-asset-acknowledge`
View Order
60
Description
**Summary**
Draft commit-এর পর (OD-11 (ক)) বা Post-এর পর (OD-11 (খ)) Asset-এ outbox event `accepted` mark করতে হবে — Accounting সরাসরি Asset table ছুঁবে না, শুধু `FinanceEventAcknowledger` interface। Acknowledge fail হলেও journal/inbox অক্ষত; পরে retry।

**Task serial**
`ACC-P4-07 — Asset-এ ফেরত জানানো ও Asset-এর ছোট পরিবর্তন` · Feature

**Depends on**
- `ACC-P4-06` (Draft journal ও inbox `draft_created`)
- `ACC-P4-01` (OD-11 `accepted` কখন, OD-21 `effective_date`/`schema_version` কোথা থেকে)

**Relation: আগের task**
- `P4-06` দিয়েছে inbox `draft_created` অবস্থা, link, এবং commit-এর পর (`afterCommit`) ধরার জায়গা। OD-11 (ক) হলে এই `afterCommit`-ই `AcknowledgeEventJob` dispatch-এর মুহূর্ত।
- `P4-01` দিয়েছে OD-11 ও OD-21, যা এই task-এর দুটো শাখা ঠিক করে (নিচে)।
- `P4-02` দিয়েছে inbox table যেখানে এই task expand migration করে। `P4-04`-এর reader আলাদা (পড়ার জন্য), এই task লেখার জন্য আলাদা interface।
- `P2-12`-এর `JournalPosted` event (Section ৯.২ ধাপ 14): OD-11 (খ) হলে এই event-এ job চালে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P4-12`: AC4.8 (`AssetBehaviorParityTest`), AC4.13 (`AcknowledgeFailureTest`) ও `AC4.10`-এর process-kill দৃশ্য (Draft তৈরির পর, ফেরতের আগে kill)।
- `P4-10` (RC8.5): Asset-এ `accepted` ↔ Accounting inbox, আর `draft_created` কিন্তু Asset-এ এখনো `pending` ও `ack_attempts` শেষ -> ব্যর্থতা। এই task-এর `acknowledged_at`, `ack_attempts` সেখানে পড়া হয়।
- `P5-03` (monitoring): `ack_failing` alert এই task-এর `ack_attempts` ও `ack_error` পড়ে।
- `P4-13`: runbook-এর "ফেরত-জানানোর ব্যর্থতা" অংশ।

**Related tables / schema**

**Accounting-এর দিক, expand migration `acc_finance_event_inbox`-এ:**

| Column | ধরন | Null | নোট |
| --- | --- | --- | --- |
| `acknowledged_at` | TIMESTAMP | হ্যাঁ | Asset-এ সফল জানানোর সময় |
| `ack_attempts` | SMALLINT UNSIGNED | না | Default 0 |
| `ack_error` | VARCHAR(500) | হ্যাঁ |  |

**Asset-এর দিক (Asset module-এ, শুধু যেটুকু দরকার):**
- বিদ্যমান `asset_finance_events` (`status`: `pending` -> `accepted`)। কোনো 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`।

**Business rules**
- R4.13: ফেরত জানানো ব্যর্থ হলে journal বা inbox-এর অবস্থা বদলায় না; পরে আবার চেষ্টা।
- Acknowledger **idempotent**: `UPDATE ... WHERE status = 'pending'`; affected rows 0 হলে আগে থেকেই `accepted` (ঠিক আছে) বা অন্য অবস্থা (সতর্কতা log)।
- **ধ্রুব নিয়ম:** Asset-এর write-off-এর business rule, executor-এর আচরণ আর বিদ্যমান test অপরিবর্তিত। এই পরিবর্তনের জন্য Asset-এর বিদ্যমান test suite সবুজ আর একটা আচরণ-তুলনা test (পরিবর্তনের আগে ও পরে write-off চালিয়ে asset-এর অবস্থা আর outbox-এর row একই, নতুন column বাদে)।
- Accounting সরাসরি Asset-এর table ছোঁয় না; শুধু interface-এর মাধ্যমে।

**DB flow: `AcknowledgeEventJob(inboxId)`**
1. company context (`P1-12`)।
2. inbox `FOR UPDATE`; `status = 'draft_created'` আর `acknowledged_at IS NULL` না হলে বেরিয়ে যাওয়া।
3. **OD-11 (ক):** commit-এর পর (`P4-06`-এর `afterCommit`) dispatch। **(খ):** `JournalPosted` event-এ, সংশ্লিষ্ট link ধরে।
4. `acknowledger->accepted(source_id)`।
5. সফল হলে `acknowledged_at = NOW()`।
6. ব্যর্থ হলে `ack_attempts + 1`, `ack_error`, backoff সহ আবার চেষ্টা। ব্যর্থতা journal বা inbox-এর অবস্থা বদলায় না।

**Asset-দিকের লেখা (`AssetFinanceEventAcknowledger::accepted(eventId)`):**
`UPDATE asset_finance_events SET status = 'accepted' WHERE id = ? AND status = 'pending'`।

**Task scope**
1. Accounting expand migration (তিন column)।
2. Interface `FinanceEventAcknowledger`: `accepted(sourceId): void` (idempotent); `source_type` অনুযায়ী container থেকে সঠিক implementation।
3. Job `AcknowledgeEventJob(inboxId)` (ওপরের flow), backoff সহ।
4. Asset-এ `AssetFinanceEventAcknowledger` (Asset-এর নতুন service, Accounting-এর interface বাস্তবায়ন করে)।
5. OD-21 (ক) হলে: Asset migration (`effective_date`, `schema_version`), `AssetFinanceEventWriter::write` write-off-এর ক্ষেত্রে `effective_date` বসায় (write-off-এর আসল তারিখ), বিদ্যমান `pending` row-এর জন্য একবার চালানো idempotent backfill (source record থেকে তারিখ)। Unique key আর বাকি schema অপরিবর্তিত।
6. আচরণ-তুলনা test।
7. OD-11-এর দুই রূপের test।

**Out of scope**
Asset-এর business rule বদল; Asset-এ journal id বা GL column; OD-22 (বাতিল বা সংশোধিত write-off) এর Asset-দিকের আরও কাজ (এই Pack-এ নেই); Asset-এর অন্য event (disposal, acquisition)।

**Contract**
Interface `FinanceEventAcknowledger::accepted(sourceId)`; Job `AcknowledgeEventJob(inboxId)`। কোনো HTTP endpoint নেই।

**Permission**
প্রযোজ্য নয় (background)।

**Error**
কোনো user-মুখী error নেই (background)।

**Audit**
`accounting.asset_status.sent`, `accounting.asset_status.failed`

**Acceptance**
AC4.8, AC4.13

**Test**
- সফল `accepted`; একই ফেরত দুইবার (idempotent)।
- Asset-এ ব্যর্থতা: journal ঠিক থাকে, inbox `draft_created`, `ack_attempts` বাড়ে, পরে আবার চেষ্টায় সফল।
- Asset-এর বিদ্যমান test suite সবুজ।
- `AssetBehaviorParityTest`: আচরণ-তুলনা (asset-এর অবস্থা ও outbox row আগে-পরে একই, নতুন column বাদে)।
- OD-11-এর দুই রূপ: (ক) Draft-এ, (খ) Post-এ।
- Backfill idempotent (দুইবার চালালে একই ফল)।

**Deliverable**
Interface, job, migration, Asset-এর acknowledger ও (প্রযোজ্য হলে) migration ও writer-এর পরিবর্তন, backfill।

**Sub-tasks**
interface, job, acknowledger, OD-21 migration ও writer, backfill, আচরণ-তুলনা test, test

**Definition of done**
- সাধারণ DoD পূরণ (এই task-এ Asset-দিকের কাজ অনুমোদিত ব্যতিক্রম)।
- AC4.8, AC4.13 সবুজ।
- Asset-এর বিদ্যমান suite সবুজ, আচরণ-তুলনা test সবুজ।
- সব migration expand-only ও পিছনে সামঞ্জস্যপূর্ণ।

**Architecture constraints**
- Accounting Asset-এর table সরাসরি ছোঁয় না; Asset-এর নিজের acknowledger দিয়ে।
- Asset-এর outbox-এ ledger-সদৃশ কিছু (journal id, GL column) যোগ নয়।
- ব্যর্থ ফেরত জানানো Accounting-এর data কখনো বদলায় না।

**Open points (Pack-এ অস্পষ্ট)**
- `ack_attempts`-এর সর্বোচ্চ সীমা ও backoff এখানে "backoff সহ আবার" বলা আছে, মান নেই (`intake_max_attempts`/`intake_retry_backoff_seconds` প্রক্রিয়ার জন্য; ack-এর জন্য ওই setting নাকি আলাদা, বলা নেই)। `P4-10` (RC8.5) ও `P5-03` (`ack_failing`) "`ack_attempts` শেষ" ধরে তাই সীমা ঠিক করা জরুরি।
- OD-11 (খ)-তে `AcknowledgeEventJob`-এর step 2-এর শর্ত (`status = 'draft_created'`) Post-এর সময়ও সত্য থাকবে ধরা হয়েছে; Draft `cancelled` হলে inbox `received`-এ ফেরে, তখন কী হবে বলা নেই।
- OD-22 (খ)/(গ) হলে Asset-এ আরও কাজ লাগবে, যা এই Pack-এ নেই।
- Asset-এর `accepted` ছাড়া অন্য অবস্থা (`rejected` ইত্যাদি) আছে কি না Pack-এ নেই, তাই "অন্য অবস্থা" সতর্কতা log ছাড়া কিছু নির্ধারিত নয়।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P4-08 — Event-journal-এর approval ও SoD

Project
ERPFlow
Module
Accounting Module
Type
Feature (service)
Title
Event-journal-এর approval ও SoD
Branch Name
`feature/accounting-event-journal-sod`
View Order
61
Description
**Summary**
Event-generated journal (`journal_type = 'event_generated'`) সাধারণ journal-এর মতোই Submit → approve → post পথে যাবে। যে mapping edit করেছে (`mapping_snapshot.updated_by`) সে সেই journal approve করতে পারবে না — `SodGuard`-এ নতুন শাখা।

**Task serial**
`ACC-P4-08 — Event-journal-এর approval ও SoD` · Feature

**Depends on**
- `ACC-P4-06` (`acc_finance_event_journal_links.mapping_snapshot`)
- `ACC-P2-08` (approval request ও সিদ্ধান্ত)
- `ACC-P2-10` (`SodGuard`)

**Relation: আগের task**
- `P4-06` দিয়েছে `mapping_snapshot` (প্রতিটা key-র `account_id`, mapping `version`, `updated_by`, সময়) যা এই task পড়ে। `P4-05` এর `updated_by` সেখানে ঢোকায়।
- `P2-08` দিয়েছে submit/approve/reject/withdraw/return পথ, `ApprovalResolver` ও request-এর `rule_snapshot`; Event-journal এই একই পথ ধরে চলে, আলাদা কিছু নেই।
- `P2-10` দিয়েছে `SodGuard::assert(action, actor, subject)` ও setting `sod_mapping_editor_cannot_approve` (default true) যা ঠিক এই task-এর জন্য `P2-10`-এ রাখা হয়েছিল; `P2-10` "অবরুদ্ধ করে" তালিকায় `P4-08` আছে।
- `P2-06` আগে থেকেই approval rule-এ `journal_type = 'event_generated'` (বা `any`) সমর্থন করে।
- `P2-02` Draft সম্পাদনার নিয়ম: `created_by` null হলে Maker = `submitted_by`।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P4-12`: Pack অনুযায়ী এই task শুধু `P4-12`-কে অবরুদ্ধ করে; AC4.11 (`MappingSodTest`) এর test।
- `P5-01` UAT-এর U11 (Asset write-off থেকে Draft) ও U13 (অনুমতিহীন চেষ্টা) পরোক্ষভাবে এই নিয়ম ছোঁয়।

**Related tables / schema**
কোনো নতুন table নেই। পড়া: `acc_finance_event_journal_links.mapping_snapshot`, `acc_journals.submitted_by`, `acc_approval_rules`/`acc_approval_requests.rule_snapshot` (rule-এর জন্য)।

**Business rules**
1. Approval rule `journal_type = 'event_generated'` (বা `any`) তৈরি করা যায় (`P2-06` আগে থেকেই সমর্থন করে)। **কোনো rule স্বয়ংক্রিয়ভাবে বানানো হয় না।** Rule না থাকলে `APR_NO_RULE` (setting `approval_no_rule_behavior` default `block` অনুযায়ী)।
2. `SodGuard::assert('approve', actor, journal)`-এ নতুন শাখা: `journal.source_type` থাকলে লিঙ্কের `mapping_snapshot.updated_by` পড়ে, approver সেই ব্যক্তি হলে `sod_mapping_editor_cannot_approve` অনুযায়ী `JRN_SOD_VIOLATION`।
3. Event-journal-এ Maker = `submitted_by` (কারণ `created_by` null)। তাই SoD "Maker নিজে approve নয়" (`sod_maker_cannot_approve`) `submitted_by`-র ওপর।
4. Event-journal Post-এর সময়ও SoD `sod_maker_cannot_post` `submitted_by`-র ওপর।
5. Setting `sod_mapping_editor_cannot_approve` বন্ধ করলে নিয়মটি আর খাটে না।

**DB flow**
কোনো নতুন লেখা নেই। পড়া:
- `acc_finance_event_journal_links.mapping_snapshot` (journal-এর লিঙ্ক ধরে, `uq_acc_fejl_journal`)
- `acc_journals.submitted_by`

**Task scope**
1. `SodGuard`-এ mapping-editor শাখা।
2. `submitted_by`-ভিত্তিক Maker নিয়ম (approve ও post দুই জায়গায়, `P2-08`/`P2-12` যেখানে `SodGuard` ডাকে)।
3. Rule না থাকলে `APR_NO_RULE` আচরণ test দিয়ে প্রমাণ।
4. Test।

**Out of scope**
নতুন table, নতুন approval engine, rule স্বয়ংক্রিয় তৈরি, mapping বদলানোর flow (`P4-05`), নতুন permission।

**Contract**
`SodGuard::assert(string $action, User $actor, mixed $subject): void` (বিদ্যমান signature, শাখা যোগ)।

**Permission**
প্রযোজ্য নয় (বিদ্যমান `accounting.journal.approve`, `accounting.journal.post`)।

**Error**
`JRN_SOD_VIOLATION` (403), `APR_NO_RULE` (422)

**Audit**
`accounting.guard.denied` (কোন নিয়ম ভাঙল সহ, `P2-10` লেখে)

**Acceptance**
AC4.11

**Test**
- Mapping-বদলকারী ওই journal approve করতে পারে না (`JRN_SOD_VIOLATION`)।
- `sod_mapping_editor_cannot_approve` বন্ধ করলে পারে।
- `submitted_by`-ভিত্তিক Maker-নিয়ম: যে submit করেছে সে নিজে approve/post করতে পারে না।
- Rule না থাকলে আটকে (`APR_NO_RULE`)।
- অন্য mapping-বদলকারী (যে বদলায়নি) approve করতে পারে।

**Deliverable**
`SodGuard`-এর শাখা, test।

**Sub-tasks**
guard শাখা, `submitted_by` নিয়ম, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC4.11 সবুজ।
- `SodGuard` ছাড়া অন্য কোথাও SoD-নিয়ম নকল নয়।

**Architecture constraints**
- SoD নিয়ম একটাই জায়গায় (`SodGuard`), বদ্ধ তালিকা।
- নতুন approval engine বা table নয়।

**Open points (Pack-এ অস্পষ্ট)**
- `mapping_snapshot` এ দুটো key (`debit_account`, `credit_account`) আলাদা `updated_by` হতে পারে। Pack বলেছে "সেই ব্যক্তি", ধরে নেওয়া হয়েছে snapshot-এর **যেকোনো** key-র `updated_by` হলেই আটকাবে। নিশ্চিত করতে হবে।
- `APR_NO_RULE` তৈরি হলে কে/কীভাবে rule বানাবে তা Setup Checklist `S07` (Phase 2 চালু হলে) আর `P2-06` করে; Event-journal-এর জন্য আলাদা rule ছাড়া কীভাবে `any` rule কাজ করবে তা `P2-07` resolver থেকে।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service + api + ui)
Title
Inbox-এর ops screen ও কাজের ইতিহাস
Branch Name
`feature/accounting-intake-ops-screen`
View Order
62
Description
**Summary**
Finance ops-এর জন্য inbox ops screen: আটকে থাকা (`held_*`), failed, conflict event দেখা, release/retry/ignore, conflict accept/reject — প্রতিটি action-এ reason ও `acc_finance_event_actions` ইতিহাস। Consumer heartbeat-ও এক পাতায়।

**Task serial**
`ACC-P4-09 — Inbox-এর ops screen ও কাজের ইতিহাস` · Feature

**Depends on**
- `ACC-P4-02` (inbox, `InboxTransitionService`)
- `ACC-P4-03` (heartbeat, consumer-এর স্বাস্থ্য)
- `ACC-P4-05` (mapping: bulk release `mapping_key` ধরে)
- `ACC-P4-06` (link ও journal-লিংক)

**Relation: আগের task**
- `P4-02` দিয়েছে inbox row ও transition table। এই task সেই table-এর "মানুষ" শাখার সব বদল (release/retry: `held_*`, `failed`, `ignored` -> `received`; `ignore`; conflict সমাধান) এক জায়গায় আনে। `EVT_STATE_INVALID` ওখান থেকে।
- `P4-03` দিয়েছে `acc_intake_heartbeats` যা "Consumer-এর স্বাস্থ্য-বক্স"-এ দেখানো হয় (`last_started_at`, `last_finished_at`, `last_error`, গণনা)।
- `P4-05` দিয়েছে mapping; mapping বসানোর পর `POST /accounting/intake/release-held` (`mapping_key` ধরে) সেই `held_mapping` event ছাড়ে। `P4-05`-এর UI এই API-কে ডাকে।
- `P4-06` দিয়েছে `acc_finance_event_journal_links` (বিস্তারিত পাতায় journal-লিংক) ও `JournalCancelled` listener (সিস্টেমের `draft_created` -> `received`)।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P4-12`: Pack অনুযায়ী এই task শুধু `P4-12`-কে অবরুদ্ধ করে; AC4.2, AC4.10।
- `P4-13` runbook-এর "আটকে থাকা event / ব্যর্থ / conflict / consumer থেমে গেলে" অংশ এই screen ধরে লেখা হয়, `P4-13` demo-ও।
- `P5-03` (monitoring): `held_events`, `failed_events` alert-এর link এই screen-এ।
- `P5-07` training (Finance-এর নির্দেশিকা: "intake-এর আটকে থাকা event")।

**Related tables / schema**

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

| 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)`।

পড়া: `acc_finance_event_inbox`, `acc_finance_event_journal_links`, `acc_journals`, `acc_intake_heartbeats`, `acc_account_mappings`।

**Business rules**
- সব বদল `InboxTransitionService` দিয়ে (`P4-02`); যে অবস্থা থেকে সরাসরি বৈধ নয় তাতে `EVT_STATE_INVALID`।
- `ignore`, `accept_conflict`, `reject_conflict`-এ `reason` বাধ্যতামূলক।
- আগে থেকেই `received` হলে release/retry কিছু না করে সফল (idempotent)।
- `release`/`retry`-এ `attempts = 0`, `next_retry_at = null`, `error_*` মোছা।
- প্রতিটা কাজ `acc_finance_event_actions`-এ ও audit-এ।

**DB flow: release বা retry**
1. idempotency (`Idempotency-Key`)।
2. transaction।
3. inbox row `FOR UPDATE`।
4. `InboxTransitionService` দিয়ে `received`, `attempts = 0`, `next_retry_at = null`, `error_*` মোছা। অবৈধ অবস্থা থেকে `EVT_STATE_INVALID`।
5. `acc_finance_event_actions` row।
6. Audit (`accounting.event.released` বা `.retried`)।
7. commit।

**DB flow: ignore**
একই কাঠামো; `reason` বাধ্যতামূলক; `status = 'ignored'`। Audit `accounting.event.ignored`।

**DB flow: conflict সমাধান**
- `accept`: transaction-এ `payload` = `conflict_payload`, `payload_hash` নতুন, `conflict_payload = null`, `received`। আগে থেকে Draft থাকলে (`draft_created` থেকে `conflict`-এ এসে থাকলে) সেটা বাতিল করে নতুনটা বানানো হবে।
- `reject`: `conflict_payload = null`, আগের অবস্থায় ফেরা (Draft থাকলে `draft_created`, নইলে `received`), `ignored` নয়।

**Task scope**
1. Migration `acc_finance_event_actions` (+ append-only trigger helper)।
2. Service: release/retry, ignore, conflict সমাধান, bulk release (`mapping_key` ধরে)।
3. API (নিচে)।
4. UI (নিচে)।
5. Test।

**Out of scope**
Consumer-এর চলা (`P4-03`); Draft তৈরি (`P4-06`); mapping CRUD (`P4-05`); alert (`P5-03`)।

**Contract**
- `GET /accounting/intake/events?status=&source=&error=&from=&to=`
- `GET /accounting/intake/events/{id}`
- `POST /accounting/intake/events/{id}/retry`, `.../release`, `.../ignore`, `.../resolve-conflict` (`accept` বা `reject`)
- `POST /accounting/intake/release-held` (`mapping_key` ধরে একসাথে, যেমন mapping বসানোর পর)

**UI**
1. গণনা-টাইল: প্রতিটা অবস্থায় কতগুলো।
2. তালিকা (filter সহ), প্রতিটায় কত দিন ধরে আটকে, কারণ।
3. বিস্তারিত পাতা: payload (সুন্দর করে), error, চেষ্টার সংখ্যা, লিঙ্কযুক্ত journal (লিংক), কাজের ইতিহাস।
4. বাটন অবস্থা অনুযায়ী (`can()`): Release, Retry, Ignore (কারণ সহ), Conflict সমাধান (পুরোনো ও নতুন payload পাশাপাশি)।
5. Consumer-এর স্বাস্থ্য-বক্স: সর্বশেষ কখন চলেছে (`acc_intake_heartbeats`), কত পিছিয়ে।
6. গত ৩০ দিনে ignored event-এর তালিকা (Head of Finance পর্যালোচনার জন্য)।

**Permission**
দেখা: `accounting.intake.view`। কাজ: `accounting.intake.manage`

**Error**
`EVT_STATE_INVALID`, `ACC_FORBIDDEN`

**Audit**
`accounting.event.retried`, `accounting.event.released`, `accounting.event.ignored`

**Acceptance**
AC4.2, AC4.10

**Test**
- প্রতিটা বৈধ ও অবৈধ কাজ (transition matrix, মানুষের সারি)।
- `ignore` কারণ ছাড়া -> ব্যর্থ।
- Conflict-এর দুই পথ (`accept`, `reject`; Draft থাকা ও না থাকা দুই অবস্থায়)।
- একই `release` দুইবার (idempotent)।
- Bulk release (`mapping_key` ধরে সবগুলো `held_mapping`)।
- অন্য company-র event (নিষেধ, `ACC_COMPANY_FORBIDDEN`)।
- `acc_finance_event_actions`-এ সরাসরি `UPDATE`/`DELETE` ব্যর্থ।

**Deliverable**
Migration, service, API, UI।

**Sub-tasks**
migration, release ও retry, ignore, conflict, bulk release, API, UI চারটা অংশ, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC4.2, AC4.10 (ops-অংশ) সবুজ।
- সব বাটন ও API permission ও `can()` মানে।

**Architecture constraints**
- অবস্থা বদলের একমাত্র পথ `InboxTransitionService`।
- Actions table append-only।
- UI-তে business rule নকল নয়।

**Open points (Pack-এ অস্পষ্ট)**
- `reject_conflict`-এ "আগের অবস্থায় ফেরা" মানে `conflict`-এ আসার আগের অবস্থা; inbox-এ আগের অবস্থা সংরক্ষণের কোনো column নেই। প্রস্তাব: লিঙ্কের অস্তিত্ব দেখে নির্ণয় (সক্রিয় link থাকলে `draft_created`, নইলে `received`) বা `acc_finance_event_actions.from_status`; নিশ্চিত করতে হবে।
- `accept` conflict-এ "আগে থেকে Draft থাকলে সেটা বাতিল করে নতুনটা বানানো হবে": কে বাতিল করে (এই service নাকি `JournalCancelled` listener) এবং Draft ইতিমধ্যে `submitted`/`approved`/`posted` হলে কী হবে Pack-এ নেই। `posted` Draft বাতিল করা যায় না (Reversal-ই একমাত্র পথ)।
- `ignored` -> `received` (transition table-এ আছে) ops screen-এর কোন বাটন এটা করে (release/retry) নিশ্চিত নয়।
- Bulk release `mapping_key` ধরে: একই `mapping_key` একাধিক source/event-এ থাকলে কোনগুলো ছাড়া পড়বে, তা `source_type`/`event_type` সহ ধরতে হতে পারে।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Feature (service)
Title
Reconciliation RC8 — Event থেকে ledger পর্যন্ত
Branch Name
`feature/accounting-recon-rc8`
View Order
63
Description
**Summary**
Reconciliation check RC8 (`P3-13` framework-এ) যোগ করতে হবে: inbox ↔ link ↔ journal মিল, duplicate নেই, posted amount = inbox amount, Asset `accepted` ↔ Accounting `draft_created`/ack সামঞ্জস্য — Asset থেকে ledger পর্যন্ত chain-এ trust।

**Task serial**
`ACC-P4-10 — Reconciliation RC8 (Event থেকে ledger পর্যন্ত)` · Feature

**Depends on**
- `ACC-P4-06` (inbox ↔ link ↔ journal)
- `ACC-P3-13` (Reconciliation framework)

**Relation: আগের task**
- `P3-13` দিয়েছে: framework (প্রতিটা check আলাদা class, নিজের snapshot transaction), `acc_reconciliation_runs` (append-only; `check_code` `RC1`-`RC9`, `result` `pass/fail/error/skipped`, `detail`, `triggered_by`), রাতের job, dashboard, `ReconciliationFailed` event। RC8 সেখানে "Phase 4" হিসেবে রাখা ছিল (`P3-13` শুধু আটটা check class, RC8 বাদে)।
- `P4-06` দিয়েছে `acc_finance_event_journal_links`, journal-এর `event_generated` ধরন ও inbox `draft_created`। RC8.1-RC8.4 এই table গুলো join করে।
- `P4-04` দিয়েছে Asset reader; RC8.5-এর জন্য এই task সেই reader-এ আলাদা read-only method `fetchAcceptedIds` যোগ করে।
- `P4-07` দিয়েছে `acknowledged_at`, `ack_attempts` (RC8.5 পড়ে)।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P4-12`: Pack অনুযায়ী এই task `P4-12`-কে অবরুদ্ধ করে; AC4.12 (`EventReconciliationTest`)।
- `P5-03` (monitoring): `recon_failed` alert RC1-RC9 যেকোনোটার `fail`-এ; এই task-এর RC8 তার অংশ (`depends on P4-10`)।
- `P3-11` (Period বন্ধ): `recon_pass` check "RC1 থেকে RC7, আর RC8 যদি intake চালু থাকে, RC9" তে RC8 ধরা হয়।
- `P5-01` UAT U12 (dashboard সব সবুজ, একটা ভাঙলে লাল, AC3.10, AC4.12) ও `P5-08` ধাপ 6 (Asset intake-এর পর্যবেক্ষণ শর্ত "RC8 সবুজ")।

**Related tables / schema**
কোনো নতুন table নেই। ফল `acc_reconciliation_runs`-এ (বিদ্যমান, `P3-13`)। পড়া: `acc_finance_event_inbox`, `acc_finance_event_journal_links`, `acc_journals`, `asset_finance_events` (reader দিয়ে)।

**উপ-যাচাই:**

| উপ-যাচাই | কী মেলায় | ব্যর্থ হলে `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 |

**Business rules**
- RC8.1 থেকে RC8.5-এর যেকোনো একটা ব্যর্থ হলে RC8 `fail`।
- RC8.6 শুধু `detail.warnings`-এ, `fail` নয়।
- P3-13-এর framework-এর একটা check class; একই রাতের job ও dashboard-এ।
- RC8.1-RC8.4 স্বাধীন যাচাই, যদিও DB constraint আছে।

**DB flow**
1. Snapshot transaction।
2. RC8.1 থেকে RC8.4 শুধু Accounting-এর table-এ join।
3. RC8.5 Asset-এর outbox পড়ে (`P4-04`-এর reader দিয়ে, আলাদা read-only method `fetchAcceptedIds`)।
4. কোনো লেখা নেই, শুধু ফল `acc_reconciliation_runs`-এ।

**Task scope**
1. `RC8` check class (ছয়টা উপ-যাচাই)।
2. Source-দিকের reader method `fetchAcceptedIds` (read-only; একই static test)।
3. Framework-এ নিবন্ধন (রাতের job, manual run, period close-এ দেখা, dashboard)।
4. Test।

**Out of scope**
Framework নিজে (`P3-13`); alert (`P5-03`); মেলেনি এমন data ঠিক করা (সেটা মানুষের কাজ, `P4-09`)।

**Contract**
P3-13-এর বিদ্যমান API (`POST /accounting/reconciliation/run`, `GET .../latest`, `GET .../runs`) এ RC8 যোগ হয়। নতুন endpoint নেই।

**Permission**
`accounting.reconcile.run` (`P3-13`-এর মতো)

**Error**
`RPT_RECON_FAILED` (তথ্য হিসেবে, error নয়, report-এর সাথে)

**Audit**
`accounting.recon.run`

**Acceptance**
AC4.12

**Test**
- সুস্থ data-য় `pass`।
- প্রতিটা উপ-যাচাইয়ের জন্য ইচ্ছা করে ভাঙা data-য় `fail` (শুধু test পরিবেশে)।
- RC8.6 শুধু warning দেয়, `fail` নয়।
- `error` পথ (check নিজেই ব্যর্থ)।

**Deliverable**
`RC8` check class, test।

**Sub-tasks**
ছয়টা উপ-যাচাই, source-দিকের reader method, framework-এ নিবন্ধন, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC4.12 সবুজ।
- RC8 `P3-13`-এর dashboard-এ ধূসর (skipped) না থেকে ফল দেখায় (intake চালু থাকলে)।

**Architecture constraints**
- কোনো লেখা নেই (ফল ছাড়া)।
- Asset-এ কিছু লেখা যাবে না; reader শুধু-পড়া।
- নতুন framework নয়; `P3-13`-এর class কাঠামো।

**Open points (Pack-এ অস্পষ্ট)**
- `monitor_approval_stuck_days` registry-তে Phase 5 setting হিসেবে চিহ্নিত, কিন্তু RC8.6 Phase 4-এ এটা পড়ে। Registry (`P1-03`) সব key একসাথে রাখে ধরে নেওয়া; নিশ্চিত করতে হবে key Phase 4 শুরুতেই registry-তে আছে।
- RC8.5-এ "`ack_attempts` শেষ" মানে কোন সীমা, `P4-07`-এর open point দেখুন।
- Intake চালু না থাকলে (flag বন্ধ) RC8 `skipped` হবে কি না Pack বলেনি; `P3-11` শর্ত "RC8 যদি intake চালু থাকে" সেদিকে ইঙ্গিত দেয়।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P4-12 — Phase 4-এর test suite

Project
ERPFlow
Module
Accounting Module
Type
Test
Title
Phase 4-এর test suite ও regression
Branch Name
`feature/accounting-phase4-tests`
View Order
64
Description
**Summary**
Phase 4-এর সব Acceptance Criteria (AC4.1–AC4.13) এক suite-এ automated test দিয়ে cover করতে হবে, সাথে write-off থেকে Post পর্যন্ত একটা full E2E। Asset suite ও পুরো ERPFlow regression সবুজ — তবেই Phase 4 exit।

**Task serial**
`ACC-P4-12 — Phase 4-এর test suite` · Test

**Depends on**
- `ACC-P4-02` থেকে `ACC-P4-11` সব (P4-02, 03, 04, 05, 06, 07, 08, 09, 10, 11)

**Relation: আগের task**
প্রতিটা Phase 4 task নিজের test লিখেছে। এই task সেগুলোকে AC ধরে এক জায়গায় আনে, ফাঁক ধরে, failure-injection helper বানায়, আর Asset ও বিদ্যমান ERPFlow regression দেখে। বিশেষ করে: `P4-02` (replay, state), `P4-03` (flag, isolation, process kill), `P4-04` (reader, static test), `P4-05` (mapping), `P4-06` (Draft, concurrency), `P4-07` (acknowledge, parity), `P4-08` (SoD), `P4-09` (ops), `P4-10` (RC8), `P4-11` (flag matrix)।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P4-13` (runbook, demo, release প্রস্তুতি)।
- **Phase 5 শুরু:** Phase 4 test সবুজ না হলে Phase 5 শুরু নয়।
- `P5-05` (শেষ security ও performance যাচাই) সরাসরি এই task-এর ওপর (`depends on P4-12`)।

**Related tables / schema**
নতুন table নেই।

**AC থেকে test:**

| 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 সবুজ।

**Business rules**
- **Golden suite-এর (Blueprint ১২.২) ৮টা সবকটা এই Phase-এ সবুজ।**
- সব test MySQL-এ (SQLite-এ trigger, generated column, composite FK, `SKIP LOCKED`, `GET_LOCK` আলাদা আচরণ করে)।
- Failure-injection helper (process kill, exception ছোঁড়া) test পরিবেশে।

**Task scope**
1. প্রতিটা test class (উপরের তালিকা)।
2. Failure-injection helper।
3. শেষ থেকে শেষ test।
4. CI-তে MySQL job; test report।
5. AC4.9: Project, Payroll, invoice, payment অক্ষত (সম্পূর্ণ বিদ্যমান ERPFlow suite সবুজ)।

**Out of scope**
নতুন feature; performance test (Phase 5 `P5-05`); UAT (`P5-01`)।

**Deliverable**
সব test class, শেষ থেকে শেষ test, failure-injection helper, test report।

**Sub-tasks**
প্রতিটা class, failure-injection helper, শেষ থেকে শেষ test

**Definition of done**
- AC4.1 থেকে AC4.13 সবুজ।
- Golden suite-এর ৮টা সবুজ।
- শেষ থেকে শেষ test সবুজ।
- Asset-এর বিদ্যমান suite ও পুরো ERPFlow regression সবুজ।
- MySQL CI job ব্যবহার হয়েছে।

**Architecture constraints**
- Concurrency, trigger, `SKIP LOCKED`, advisory lock test SQLite-এ চালানো যাবে না।
- Test-এর জন্য production code-এ দরজা খোলা নয়; failure injection শুধু test hook দিয়ে।

**Open points (Pack-এ অস্পষ্ট)**
- AC4.9-এর জন্য আলাদা কোনো Phase 4 task নির্দিষ্ট নেই; এই task-এর সম্পূর্ণ regression দিয়েই প্রমাণ হয়।
- Golden suite ৮টা, Blueprint ১২.২-এ সংজ্ঞায়িত (এই Pack-এ নেই); Phase 3 পর্যন্ত ৭টা বলা আছে, ৮ম টা Phase 4-এর, তার বিবরণ Blueprint থেকে নিতে হবে।
- Test class নামগুলো "প্রস্তাব"।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Operations (`ops`)
Title
Runbook, demo ও release প্রস্তুতি
Branch Name
`feature/accounting-phase4-runbook`
View Order
65
Description
**Summary**
Phase 4 merge gate: সব company-তে intake flag বন্ধ প্রমাণ, runbook-এ Intake অংশ (আটকে থাকা event, conflict, consumer থেমে গেলে, ack fail), accountant demo ×৪ দৃশ্যে লিখিত মতামত। তারপর Phase 5 (UAT) শুরু।

**Task serial**
`ACC-P4-13 — Runbook, demo ও release প্রস্তুতি` · Ops

**Depends on**
- `ACC-P4-12`

**Relation: আগের task**
`P4-12` প্রমাণ করেছে AC4.1-AC4.13, শেষ থেকে শেষ test ও golden suite সবুজ। এখন সেই প্রমাণ নিয়ে merge gate, runbook ও মানুষের মতামত নেওয়া হবে। `P1-14` (Phase 1 runbook ও merge gate) ও `P2-17` (runbook হালনাগাদ) যে runbook শুরু করেছিল, এই task তাতে Intake অংশ যোগ করে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- **Phase 5 শুরু:** Pack বলে এই task Phase 5 শুরুকে অবরুদ্ধ করে।
- সরাসরি `depends on P4-13`: `P5-01` (UAT pack ও সই) ও `P5-04` (Rollback ও জরুরি বন্ধ পরিকল্পনা)।
- `P5-07` (Training) Intake অংশের runbook ব্যবহার করে; `P5-08` শুরুর আগে সব flag বন্ধ প্রমাণ এই task থেকে।

**Related tables / schema**
নতুন table নেই।

**Business rules**
- Phase 4 exit criteria (Blueprint ৯.১০) পূরণ।
- `development`-এ push মানে production deploy, তাই merge gate কঠোর; সব flag সব company-তে বন্ধ থেকে merge।
- Asset-এর ভবিষ্যৎ event (disposal, acquisition) Phase 4-এ করা হয় না, শুধু কী কী লাগবে তার তালিকা (নথি)।

**Task scope**
1. **Runbook-এ Intake অংশ:** আটকে থাকা event, ব্যর্থ, conflict, consumer থেমে গেলে, কীভাবে নিরাপদে বন্ধ করা (flag বন্ধ, `P4-11`), ফেরত-জানানোর ব্যর্থতা (`P4-07`)।
2. **Accountant demo:** স্বাভাবিক write-off, mapping না থাকা ও ঠিক করে ছাড়া, duplicate event, conflict। মতামত লিখে রাখা।
3. **Asset-এর ভবিষ্যৎ event (disposal, acquisition)-এর জন্য কী কী লাগবে তার তালিকা** (Phase 4-এর বাইরে, শুধু নথি)।
4. **সব flag সব company-তে বন্ধ অবস্থায় release প্রস্তুত, প্রমাণসহ।**
5. **Merge gate** (সই করা checklist): flag Default বন্ধ প্রমাণ; প্রতিটা migration পিছনে সামঞ্জস্যপূর্ণ প্রমাণ; বিদ্যমান suite সবুজ; Project/Payroll/Commissioning-এর code অপরিবর্তিত; Asset-এর diff শুধু `P4-07`-এর অনুমোদিত পরিবর্তন।

**Out of scope**
Production-এ flag চালু করা (`P5-06`, `P5-08`); disposal/acquisition বাস্তবায়ন; UAT।

**Acceptance**
Phase 4 exit criteria (Blueprint ৯.১০)

**Test**
কোড test নেই। যাচাই: সব flag বন্ধ প্রমাণের স্ক্রিপ্ট/query ফল; demo-র চারটা দৃশ্য সম্পন্ন; runbook-এ প্রতিটা Intake পরিস্থিতির ধাপ আছে; merge checklist সই।

**Deliverable**
Runbook-এর Intake অংশ, demo-র মতামত, ভবিষ্যৎ event-এর তালিকা, সই করা merge checklist।

**Sub-tasks**
runbook Intake অংশ, demo ×4 দৃশ্য, ভবিষ্যৎ event তালিকা, flag-বন্ধ প্রমাণ, merge checklist

**Definition of done**
- সই করা merge checklist আছে।
- Accountant-এর লিখিত মতামত আছে।
- সব flag সব company-তে বন্ধ, প্রমাণসহ।
- Phase 4 exit criteria (Blueprint ৯.১০) পূরণ।

**Architecture constraints**
- Merge হবে শুধু flag বন্ধ রেখে।
- Phase 4-এ disposal/acquisition ঢোকানো যাবে না।

**Open points (Pack-এ অস্পষ্ট)**
- Phase 4 exit criteria (Blueprint ৯.১০) এই Pack-এ লেখা নেই; Blueprint থেকে তুলে checklist-এ বসাতে হবে।
- Runbook-এর Intake অংশের ঠিক কাঠামো ও "ফেরত-জানানোর ব্যর্থতা"-র ধাপ `P4-07`-এর ack সীমা (open point) ঠিক হলে চূড়ান্ত হবে।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

<!-- ===== END: ACC-Phase-4-Tasks.md ===== -->


<!-- ===== BEGIN: ACC-Phase-5-Tasks.md ===== -->

# Accounting Module — Phase 5 Tasks (UAT ও Controlled Release)

> এখানকার প্রতিটা task আলাদা করে একজন কাজ করতে পারবে।
> Phase 5-এ নতুন feature নেই—এখানে journal বা report নতুন বানানোর কাজ নয়। কাজ হলো প্রমাণ, মহড়া, নজরদারি আর ধাপে ধাপে চালু করা। শুধু দুটো card-এ code আছে: monitoring (`P5-03`) আর release gate (`P5-06`); `P5-05`-এ test code ও ফাঁক সংশোধন।
> মনে রাখো: `development`-এ merge মানেই production-এ deploy। তাই merge আর "চালু করা" আলাদা কাজ—flag বন্ধ রেখেই merge করতে হবে।
> ক্রম (master sequence অনুযায়ী): UAT → cutover মহড়া → monitoring → rollback → security ও performance → release gate → training → pilot চালু।
> Estimate, Owner, Sprint ইচ্ছা করে খালি রাখা হয়েছে।

## সব Phase 5 task-এ প্রযোজ্য সাধারণ নিয়ম (প্রতিটা task নিজের DoD-তে এগুলো ধরে নেয়)

- Phase 5 শুরু হয় Phase 4 পুরো সবুজ (`ACC-P4-12`) আর release-প্রস্তুত (`ACC-P4-13`, সব flag সব company-তে বন্ধ) হওয়ার পর। `P5-01` ও `P5-04` সরাসরি `P4-13`-এর ওপর; `P5-03`, `P5-05` সরাসরি `P4-03/P4-10`, `P4-12`-এর ওপর।
- **Merge আর চালু করা আলাদা।** `development`-এ merge মানে production deploy; flag বন্ধ রেখেই merge। কোনো company-তে `accounting.enabled` চালু হয় শুধু `P5-06`-এর release gate দিয়ে, `P5-08`-এ।
- **Production-এ কখনো করা যাবে না** (`P5-04`-এ বিস্তারিত): (১) Trigger বা unique constraint-এর migration-এর `down()` চালানো। (২) Accounting-এর কোনো table-এর row সরাসরি SQL-এ বদলানো বা মোছা। (৩) Posted journal ঠিক করতে trigger বন্ধ করা। ভুল ঠিক করার একমাত্র পথ Reversal ও Adjustment।
- Database (যেখানে code আছে): MySQL, InnoDB, `utf8mb4`, prefix `acc_`, `company_id NOT NULL` ও composite index-এ সবার আগে, টাকা `DECIMAL(20,4)`, enum `VARCHAR` + `CHECK`, `version` কলাম ও optimistic lock, composite FK `(company_id, *_id)`, `ON DELETE RESTRICT`। Migration আলাদা, পিছনে সামঞ্জস্যপূর্ণ (আগে যোগ, পরে বদল, শেষে মোছা), `down()` নিরাপদ। Append-only table `Schema::appendOnly('table')` (`ACC-P1-10`)।
- Production-এ কোনো লেখা নেই এমন কাজ (মহড়া, UAT, security test): শুধু **staging**, production-এর মতো কাঠামো। Staging data মুছে আবার শুরু করা যায়।
- একই সময়ে একটার বেশি company চালু নয় (প্রক্রিয়াগত নিয়ম, system আটকায় না, Release manager আটকায়)।
- Blueprint-এর `R5.x`, `AC5.x` নম্বর Pack-এ যেভাবে উল্লেখ আছে ঠিক সেভাবে রাখা হয়েছে; Blueprint নিজে এই task-এর সাথে নেই, তাই এই নম্বরগুলোর সংজ্ঞা open point হিসেবে চিহ্নিত।
- **DoD-র সাধারণ অংশ (code-ভিত্তিক card-এ, `P5-03`, `P5-05`-এর fix, `P5-06`):** Service–Repository architecture (interface সহ), Form Request, API Resource; `company_id` tenant scope + IDOR test; Feature test (happy path, validation, unauthorized/forbidden); audit event `AccountingAudit` দিয়ে; error `AccountingError` enum-এর code দিয়ে; Job হলে `AccountingJob` base (company context); concurrency/constraint test MySQL-এ; বিদ্যমান ERPFlow regression সবুজ; Project/Payroll/Asset/Commissioning-এর code অপরিবর্তিত।
- **Process/ops card-এর DoD (`P5-01`, `02`, `04`, `07`, `08`):** লিখিত deliverable নথি, সই বা লিখিত নিশ্চয়তা, প্রমাণ (screenshot/export/log) সংযুক্ত, নথি প্রকল্পের নথি-ভান্ডারে, QA tool বা release রেকর্ডে লিংক।

**Phase 5 নির্ভরতা-ছবি:**

```
P3-15 + P4-13 ─► P5-01 ─┬─► P5-06 ─────────────────────┐
                        └─► P5-07 ──────────────────────┤
P3-15 + P2-14 + P3-11 ─► P5-02 ─► P5-06                  │
P4-12 + P3-14 ─► P5-05 ─► P5-06   (P1-05, P4-11-ও)       ├─► P5-08  (শেষ card)
P3-13 + P4-03 + P4-10 + P2-15 ─► P5-03 ──────────────────┤
P4-13 ─► P5-04 ──────────────────────────────────────────┘
```

**Build order (master sequence ক্রম 41 থেকে 43):** ক্রম 41: P5-01, P5-02, P5-03, P5-04, P5-05 (একসাথে চলতে পারে) → ক্রম 42: P5-06, P5-07 → ক্রম 43: P5-08।

---

## ACC-P5-01 — UAT pack ও সই

Project
ERPFlow
Module
Accounting Module
Type
Test + Governance (`test` + `gov`)
Title
UAT pack ও সই
Branch Name
কোনো branch নেই (QA tool ও নথি-ভিত্তিক কাজ)
View Order
66
Description
**Summary**
Client-এর accountant আর finance team staging-এ নিজের হাতে সত্যিকারের পরিস্থিতিতে পুরো system যাচাই করবে, তারপর লিখিতভাবে সই করবে। production-এ চালুর আগে "ব্যবহারকারী মেনে নিয়েছে"—এর প্রমাণ থাকতে হবে; Finance owner-ও সেটাই চায়।

**Task serial**
`ACC-P5-01 — UAT pack ও সই` · Test + Governance

**Depends on**
- `ACC-P3-15` (accountant-এর সই করা নমুনা report pack)
- `ACC-P4-13` (Phase 4 release-প্রস্তুত, runbook, demo)
- OD: `OD-17` (কে সই করবে)

**Relation: আগের task**
- `P3-15` দিয়েছে: সই করা নমুনা report pack, যা U08, U10 (report ও export-এর মিল) এর তুলনামূলক ভিত্তি। Pack-এ `P3-15` "অবরুদ্ধ করে: Phase 4 শুরু, P5-01"।
- `P4-13` দিয়েছে: Phase 4 exit criteria পূরণ, সব flag বন্ধ থাকা release, Intake অংশের runbook ও accountant demo-র মতামত (U11-এর ভিত্তি)।
- প্রতিটা আগের Phase-এর test suite (`P1-13`, `P2-16`, `P3-14`, `P4-12`) এখানে স্বয়ংক্রিয় test নয়, মানুষের হাতে দৃশ্য হিসেবে আবার চলে। UAT-এর AC নম্বর (AC1.x থেকে AC4.x) সেই suite-এর সাথে মেলে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P5-06` (Release gate): Pack বলে এই task `P5-06`-কে অবরুদ্ধ করে। এই task-এর সই করা নথিই `gate:uat`-এর ভিত্তি; Finance owner (বা OD-17-এর ভূমিকা) সই করে `POST /accounting/release-gates/gate:uat`-এ প্রমাণের লিংক দেয়।
- `P5-07` (Training ও support): UAT-এর পরিস্থিতি ও ফলের ওপর ভিত্তি করে নির্দেশিকা ও Error বার্তা সহায়িকা লেখা হয়; UAT-এ ধরা পড়া সমস্যাও সেখানে যায়।
- `P5-08` পরোক্ষভাবে (`P5-06` হয়ে)।

**Related tables / schema**
এই Pack-এ **নতুন table নেই**। UAT-এর ফল কোনো DB table-এ নয়, QA tool-এ রাখা হয়। UAT-এর সময় যে table পড়া/লেখা হয় সেগুলো আগের Phase-এর (পুরো Accounting schema)।

**পরিবেশ:** Staging, production-এর মতো কাঠামো। আসল বা মুখোশ-পরানো (masked) data।

**Business rules**
- গুরুতর defect থাকলে সংশোধনের পর **সংশ্লিষ্ট সব পরিস্থিতি আবার চালানো**, আংশিক সই নয়।
- প্রতিটা পরিস্থিতির ধাপ-ধাপ script QA লেখে; accountant ও QA একসাথে চালায়।
- সই করবেন OD-17-এ নির্ধারিত ব্যক্তি।

**UAT পরিস্থিতি (U01 থেকে U14):**

| # | পরিস্থিতি | কভার করে |
| --- | --- | --- |
| 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 |

**DB flow**
UAT নিজে কোনো নতুন DB flow বানায় না। প্রতিটা পরিস্থিতি আগের Phase-এর flow চালায় (Pack Section ৯: Post ৯.২, journal যাত্রা ৯.৩, Event থেকে Draft ৯.৪, Period বন্ধ ৯.৫, Reversal ৯.৬)। প্রমাণ হিসেবে screenshot, export বা (প্রয়োজনে) read-only query ফল জমা হয়।

**Task scope**
1. প্রতিটা পরিস্থিতির (U01-U14) ধাপ-ধাপ script লেখা (QA)।
2. Staging-এ data প্রস্তুত (আসল বা masked)।
3. Accountant ও QA একসাথে চালানো; প্রতিটার ফল (পাস, ব্যর্থ)।
4. প্রমাণ সংগ্রহ (screenshot বা export)।
5. Defect তালিকা, গুরুতর defect সংশোধন ও সংশ্লিষ্ট সব পরিস্থিতি আবার চালানো।
6. OD-17-এর ব্যক্তির সই।

**Out of scope**
নতুন feature বা নতুন table; স্বয়ংক্রিয় test (এগুলো Phase-ভিত্তিক suite-এ আছে); security ও performance (`P5-05`); cutover মহড়া (`P5-02`)।

**Contract**
প্রযোজ্য নয়। (সই করা নথি পরে `P5-06`-এর `POST /accounting/release-gates/{gate}`-এ লিংক হিসেবে যায়।)

**Permission**
প্রযোজ্য নয় (UAT ব্যবহারকারীরা নিজ নিজ role-এ চালায়; U13 permission matrix ইচ্ছা করে ভেঙে দেখে)।

**Audit**
UAT-এর ফল audit event নয়। তবে UAT চালানো কাজগুলো (post, approve ইত্যাদি) স্বাভাবিক audit event লেখে।

**Acceptance**
AC5.1, AC5.3

**Test**
- U01-U14 প্রতিটা চালানো ও ফল রেকর্ড।
- গুরুতর defect সংশোধনের পর সংশ্লিষ্ট পরিস্থিতিগুলো আবার চালানো (আংশিক সই নয়)।

**Deliverable**
UAT script (১৪টা), ফল ও প্রমাণ, defect তালিকা, সই করা নথি (OD-17-এর ব্যক্তি)।

**Sub-tasks**
script ×14, চালানো, defect সংশোধন ও আবার চালানো, সই

**Definition of done**
- ১৪টা পরিস্থিতির সবকটা `পাস`, প্রমাণসহ।
- কোনো গুরুতর defect খোলা নেই।
- OD-17-এ নির্ধারিত ব্যক্তির সই করা নথি আছে।
- সই করা নথির লিংক `P5-06`-এর `gate:uat`-এ দেওয়ার জন্য প্রস্তুত।

**Architecture constraints**
- UAT-এর ফল DB table-এ নয়, QA tool-এ।
- Production data/environment-এ UAT নয়।

**Open points (Pack-এ অস্পষ্ট)**
- "গুরুতর defect"-এর সংজ্ঞা (severity scale) Pack-এ নেই।
- OD-17 (কে সই করবে) বন্ধ হওয়ার আগে এই card `Blocked`; কে সই করবে Pack-এ নেই। `P5-06`-এর gate-ও "Finance owner বা OD-17-এর ভূমিকা" বলে।
- AC5.1 ও AC5.3 দুটোই `P5-01` ও `P5-06` উভয়ের Acceptance-এ আছে; Blueprint-এ এদের সংজ্ঞা না দেখে ভাগ নির্ধারণ করা যায় না।
- UAT-এর staging data-য় "আসল না মুখোশ-পরানো" কোনটা হবে ও মুখোশ কীভাবে হবে Pack-এ নেই।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P5-02 — Cutover মহড়া

Project
ERPFlow
Module
Accounting Module
Type
Operations (`ops`)
Title
Cutover মহড়া
Branch Name
কোনো branch নেই (staging-এ ops কাজ, code বদলায় না)
View Order
67
Description
**Summary**
Production-এ cutover যা হবে, ঠিক সেইভাবে আগে staging-এ মহড়া নিতে হবে—কত সময় লাগে, কোথায় ঠেকে, কোথায় ফাঁক—সব মাপা। Opening balance একবার Post হয়ে গেলে আর বদলানো যায় না; তাই প্রথমবারেই নির্ভুল হওয়া জরুরি।

**Task serial**
`ACC-P5-02 — Cutover মহড়া` · Ops

**Depends on**
- `ACC-P3-15` (সই করা report pack, TB/BS মেলানোর পদ্ধতি)
- `ACC-P2-14` (Opening journal-এর Post)
- `ACC-P3-11` (Period বন্ধ ও আবার খোলা)
- OD: `OD-9` (cutover তারিখ ও কোন কাগজ থেকে মেলানো)

**Relation: আগের task**
- `P2-14` দিয়েছে: Opening journal সাধারণ Submit/approve/post পথে Post হওয়া, **Submit ও Post দুই জায়গাতেই** `journal_date`-এর সাথে `opening_cutover_date` মেলানো (`JRN_DATE_INVALID`), Post-এর পর lock-করা setting `SET_LOCKED`, আর `S10` সবুজ। Pack: `P2-14` "অবরুদ্ধ করে: P2-16, P5-02"।
- `P1-11` (পরোক্ষে `P2-14`-এর মাধ্যমে) দিয়েছে Opening Balance গ্রিড ও Draft: যা এই মহড়ার ধাপ 3।
- `P3-11` দিয়েছে Period বন্ধ/খোলা; Pack: "অবরুদ্ধ করে: P3-12, P5-02"। মহড়ায় period যাচাই হয় (cutover period)।
- `P3-15` দিয়েছে Trial Balance, Balance Sheet ও সই করা হিসাব-মেলানোর পদ্ধতি, যার সাথে পুরোনো system মেলানো হয়।
- `P1-05` দিয়েছে Setup Checklist (ধাপ 1)।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P5-06`: Pack বলে এই task `P5-06`-কে অবরুদ্ধ করে। `gate:cutover` এই মহড়ার রিপোর্ট ও production cutover-এর প্রস্তুতি দিয়ে সই হয়।
- `P5-08` ধাপ 1 ("Production-এ Setup Checklist সম্পূর্ণ, opening balance Post (মহড়ার checklist মেনে)") এই task-এর **Production cutover-এর ধাপ-ধাপ checklist** সরাসরি ব্যবহার করে।
- `P5-04` রিহার্সালে একই staging ব্যবহার করতে পারে (আলাদা কাজ)।

**Related tables / schema**
কোনো নতুন table নেই। পড়া/লেখা (staging-এ): `acc_settings`, `acc_accounts`, `acc_periods`, `acc_journals` (opening), `acc_journal_lines`, `acc_setup_status`, report service।

**Business rules**
- **Production-এ কোনো লেখা নেই।**
- Staging-এর data মুছে আবার শুরু করে **অন্তত দুইবার** চালানো (দ্বিতীয়বার দ্রুত ও নির্ভুল হওয়া উচিত)।
- Opening Balance ভুল হলে Post-এর পরে আর বদলানো যায় না (শুধু Adjustment), তাই মহড়াই একমাত্র সুযোগ।
- Cutover তারিখ ও কোন কাগজ থেকে মেলানো হবে তা OD-9-এর সিদ্ধান্ত।

**মহড়ার ধাপ ও মাপ:**

| ধাপ | কী | মাপ |
| --- | --- | --- |
| 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) |  |

**DB flow**
নতুন নেই; ধাপগুলো আগের Phase-এর flow: Setup Checklist (`P1-05`), CoA (`P1-06`/`P1-07`), Opening Draft (`P1-11`), Submit/approve (`P2-08`), Post (`P2-12`, `P2-14`, Section ৯.২), TB/BS (`P3-04`, `P3-06`)।

**Task scope**
1. ধাপ 1 থেকে 7 স্টেজিংয়ে প্রথম মহড়া, প্রতিটার সময় ও ঠেকার তালিকা।
2. Staging data মুছে দ্বিতীয় মহড়া (দ্রুত, নির্ভুল)।
3. অমিলের কারণ ও সমাধানের রেকর্ড।
4. **Production cutover-এর ধাপ-ধাপ checklist** (মহড়ার শিক্ষা থেকে)।
5. মহড়ার রিপোর্ট।

**Out of scope**
Production-এ কোনো লেখা; নতুন feature; `P5-08`-এর আসল production cutover।

**Contract / Permission / Error**
প্রযোজ্য নয় (নতুন API বা error নেই)। মহড়ায় আগের Phase-এর permission ও error কোড (যেমন `JRN_DATE_INVALID`, `SET_LOCKED`, `SET_INCOMPLETE`) স্বাভাবিকভাবে আসতে পারে এবং রেকর্ড করতে হবে।

**Audit**
আগের Phase-এর স্বাভাবিক audit event।

**Acceptance**
AC5.2

**Test**
- দুইবার সম্পূর্ণ মহড়া; দ্বিতীয়বারে অমিল শূন্য বা সব ব্যাখ্যাত।
- চূড়ান্ত তুলনা: TB পুরোনো system-এর সাথে account ধরে মেলে; Balance Sheet সমীকরণ (Asset = Liability + Equity) মেলে।

**Deliverable**
মহড়ার রিপোর্ট (ধাপ ধরে সময়, অমিল ও তার কারণ, চূড়ান্ত তুলনা), Production cutover-এর ধাপ-ধাপ checklist।

**Sub-tasks**
ধাপ ১ থেকে ৭, দ্বিতীয় মহড়া, রিপোর্ট, production checklist

**Definition of done**
- অন্তত দুইবার মহড়া সম্পন্ন, দ্বিতীয়বার দ্রুত ও নির্ভুল।
- সব অমিলের কারণ জানা ও সমাধান রেকর্ড করা।
- মহড়ার রিপোর্ট ও production cutover checklist জমা, Finance owner দেখেছেন।
- Production-এ কোনো লেখা হয়নি (প্রমাণ)।

**Architecture constraints**
- Production-এ লেখা নিষিদ্ধ।
- Opening Post-এর পর ভুল ঠিক করার পথ Adjustment মাত্র; মহড়া এই ঝুঁকি আগেই ধরার জন্য।

**Open points (Pack-এ অস্পষ্ট)**
- OD-9 বন্ধ হওয়ার আগে এই card `Blocked`; cutover তারিখ ও মেলানোর কাগজ (কোন পুরোনো system-এর রিপোর্ট) Pack-এ নেই।
- "কত account", "কত সময়" কোনো লক্ষ্যমাত্রা Pack-এ দেওয়া নেই; মাপ শুধু রেকর্ড করার জন্য।
- OD-23 (Opening-এ Revenue/Expense থাকতে পারবে কি) `P3-15`-এ বন্ধ হয়ে থাকার কথা; বছরের মাঝে cutover হলে চালু-মাসের P&L ফোলার ঝুঁকি মহড়ায় লক্ষ্য রাখতে হবে।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P5-03 — Monitoring ও alert

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + job) + Ops
Title
Monitoring ও alert — health check, alert state ও বিজ্ঞপ্তি
Branch Name
`feature/accounting-monitoring-alerts`
View Order
68
Description
**Summary**
কিছু ভাঙলে মানুষ জানার আগেই system যেন জানায়—Head of Finance আর tech on-call পাবে। কিন্তু একই alert বারবার ঝড় তুলবে না; সমস্যা দ্রুত ধরা পড়বে, বিজ্ঞপ্তি বিরক্তিকর হবে না।

**Task serial**
`ACC-P5-03 — Monitoring ও alert` · Feature + Ops

**Depends on**
- `ACC-P3-13` (Reconciliation framework, `acc_reconciliation_runs`, `ReconciliationFailed` event)
- `ACC-P4-03` (Consumer job, `acc_intake_heartbeats`)
- `ACC-P4-10` (RC8)
- `ACC-P2-15` (Queue ও Register, আটকে থাকা approval)

**Relation: আগের task**
- `P3-13` দিয়েছে: RC1-RC9 ফল (`acc_reconciliation_runs`) এবং নিয়ম "কোনো check ব্যর্থ হলে `ReconciliationFailed` event, যা P5-03 alert-এ বাঁধা"। `recon_failed` alert এর ভিত্তি।
- `P4-03` দিয়েছে `acc_intake_heartbeats` (`last_started_at`, `last_finished_at`, `last_error`, গণনা) -> `consumer_lag`।
- `P4-10` দিয়েছে RC8 (event থেকে ledger পর্যন্ত মিল), যা `recon_failed`-এর অংশ।
- `P2-15` দিয়েছে আটকে থাকা approval-এর তালিকা (`monitor_approval_stuck_days`) ও অপর্যালোচিত override-এর তালিকা -> `approval_stuck`, `override_unreviewed`।
- অন্যান্য উৎস যা এই task পড়ে (আগের Phase-এর table/event): inbox (`P4-02`, `P4-07`-এর `ack_attempts`), `acc_approval_requests`/`acc_approval_actions` (`P2-05`), `acc_report_exports` (`P3-10`), `activity_logs` (`P1-12`, post ব্যর্থতা `P2-12`, `accounting.period.locked_post_attempt` `P3-12`)।
- `P1-03` registry-র `monitor_*` setting।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P5-08`: Pack বলে এই task `P5-08`-কে অবরুদ্ধ করে। পিলট চালুর পর্যবেক্ষণ-সময়ে "রোজ RC1 থেকে RC9-এর সারাংশ দেখা, alert-এর সাড়া" এই task-এর `GET /accounting/health` ও বিজ্ঞপ্তির ওপর দাঁড়ায়।
- `P5-04` (Rollback): alert পেলে প্রথম পদক্ষেপ flag বন্ধ; runbook alert-কে rollback স্তর ১-এর ট্রিগার হিসেবে ধরে।
- `P5-07`: Support playbook-এ "কোন alert-এ কী করবে"।

**Related tables / 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)`।

**Settings (`P1-03` registry, সবই Phase 5, default):** `monitor_held_event_age_hours` = 24, `monitor_held_event_count` = 10, `monitor_approval_stuck_days` = 3, `monitor_consumer_lag_minutes` = 30, `monitor_closed_period_post_attempts` = 5।

**Alert তালিকা:**

| 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` |

**Business rules**
- Alert state-machine: `ok` <-> `firing`। শর্ত সত্য আর আগে `ok` ছিল -> `firing`, `first_fired_at` বসে, বিজ্ঞপ্তি পাঠানো, `last_notified_at`। আগে থেকেই `firing` -> আবার বিজ্ঞপ্তি শুধু **নির্ধারিত বিরতির পর** (প্রস্তাব ৬ ঘণ্টা)। শর্ত মিথ্যা হলে `ok` (আর একটা সমাধানের বিজ্ঞপ্তি)।
- **ঝড় নেই:** একই শর্তে নির্ধারিত বিরতির আগে দ্বিতীয় বিজ্ঞপ্তি নয়।
- কে পাবে: Head of Finance আর tech on-call।
- বিজ্ঞপ্তির মাধ্যম: ERPFlow-এর বিদ্যমান notification বা ইমেইল ব্যবস্থা (`P0-02`-এর Convention Sheet-এ যা পাওয়া গেছে)। না থাকলে সবচেয়ে সরল: নির্ধারিত ইমেইল তালিকা।
- Check শুধু পড়ে (কোনো business data বদলায় না); শুধু `acc_alert_state` লেখে।

**DB flow**
1. `accounting:health-check` command প্রতি পাঁচ মিনিটে (scheduler)।
2. প্রতিটা **চালু company**-র জন্য company context-এ একটা job (`AccountingJob`)।
3. প্রতিটা alert-এর জন্য শর্ত দেখা (উৎস table পড়া)।
4. `acc_alert_state` upsert (`uq_acc_alert (company_id, alert_key)`):
   - শর্ত সত্য + আগে `ok` -> `firing`, `first_fired_at`, `last_fired_at`, `detail`, বিজ্ঞপ্তি, `last_notified_at`।
   - আগে থেকেই `firing` -> `last_fired_at` হালনাগাদ; বিজ্ঞপ্তি শুধু বিরতির পর।
   - শর্ত মিথ্যা -> `ok` আর একটা সমাধানের বিজ্ঞপ্তি।
5. `GET /accounting/health` সব alert-এর বর্তমান অবস্থা পড়ে।

**Task scope**
1. Migration `acc_alert_state`।
2. Command `accounting:health-check` ও scheduler (৫ মিনিট)।
3. প্রতিটা alert-এর আলাদা checker (দশটা)।
4. State-machine ও ঝড়-নিয়ন্ত্রণ।
5. বিজ্ঞপ্তির ব্যবস্থা (ইমেইল/notification)।
6. Health API ও UI।
7. Test।

**Out of scope**
Alert-এর কারণ ঠিক করা (ops screen `P4-09`, runbook); নতুন reconciliation check; SMS/Slack integration (Pack-এ নেই)।

**Contract**
`GET /accounting/health` (সব alert-এর বর্তমান অবস্থা, UI-র জন্য)

**UI**
Health পাতা: দশটা alert-এর অবস্থা (সবুজ/লাল), কখন প্রথম/সর্বশেষ জ্বলেছে, `detail`, সংশ্লিষ্ট পাতার লিংক (যেমন `held_events` -> ops screen)।

**Permission**
Pack-এ নির্দিষ্ট নেই (open point নিচে)।

**Error**
প্রযোজ্য নয় (background); `ACC_FORBIDDEN`/`ACC_MODULE_DISABLED` health API-তে স্বাভাবিক।

**Audit**
Pack-এ নির্দিষ্ট নেই; alert অবস্থার বদল `acc_alert_state`-এই রেকর্ড (open point নিচে)।

**Acceptance**
AC5.4

**Test**
- প্রতিটা alert ইচ্ছা করে ঘটিয়ে `firing` ও বিজ্ঞপ্তি।
- ঝড় নেই: একই শর্তে নির্ধারিত বিরতির আগে আবার বিজ্ঞপ্তি নয়।
- শর্ত মিটলে `ok` আর সমাধানের বিজ্ঞপ্তি।
- Company context: এক company-র alert অন্য company-কে প্রভাবিত করে না।
- Flag/`accounting.enabled` বন্ধ company-তে job চলে না।

**Deliverable**
Migration, command, job, health API ও UI, বিজ্ঞপ্তির ব্যবস্থা।

**Sub-tasks**
migration, প্রতিটা alert-এর checker, state-machine ও ঝড়-নিয়ন্ত্রণ, বিজ্ঞপ্তি, API ও UI, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC5.4 সবুজ।
- সব দশটা alert staging-এ ঘটিয়ে প্রমাণিত।
- Head of Finance ও tech on-call বিজ্ঞপ্তি পেয়েছেন (staging প্রমাণ)।

**Architecture constraints**
- Checker শুধু পড়ে; business table বদলায় না।
- `AccountingJob` base (company context) ব্যবহার।
- Threshold সব `monitor_*` setting থেকে, কোডে বাঁধা নয় (যেখানে setting আছে)।

**Open points (Pack-এ অস্পষ্ট)**
- বিজ্ঞপ্তির পুনরাবৃত্তির বিরতি ("প্রস্তাব ৬ ঘণ্টা") কোনো setting-এর key নয়; কনফিগারযোগ্য হবে কি কোডের ধ্রুবক, নির্ধারিত নেই।
- `override_unreviewed`-এর সীমা "২৪ ঘণ্টা" ও `post_failures`-এর "পরপর তিনটা গুরুতর" setting-এ নেই। `acc_alert_state`-এ severity column নেই; গুরুতরতা `detail`-এ যাবে কি না ঠিক করতে হবে।
- "Post ব্যর্থ হওয়ার audit event"-এর সুনির্দিষ্ট event নাম Pack-এর audit registry-তে নেই (`P2-12` বলে "ব্যর্থ চেষ্টাও" audit হয়)। নাম নির্ধারণ করে checker বাঁধতে হবে।
- `ack_failing`-এর "ack_attempts শেষ" সীমা `P4-07`-এর open point।
- `GET /accounting/health`-এর permission slug নির্দিষ্ট নেই (প্রস্তাব: বিদ্যমান slug থেকে, নতুন slug না বানিয়ে)।
- বিজ্ঞপ্তির মাধ্যম `P0-02`-এর উত্তরের ওপর নির্ভর করে; Pack এখানে ফলাফল জানায় না।
- `export_failures` ভিত্তি `acc_report_exports.status = 'failed'`; কতক্ষণ আগের পর্যন্ত গোনা হবে (সময়-জানালা) বলা নেই। একই প্রশ্ন `failed_events`/`recon_failed`-এর ক্ষেত্রে ("সমাধান হয়েছে" কখন ধরা হবে)।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Operations (`ops`)
Title
Rollback ও জরুরি বন্ধ পরিকল্পনা
Branch Name
`feature/accounting-phase5-rollback-plan` (শুধু নথি/runbook; code বদলায় না)
View Order
69
Description
**Summary**
কিছু ভুল হলে কী করতে হবে—আগে থেকেই লেখা থাকবে, staging-এ মহড়াও দেওয়া থাকবে। জরুরি সময়ে আন্দাজে নয়, নির্ধারিত ধাপে ফিরতে হবে; data হারানো যাবে না। Release manager-এর কাজ এটাই।

**Task serial**
`ACC-P5-04 — Rollback ও জরুরি বন্ধ পরিকল্পনা` · Ops

**Depends on**
- `ACC-P4-13` (Phase 4 release-প্রস্তুতি ও runbook)

**Relation: আগের task**
- `P4-13` দিয়েছে: সব flag বন্ধ থাকা release, runbook-এর Intake অংশ (নিরাপদে বন্ধ করা) ও সই করা merge checklist। এই task সেই runbook-এর ওপর পূর্ণ rollback পরিকল্পনা বসায়।
- `P1-14` runbook-এর প্রথম খসড়ায় "trigger-এর সতর্কতা, `down()` চালানো নিষেধ" রেখেছিল; `P1-10` সেই trigger বানিয়েছে ও `down()` সতর্কতা লিখেছে।
- `P1-01`/`P4-11` দিয়েছে flag ও বন্ধ করার (অনুমোদন ছাড়া, কারণসহ) পথ; এটাই স্তর ১।
- প্রতিটা আগের Phase-এর migration (expand-only, পিছনে সামঞ্জস্যপূর্ণ নিয়ম `P0-03`) এই task-এর "migration সামঞ্জস্য তালিকা"-র উপাদান।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P5-08`: Pack বলে এই task `P5-08`-কে অবরুদ্ধ করে। পিলট চালুর আগে rollback মহড়া সম্পন্ন থাকতে হবে।
- `P5-07` (Support playbook): "কখন flag বন্ধ করবে" স্তর ১ থেকে আসে।
- `P5-03`: alert পেলে প্রথম পদক্ষেপ স্তর ১।

**Related tables / schema**
কোনো নতুন table নেই। তালিকাভুক্ত করার জন্য প্রতিটা Accounting migration (Pack-এর table index + expand migration + trigger)।

**Business rules**
**তিন স্তরের rollback:**

| স্তর | কখন | কী করে | কী হারায় না |
| --- | --- | --- | --- |
| ১. 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।**

**DB flow**
নতুন নেই। মহড়ায় migration-এর আচরণ যাচাই (নিচে)।

**Task scope**
1. স্তর ১ থেকে ৩ লেখা: কে সিদ্ধান্ত নেয়, কোন ধাপ, কত সময়, যোগাযোগ।
2. "করা যাবে না" তালিকা (ওপরের তিনটা)।
3. **Migration সামঞ্জস্য তালিকা:** প্রতিটা Accounting migration-এর জন্য একটা সারি: কী যোগ করে, পুরোনো code-এর সাথে চলে কি না, `down()` নিরাপদ কি না। Pack-এর সব table (`P1-03` থেকে `P5-06`, নিচের open point-এ সংখ্যা) আর expand migration (যেমন `acc_journals.posted_hash` `P1-10`, generated column `source_active` `P1-09`, `reversal_active` `P2-13`, `acc_periods.balance_version` `P3-02`, index migration `P2-15`, inbox-এর `acknowledged_at/ack_attempts/ack_error` `P4-07`, Asset-এর `effective_date/schema_version` যদি OD-21 (ক)) ও trigger (`P1-10`, `P3-11`-এর `acc_period_events`) সব এতে।
4. **মহড়া (staging):** (১) N-1 code deploy, (২) N-এর migration চালানো, (৩) আবার N-1 code-এ ফেরা, কিছু ভাঙেনি দেখা, (৪) Flag বন্ধ করে চালু করা, data অক্ষত।
5. মহড়ার রেকর্ড।

**Out of scope**
Disaster recovery / backup-restore (Pack-এ নেই); production-এ rollback চালানো; code পরিবর্তন।

**Contract / Permission / Error**
প্রযোজ্য নয়। (Flag বন্ধ করার API ও permission `P4-11`/`P5-06`-এর: `accounting.enable.manage`।)

**Audit**
মহড়ায় flag বন্ধ/চালু `accounting.flag.disabled`, `accounting.flag.enabled` audit লেখে (বিদ্যমান)।

**Acceptance**
AC5.5, AC5.6

**Test**
- N-1 code + N migration: কিছু ভাঙে না (পুরোনো code নতুন schema-র সাথে চলে)।
- Flag বন্ধ -> চালু: data (Draft, submitted, posted, event, audit) অক্ষত।
- Module বন্ধ (`modules_statuses.json`) staging-এ: route/UI বন্ধ, table ও data থেকে যায়।

**Deliverable**
Rollback পরিকল্পনা, migration সামঞ্জস্য তালিকা, মহড়ার রেকর্ড।

**Sub-tasks**
স্তর ১ থেকে ৩ লেখা, "করা যাবে না" তালিকা, migration তালিকা, মহড়া

**Definition of done**
- তিন স্তর ও "করা যাবে না" তালিকা লিখিত, tech lead ও Release manager দেখেছেন।
- Migration সামঞ্জস্য তালিকায় প্রতিটা Accounting migration আছে।
- Staging মহড়া সম্পন্ন, রেকর্ড জমা।
- `P1-14` runbook-এ এই পরিকল্পনার লিংক যোগ।

**Architecture constraints**
- Migration কখনো rollback হয় না; তাই প্রতিটা migration পিছনে সামঞ্জস্যপূর্ণ।
- Production-এ ভুল ঠিক করার একমাত্র পথ Reversal ও Adjustment।

**Open points (Pack-এ অস্পষ্ট)**
- **Table সংখ্যার অসঙ্গতি:** Pack-এ P5-04 বলে "এই Pack-এর ৩০টা table"; Section ১০.৩ বলে মোট ৩১টা (index-এর ২৮ + তিনটা নতুন: `acc_approval_override_reviews`, `acc_intake_heartbeats`, `acc_alert_state`) এবং বলে "৩০টা মানে এই প্রায় সংখ্যা"। কিন্তু Section ২.১-এর index-এ বাস্তবে ২৬টা `acc_` table আছে, তিনটা যোগ করলে ২৯। তালিকা বানানোর সময় বাস্তব migration গণনাই সত্যের উৎস; সংখ্যার মিল আলাদা করে মেলাতে হবে।
- "CI/CD-র সুযোগ অনুযায়ী আগের SHA-তে ফেরা" কীভাবে হবে (`P0-03`-এর ফল) Pack-এ বিস্তারিত নেই।
- Release manager কে ও সিদ্ধান্তের কর্তৃপক্ষ কে (স্তর ২/৩ ডাকার) Pack-এ নেই।
- স্তর ৩ ("Module বন্ধ") production-এ কে কীভাবে `modules_statuses.json` বদলাবে (deploy লাগে কি না) বলা নেই।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Test
Title
শেষ security ও performance যাচাই
Branch Name
`feature/accounting-phase5-security-perf`
View Order
70
Description
**Summary**
চালুর আগে শেষবার সব endpoint-এর দরজা যাচাই: company isolation, permission, input, upload, export, audit tamper, rate limit—আর গতিও। কোনো গুরুতর ফাঁক নিয়ে production-এ যাওয়া যাবে না; security-minded tech lead এটা চালাবে।

**Task serial**
`ACC-P5-05 — শেষ security ও performance যাচাই` · Test

**Depends on**
- `ACC-P4-12` (Phase 4-এর সম্পূর্ণ test suite, সব route নিবন্ধিত)
- `ACC-P3-14` (Performance seeder, প্রস্তাবিত সীমা ও Phase 3 suite)

**Relation: আগের task**
- `P4-12` দিয়েছে: সব Phase-এর API এখন পূর্ণ (intake, mapping, ops, flags সহ), তাই route-ঘোরা test সব endpoint ধরতে পারে। এবং company isolation (AC4.6), permission matrix ইত্যাদির Phase-ভিত্তিক test।
- `P3-14` দিয়েছে: performance seeder command (৫ বছর, ৫০০ account, ১০ লক্ষ journal line, ৬০টা period), প্রস্তাবিত সীমা (TB/BS/Cash Flow কয়েক সেকেন্ড, GL ১ লক্ষ row account এক পৃষ্ঠা ~১ সেকেন্ড, export ১ লক্ষ row queue-তে কয়েক মিনিট) ও `EXPLAIN` অভ্যাস। এখানে "P3-14-এর সীমা আবার" বলতে সেই সীমা।
- `P1-10`/`P3-13`/`P3-11` দিয়েছে audit ও history table-এর DB trigger (tamper test-এর বিষয়)। `P1-09` দিয়েছে attachment table ও file নিয়ম, `P3-10` export ও মেয়াদী লিংক।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P5-06`: Pack বলে এই task `P5-06`-কে অবরুদ্ধ করে। `gate:security` এই task-এর রিপোর্ট ও ফাঁক সংশোধনের প্রমাণে সই হয়।
- `P5-08` পরোক্ষে (`P5-06` হয়ে)।

**Related tables / schema**
নতুন table নেই। সব `accounting` route ও পুরো schema পরীক্ষা হয়। ফাঁক সংশোধন code-এ হলে সংশ্লিষ্ট card-এর migration/code-এ ছোট fix।

**Business rules**
- নতুন route যোগ হলে isolation test নিজে থেকে তাকে ধরে (route তালিকা থেকে স্বয়ংক্রিয়)।
- গুরুতর ফাঁক থাকলে সংশোধনের পর সংশ্লিষ্ট যাচাই আবার।
- Performance: staging-এ বাস্তব আকারের data, কখনো production নয়।

**যাচাই তালিকা:**

| যাচাই | কীভাবে |
| --- | --- |
| 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 পর্যালোচনা |

**DB flow**
নতুন নেই। Performance অংশে `EXPLAIN`, slow query log ও index পর্যালোচনা। Tamper test সরাসরি SQL (শুধু staging)।

**Task scope**
1. Route-ঘোরা isolation test (স্বয়ংক্রিয়, নতুন route-ও ধরে)।
2. Permission matrix (সব role × সব endpoint)।
3. Mass assignment ও input যাচাই, SQL injection।
4. Attachment upload যাচাই।
5. Export download যাচাই (isolation, মেয়াদ)।
6. Audit/history tamper।
7. Rate limit।
8. Performance আবার (P3-14-এর সীমা), slow query log, index পর্যালোচনা।
9. ফাঁক সংশোধন (আলাদা fix commit সংশ্লিষ্ট branch-এ) ও আবার যাচাই।
10. রিপোর্ট।

**Out of scope**
নতুন feature; external penetration test (Pack-এ নেই); load/stress test production-এ।

**Contract / Permission**
প্রযোজ্য নয়। (Test সব বিদ্যমান permission মেলায়।)

**Error**
প্রত্যাশিত: `ACC_COMPANY_FORBIDDEN`, `ACC_FORBIDDEN`, `ACC_APPEND_ONLY`/`JRN_IMMUTABLE` (tamper), `JRN_*` validation; rate limit-এ 429 (বিদ্যমান ERPFlow throttling অনুযায়ী)।

**Audit**
ফাঁক খোঁজার চেষ্টায় `accounting.guard.denied` লেখা হচ্ছে কি না তা-ও দেখা।

**Acceptance**
কোনো গুরুতর ফাঁক নেই, performance সীমার মধ্যে। (Pack-এ AC নম্বর নেই।)

**Test**
উপরের নয়টা যাচাই-ই test। Isolation ও permission স্বয়ংক্রিয় (CI-তে চালানো যায়); performance আলাদা (CI-তে নয়, staging-এ)।

**Deliverable**
Security ও performance রিপোর্ট, ফাঁক সংশোধন।

**Sub-tasks**
route-ঘোরা isolation test, permission matrix, input যাচাই, upload, export, tamper, rate limit, performance

**Definition of done**
- সাধারণ DoD পূরণ (fix থাকলে)।
- সব যাচাই পাস; গুরুতর ফাঁক শূন্য।
- Performance প্রস্তাবিত সীমার মধ্যে বা সীমা ছাড়ালে tech lead-এর লিখিত সিদ্ধান্ত।
- রিপোর্ট `P5-06`-এর `gate:security`-এ প্রমাণ হিসেবে লিংক করার জন্য প্রস্তুত।

**Architecture constraints**
- Isolation test route তালিকা থেকে স্বয়ংক্রিয়, হাতে লেখা তালিকা নয়।
- Performance শুধু staging-এ, production-এ নয়।
- ফাঁক সংশোধনে নতুন architecture নয়; নিয়ম আগের Phase-এর মতো।

**Open points (Pack-এ অস্পষ্ট)**
- "গুরুতর ফাঁক"-এর মানদণ্ড ও পাস/ফেল সীমা Pack-এ নেই।
- Performance সীমার সংখ্যাগুলো `P3-14`-এ "প্রস্তাব" ছিল, client ও tech lead নিশ্চিত করবে; নিশ্চিত হয়েছে কি না এখানে আবার দেখতে হবে।
- Virus scan "থাকলে", ERPFlow-এ আছে কি না `P0-02`-এর উত্তরের ওপর।
- Rate limit-এর সুনির্দিষ্ট সীমা Pack-এ নেই (বিদ্যমান ERPFlow throttle ধরা হয়েছে)।
- Acceptance-এ কোনো AC5.x নম্বর দেওয়া নেই।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service + api + ui)
Title
Release gate ও চালুর রেকর্ড
Branch Name
`feature/accounting-release-gate`
View Order
71
Description
**Summary**
কোনো company-তে `accounting.enabled` চালু হবে শুধু তখনই, যখন তিন গেট, Setup Checklist আর দুজনের সম্মতি—সব লিখিতভাবে সবুজ। প্রতিটা চালু-বন্ধের পূর্ণ রেকর্ড থাকবে; অপরিকল্পিত বা একজন একাই চালু করা যাবে না।

**Task serial**
`ACC-P5-06 — Release gate ও চালুর রেকর্ড` · Feature

**Depends on**
- `ACC-P5-01` (UAT সই -> `gate:uat`)
- `ACC-P5-02` (Cutover মহড়া -> `gate:cutover`)
- `ACC-P5-05` (Security ও performance -> `gate:security`)
- `ACC-P1-05` (Setup Checklist, `canEnable()`)
- `ACC-P4-11` (তিন স্তরের flag, `flag:` applier, `ChecklistService`)

**Relation: আগের task**
- `P5-01` দিয়েছে সই করা UAT নথি = `gate:uat`-এর প্রমাণ। `P5-02` দিয়েছে মহড়ার রিপোর্ট ও production cutover checklist = `gate:cutover`। `P5-05` দিয়েছে security ও performance রিপোর্ট = `gate:security`।
- `P1-05` দিয়েছে `ChecklistService::evaluate`, `canEnable()` (`S01`-`S10` সবুজ, নইলে `SET_INCOMPLETE`) এবং `POST /accounting/enable` endpoint-এর কথা "পূর্ণ রূপ P5-06-এ" (`P1-05`-এর নিজের contract-এ লেখা)। Pack: `P1-05` "অবরুদ্ধ করে: accounting.enabled চালু করার কাজ (P5-06)"।
- `P4-11` দিয়েছে `AccountingFeatureService`-এর চূড়ান্ত নিয়ম, flag চালু-বন্ধের API `POST /accounting/flags/{flag}`, "প্রথমবারে দুজন, পরে একজন; বন্ধ সঙ্গে সঙ্গে, কারণ বাধ্যতামূলক" নিয়ম ও `ChangeApplier`-এর `flag:` prefix। এই task `flag:accounting.enabled` key-তে একই পথ ব্যবহার করে।
- `P1-04` দিয়েছে change request (Maker–Checker), `P1-10` দিয়েছে `Schema::appendOnly` helper (`acc_enablement_log`-এর জন্য, `P1-10`-এর তালিকায় এটার নাম আছে), `P1-01` দিয়েছে audit event নাম নিবন্ধন (`accounting.flag.enabled/disabled`; "flag বদলানোর card `P5-06` আর `P4-11`-এ লেখা হবে")।
- `P1-02` দিয়েছে permission `accounting.enable.manage`, `accounting.settings.approve`।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P5-08`: Pack বলে এই task `P5-08`-কে অবরুদ্ধ করে। `P5-08` ধাপ 2 ("`accounting.enabled` চালু (দুজন)") "P5-06-এর সব শর্ত" মেনে এই task-এর release পাতা ও API দিয়ে চলে; `acc_enablement_log` চালুর রেকর্ড ও `gate_snapshot` সেখানে জমা হয়।
- `P5-04` ও `P5-03` ও `P5-07` (রেকর্ডে "কে কখন বন্ধ করল" support playbook ব্যবহার করে)।

**Related tables / schema**

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

| 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)`।

পড়া/লেখা: `Setting` (flag, `P1-01`), `acc_setup_status` (`P1-05`), `acc_setting_change_requests` (`P1-04`), `activity_logs`।

**Business rules**
- **তিনটা গেট:** `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` (কোনটা বাকি তা বলে)।
- **বন্ধ করা:** এক ব্যক্তি, কারণ বাধ্যতামূলক, সঙ্গে সঙ্গে, `off` row।
- R5.1 থেকে R5.5, R5.8, R5.10 (Blueprint)।
- একই সময়ে একটার বেশি company চালু করা নয় (প্রক্রিয়াগত; system আটকায় না, Release manager আটকায়)।
- Maker নিজে অনুমোদন করতে পারে না (`JRN_SOD_VIOLATION`)।

**DB flow**
**চালু (`accounting.enabled`):**
1. transaction।
2. সব শর্ত পড়া: `acc_enablement_log`-এ প্রতিটা গেটের সর্বশেষ row `gate_passed` কি না; `ChecklistService::evaluate`/`canEnable()`; change request অনুমোদিত কি না ও Maker ≠ Checker।
3. `Setting` লেখা (`accounting.enabled = true`)।
4. `acc_enablement_log`-এ row (`flag = 'accounting.enabled'`, `action = 'on'`, `requested_by`, `approved_by`, `reason`, `gate_snapshot`)।
5. Audit (`accounting.enablement.changed`, `accounting.flag.enabled`)।
6. commit।

**গেট সই/বাতিল:** `acc_enablement_log`-এ row (`flag = 'gate:*'`, `action = 'gate_passed'` বা `gate_revoked`, `reason`, প্রমাণের লিংক `reason`/`gate_snapshot`-এ)।

**বন্ধ:** `Setting` লেখা (`false`), `off` row, audit `accounting.flag.disabled`।

**Task scope**
1. Migration `acc_enablement_log` + `Schema::appendOnly`।
2. গেট সই/বাতিল service ও API `POST /accounting/release-gates/{gate}`।
3. চালুর শর্ত যাচাই (তিন গেট + Checklist + দুজন)।
4. Change request সংযোগ: `flag:accounting.enabled` applier (`P1-04`-এর `ChangeApplier` রেজিস্ট্রিতে; `P4-11`-এর `flag:` applier-এর সাথে একই পথ)।
5. বন্ধ করার পথ ও `off` row।
6. Release পাতা (UI)।
7. Test।

**Out of scope**
Intake flag-এর চালুর শর্ত (`P4-11`); production-এ আসলে চালু করা (`P5-08`); UAT/মহড়া/security কাজ নিজে (`P5-01`/`02`/`05`)।

**Contract**
- `POST /accounting/release-gates/{gate}` (গেট সই; বাতিলের জন্য এই route-এর বাতিল রূপ, নিচের open point)
- `POST /accounting/enable` (`P1-05`-এ উল্লিখিত, এখানে পূর্ণ)
- `POST /accounting/flags/{flag}` (`P4-11`-এর, `accounting.enabled`-এর জন্যও ব্যবহৃত হতে পারে)
- Release পাতার জন্য `GET` (গেট ও log)

**UI**
Release পাতা: তিন গেট (অবস্থা, কে কখন সই করেছে, প্রমাণ-লিংক), Setup Checklist (`S01`-`S10`), চালুর বাটন (শর্ত পূরণে সক্রিয়), ইতিহাস (`acc_enablement_log`)।

**Permission**
`accounting.enable.manage` (গেট সই, চালু, বন্ধ), `accounting.settings.approve` (দ্বিতীয় জনের অনুমোদন)

**Error**
`SET_INCOMPLETE`, `ACC_FORBIDDEN`, `JRN_SOD_VIOLATION`

**Audit**
`accounting.enablement.changed`, `accounting.flag.*`

**Acceptance**
AC5.1, AC5.3

**Test**
- কোনো গেট ছাড়া চালু (নিষেধ, `SET_INCOMPLETE`)।
- গেট বাতিলের পর চালু (নিষেধ)।
- নিজে অনুমোদন (নিষেধ, `JRN_SOD_VIOLATION`)।
- Checklist লাল -> চালু নিষেধ।
- বন্ধ করলে data অক্ষত আর `off` row।
- `acc_enablement_log`-এ `UPDATE`/`DELETE` ব্যর্থ (append-only)।
- `gate_snapshot` চালুর মুহূর্তের Checklist ও গেটের অবস্থা ঠিক ধরে।

**Deliverable**
Migration, service, API, UI।

**Sub-tasks**
migration ও append-only, গেট সই, চালুর শর্ত, change request সংযোগ, UI, test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC5.1, AC5.3 (এই task-সংক্রান্ত অংশ) সবুজ।
- কোনো গেট/Checklist/দ্বিতীয় জন ছাড়া `accounting.enabled` চালু করা যায় না (test প্রমাণ)।
- Flag Default বন্ধ।

**Architecture constraints**
- `acc_enablement_log` append-only, কখনো বদলানো/মোছা নয়।
- আলাদা approval table নয়; `P1-04`-এর change request পুনর্ব্যবহার।
- বন্ধ করা কখনো অনুমোদনের পেছনে আটকে থাকে না।

**Open points (Pack-এ অস্পষ্ট)**
- গেট বাতিলের (`gate_revoked`) জন্য নির্দিষ্ট endpoint/verb Pack-এ নেই (শুধু "বাতিল সম্ভব")।
- `POST /accounting/enable` (`P1-05`) ও `POST /accounting/flags/{flag}` (`P4-11`) একই কাজের জন্য দুটো endpoint; একটা অন্যটার alias কি না ঠিক করতে হবে।
- `acc_enablement_log` intake flag-ও ধরে (`flag` column-এ `event_intake.*`), কিন্তু `P4-11` আগেই flag চালু-বন্ধ করে। `P4-11`-এর flag service-কে এখানে log লেখাতে হবে কি না নির্ধারিত নেই।
- "দুজন"-এর দ্বিতীয় জন কে (Finance owner? `accounting.settings.approve`-ওয়ালা অন্য কেউ?) Pack-এ সুনির্দিষ্ট নেই; change request-এর নিয়ম (অনুরোধকারী ≠ অনুমোদনকারী) অনুযায়ী ধরা হয়েছে।
- R5.1 থেকে R5.10 ও AC5.1, AC5.3-এর সংজ্ঞা Blueprint-এ; `P5-01` ও এই task দুটোই এগুলো Acceptance-এ লেখে।
- প্রমাণের লিংক কোথায় সংরক্ষিত (`reason`, `gate_snapshot` নাকি আলাদা column) table-এ আলাদা column নেই।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P5-07 — Training ও support

Project
ERPFlow
Module
Accounting Module
Type
Operations (`ops`)
Title
Training ও support
Branch Name
কোনো branch নেই (নথি ও অধিবেশন-ভিত্তিক কাজ)
View Order
72
Description
**Summary**
Accountant, approver, admin, auditor, finance—সব role-এর মানুষ নিজ নিজ কাজের নির্দেশিকা আর ছোট training পাবে; সমস্যায় কাকে জানাতে হবে সেটাও জানবে। চালুর পর ভুল ব্যবহার বা অজানা error-এ আটকে পড়বে না।

**Task serial**
`ACC-P5-07 — Training ও support` · Ops

**Depends on**
- `ACC-P5-01` (UAT পরিস্থিতি ও ফল)

**Relation: আগের task**
- `P5-01` দিয়েছে: ১৪টা UAT পরিস্থিতির script ও ফল, যা নির্দেশিকার ধাপ-ধাপ উদাহরণ ও screenshot-এর উৎস, এবং UAT-এ ধরা পড়া সমস্যা ও ব্যবহারকারীর প্রশ্ন (নির্দেশিকা/FAQ-এর উপকরণ)।
- আগের Phase-গুলো দিয়েছে বিষয়বস্তু: `P1-*` (Settings, Checklist, CoA), `P2-*` (journal, approval, delegation, override, reversal, queue), `P3-*` (report, period বন্ধ/খোলা, reconciliation), `P4-*` (mapping, intake ops screen)। এবং Pack Section ২.৩-এর error code-তালিকা (Error সহায়িকার উৎস) ও Section ২.৫ audit event।
- `P1-12` দিয়েছে `correlation_id` যা Support playbook-এ "কী তথ্য চাইবে"।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P5-08`: Pack বলে এই task `P5-08`-কে অবরুদ্ধ করে। ব্যবহারকারী training না পেলে pilot company চালু নয়; `P5-08` ধাপ 3 (পর্যবেক্ষণ) Support playbook অনুযায়ী চলে।
- `P5-04`/`P5-03` এর সাথে মিলিয়ে: Support playbook-এ "কখন flag বন্ধ করবে" (rollback স্তর ১) ও alert দেখে কী করবে।

**Related tables / schema**
কোনো নতুন table নেই।

**Business rules**
- নির্দেশিকা ভূমিকা ধরে; Error সহায়িকা সবার জন্য।
- Training ছোট অধিবেশন, ভূমিকা ধরে, **উপস্থিতির রেকর্ড** রাখা।
- Support playbook "কখন flag বন্ধ করবে" স্পষ্ট বলবে।

**উপকরণ:**

| উপকরণ | কার জন্য | বিষয় |
| --- | --- | --- |
| 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 বন্ধ করবে |

**DB flow**
প্রযোজ্য নয়।

**Task scope**
1. পাঁচটা role-ভিত্তিক নির্দেশিকা।
2. Error বার্তার সহায়িকা: Section ২.৩-এর প্রতিটা code (এবং Section ১০.৩-এর নতুন code: `COA_CODE_INVALID`, `SET_STALE_REQUEST`, `ACC_APPEND_ONLY`, `EVT_STATE_INVALID`, `EVT_PROCESSING_ERROR`, `RPT_BASIS_UNSUPPORTED`) সহজ ভাষায়, "কী করবেন" সহ।
3. Support playbook।
4. ভূমিকা ধরে training অধিবেশন; উপস্থিতির রেকর্ড।

**Out of scope**
নতুন feature বা UI পরিবর্তন; টিকেটিং system; (এগুলো Pack-এ নেই)।

**Contract / Permission / Error / Audit**
প্রযোজ্য নয়। (Error সহায়িকা বিদ্যমান code-গুলো ব্যাখ্যা করে, নতুন code বানায় না।)

**Acceptance**
AC5.9

**Test**
কোড test নেই। যাচাই: প্রতিটা Section ২.৩ code সহায়িকায় আছে; প্রতিটা role-এর কমপক্ষে একজন training-এ উপস্থিত; Support playbook একটা কাল্পনিক ঘটনায় (যেমন `held_events` alert) মহড়া দিয়ে দেখা।

**Deliverable**
সব নির্দেশিকা, Error সহায়িকা, Support playbook, অধিবেশন ও উপস্থিতি।

**Sub-tasks**
প্রতিটা নির্দেশিকা, Error সহায়িকা, playbook, অধিবেশন

**Definition of done**
- সাতটা উপকরণ সব জমা।
- Training অধিবেশন হয়েছে, উপস্থিতির রেকর্ড আছে।
- Support playbook-এ `correlation_id` চাওয়া ও flag বন্ধের ধাপ আছে।
- Client-এর Finance owner উপকরণ দেখেছেন।

**Architecture constraints**
- নির্দেশিকা বর্তমান UI/permission মেনে; নতুন কিছু কথা দেওয়া নয়।

**Open points (Pack-এ অস্পষ্ট)**
- Training-এর ভাষা, মাধ্যম (live/recorded), উপস্থিতির কাঠামো নির্দিষ্ট নেই।
- Support-এর স্তর ও সময়সীমা (SLA) Pack-এ নেই।
- "Finance" role (Auditor-এর পাশে আলাদা) default role তালিকায় (Accountant, Finance Manager, Head of Finance, Auditor, Admin) নেই; এখানে কোন role-কে বোঝানো হয়েছে স্পষ্ট করতে হবে।
- AC5.9-এর সংজ্ঞা Blueprint-এ।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Operations (`ops`)
Title
Pilot company চালু ও ধাপে ধাপে বিস্তার
Branch Name
কোনো branch নেই (release রেকর্ড-ভিত্তিক ops; deploy হয় ইতিমধ্যে merge হওয়া SHA থেকে)
View Order
73
Description
**Summary**
প্রথমে একটা pilot company-তে সতর্কভাবে চালু, তারপর পর্যবেক্ষণ। তারপর আলাদা সিদ্ধান্তে Asset intake; স্থিতিশীল হলে বাকি company। সমস্যা হলে একটা company-তেই সীমাবদ্ধ থাকবে—Release manager এটাই চায়।

**Task serial**
`ACC-P5-08 — Pilot company চালু ও ধাপে ধাপে বিস্তার` · Ops

**Depends on**
- `ACC-P5-03` (monitoring ও alert)
- `ACC-P5-04` (rollback পরিকল্পনা ও মহড়া)
- `ACC-P5-06` (release gate ও চালুর রেকর্ড)
- `ACC-P5-07` (training ও support)

**Relation: আগের task**
- `P5-03` দিয়েছে `GET /accounting/health`, দশটা alert ও বিজ্ঞপ্তি: পর্যবেক্ষণ-সময়ে "alert-এর সাড়া" এর ভিত্তি।
- `P5-04` দিয়েছে তিন স্তরের rollback ও "করা যাবে না" তালিকা: সমস্যা হলে কী করতে হবে, ও মহড়া দেওয়া।
- `P5-06` দিয়েছে release পাতা, তিন গেট, `acc_enablement_log` ও দুজনের সম্মতিতে `accounting.enabled` চালুর পথ। `P5-01`/`02`/`05` (গেটের উৎস) এর মাধ্যমে পরোক্ষে এসেছে: `gate:cutover`-এর ভিত্তি `P5-02`-এর production cutover checklist যা ধাপ 1 ব্যবহার করে।
- `P5-07` দিয়েছে ব্যবহারকারী training ও Support playbook।
- `P4-11` দিয়েছে intake flag-এর প্রথমবারের নিয়ম (ধাপ 5), `P4-10` দিয়েছে RC8 (ধাপ 6), `P3-13` দিয়েছে RC1-RC9 ও dashboard।

**Relation: পরের task (কে অপেক্ষা করছে)**
কেউ নেই, এটা শেষ card (Pack: "অবরুদ্ধ করে: —")। এই task-এর শেষে Phase 5 ও পুরো Accounting rollout (Phase 0-5) সম্পন্ন। পরের Phase (Phase 6 ইত্যাদি) আলাদা scope।

**Related tables / schema**
নতুন table নেই। পড়া/লেখা (production): পুরো Accounting schema স্বাভাবিক ব্যবহারে; `acc_enablement_log` (চালু-বন্ধ রেকর্ড), `acc_alert_state`, `acc_reconciliation_runs`, `acc_finance_event_inbox`।

**Business rules**
- **R5.4, R5.9:** Asset intake কখনো accounting চালুর সাথে একসাথে নয়। যেকোনো RC ব্যর্থ হলে পরের company-র চালু থামে।
- একই সময়ে একটার বেশি company চালু নয় (প্রক্রিয়াগত)।
- `accounting.enabled` চালু (ধাপ 2) এবং intake চালু (ধাপ 5) আলাদা সিদ্ধান্ত।
- গুরুতর defect থাকলে ধাপ 4-এর সই হয় না।
- সমস্যায় প্রথম পদক্ষেপ flag বন্ধ (rollback স্তর ১)।

**ধাপ:**

| ধাপ | কী | শর্ত |
| --- | --- | --- |
| 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 অনুমোদিত, ধাপ 4 সম্পন্ন |
| 6 | Asset intake-এর জন্য আলাদা পর্যবেক্ষণ-সময় | RC8 সবুজ |
| 7 | পরের company: ধাপ 1 থেকে আবার | আগেরটা স্থিতিশীল |

**DB flow**
নতুন নেই। ধাপ 1: Setup Checklist (`P1-05`), CoA, Opening Draft -> Submit -> approve -> Post (`P1-11`, `P2-08`, `P2-12`, `P2-14`)। ধাপ 2: Section ৯-র flag-চালু (`P5-06`)। ধাপ 3: RC1-RC9 (`P3-13`)। ধাপ 5: `P4-11` + consumer (`P4-03`) + Section ৯.৪।

**Task scope**
1. ধাপ 1: production cutover (`P5-02`-এর checklist অনুযায়ী), `gate:cutover` পাস থাকা নিশ্চিত।
2. ধাপ 2: `accounting.enabled` চালু (Maker–Checker, `P5-06`)।
3. ধাপ 3: পর্যবেক্ষণ-সময় (দৈনিক RC সারাংশ, alert-এর সাড়া, সীমিত ব্যবহার)।
4. ধাপ 4: Finance owner-এর সই।
5. ধাপ 5-6: Asset intake চালু ও আলাদা পর্যবেক্ষণ।
6. ধাপ 7: পরের company-র সময়সূচি ও চক্র।
7. চালুর রিপোর্ট।

**Out of scope**
নতুন feature বা code; Disposal/acquisition event intake; Phase 6 কাজ।

**Contract / Permission**
ব্যবহৃত API: `P5-06`, `P4-11`, `P3-13`, `P5-03`-এর বিদ্যমান। Permission বিদ্যমান (`accounting.enable.manage`, `accounting.settings.approve`)।

**Error**
প্রত্যাশিত: `SET_INCOMPLETE`, `ACC_FORBIDDEN`, `JRN_SOD_VIOLATION` (চালুর ধাপে ভুল হলে)।

**Audit**
`accounting.enablement.changed`, `accounting.flag.enabled`, `accounting.flag.disabled`, `accounting.recon.run`

**Acceptance**
AC5.7, AC5.8, AC5.10

**Test**
কোড test নেই। যাচাই: প্রতিটা ধাপের শর্ত পূরণের প্রমাণ (log, dashboard screenshot, সই); পর্যবেক্ষণ-সময়ে কোনো RC `fail` নয় (দৈনিক সারাংশ); alert এলে সাড়ার রেকর্ড; ধাপ 5 কখনো ধাপ 2-এর সাথে একই দিনে/একই সিদ্ধান্তে নয়।

**Deliverable**
চালুর রিপোর্ট, পর্যবেক্ষণের দৈনিক সারাংশ, সই, বাকি company-র সময়সূচি।

**Sub-tasks**
production cutover, চালু, পর্যবেক্ষণ, সই, Asset intake ধাপ, পরের company

**Definition of done**
- Pilot company-তে `accounting.enabled` চালু, `acc_enablement_log`-এ রেকর্ড।
- পর্যবেক্ষণ-সময়ে কোনো RC ব্যর্থ হয়নি, দৈনিক সারাংশ জমা।
- Finance owner-এর লিখিত সই আছে।
- Asset intake আলাদা সিদ্ধান্তে ও আলাদা পর্যবেক্ষণে চালু, RC8 সবুজ।
- পরের company-র সময়সূচি আছে, আগেরটা স্থিতিশীল প্রমাণসহ।

**Architecture constraints**
- Asset intake accounting চালুর সাথে একসাথে নয়।
- কোনো RC ব্যর্থ হলে পরের company-র চালু থামে।
- Production-এ সরাসরি SQL-এ data বদল নয়; ভুল ঠিক করার একমাত্র পথ Reversal ও Adjustment।

**Open points (Pack-এ অস্পষ্ট)**
- পর্যবেক্ষণ-সময়ের দৈর্ঘ্য "client-এর সাথে ঠিক" বলা আছে, মান নেই। ধাপ 3 ও 6 দুটোর জন্যই আলাদা সময় ঠিক করতে হবে।
- Pilot company কোনটা, কার সিদ্ধান্তে নির্বাচিত, Pack-এ নেই।
- "স্থিতিশীল" ও "গুরুতর defect"-এর সংজ্ঞা নেই।
- AC5.7, AC5.8, AC5.10 ও R5.4, R5.9-এর সংজ্ঞা Blueprint-এ।
- ধাপ 5-এ OD-18 "অনুমোদিত" যাচাই হয় নথি দেখে (system যাচাই করে না, `P4-11`)।
- Rollback-এর সিদ্ধান্ত কে নেয় (পর্যবেক্ষণ-সময়ে সমস্যা হলে) `P5-04`-এর open point।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

<!-- ===== END: ACC-Phase-5-Tasks.md ===== -->


<!-- ===== BEGIN: ACC-Phase-6-Tasks.md ===== -->

# Accounting Module — Phase 6 Tasks (Journal Management ও Analytical Dimensions)

> এখানকার প্রতিটা task আলাদা করে একজন কাজ করতে পারবে।
> Phase 6 শেষ হলে journal-এর ধরন, নম্বর series, template, bulk import, সংযুক্তি, comment আর analytical dimension — সব একটা নিয়ন্ত্রিত কাঠামোয় চলে আসে। দুটো capability flag (`journal_mgmt.enabled`, `dimensions.enabled`) default-এ বন্ধ; বন্ধ থাকলে আচরণ Phase 5 শেষের মতোই (table থাকে, কিন্তু কেউ পড়ে না, নতুন ত্রুটি আসে না)।
> ক্রম (Pack-এর "Build ক্রমের সারি" অনুযায়ী): type registry → type আচরণ → number series → voucher cutover → document date → dimension framework → dimension rule → dimension value/mirror → template → import → সংযুক্তি/comment/search → reclassification → report filter → UI → enablement।
> Estimate, Owner, Sprint ইচ্ছা করে ফাঁকা — Discovery বা design শেষ হলে ভর্তি হবে।
> নোট: এই file-এ P6-01 থেকে P6-15 সব card আছে। Phase 7 (P7-01..P7-16), Phase 9–16-এর card আর Enterprise Registry / Master Build Sequence / WBS Mapping (§২৩–২৫) আলাদা tab-এ, তাই এখানে নেই।

## সব Phase 6 task-এ প্রযোজ্য সাধারণ নিয়ম (প্রতিটা task নিজের DoD-তে এগুলো ধরে নেয়)

- Database: MySQL, InnoDB, `utf8mb4`। Table prefix `acc_`। Phase 1-এর সাধারণ নিয়ম (PK ধরন `ACC-P0-02`-এর Convention Sheet অনুযায়ী, `company_id NOT NULL`, টাকা `DECIMAL(20,4)`, `version INT UNSIGNED` দিয়ে optimistic check, `ON DELETE RESTRICT`, লেজারের table-এ `CASCADE` নয়) Phase 6-তেও সমান খাটে।
- **Composite FK নিয়ম বহাল:** `account_id`, `journal_id`, `value_id` ইত্যাদি রাখা যেকোনো table-এ FK `(company_id, x_id)` আকারে; প্রতিটা নতুন parent table-এ `uq (company_id, id)` থাকে। Phase 6-এর সব নতুন table এই নিয়মে।
- **সব migration expand-only** (আগে যোগ, পরে বদল, শেষে মোছা; প্রয়োগ হওয়া migration সম্পাদনা নয়)। বড় table-এ column বা FK বদলাতে online schema change tool।
- **Journal তৈরির একমাত্র পথ** `JournalService::createDraft`, সবসময় Draft-এ। **`acc_journals.status` শুধু `PostingService` (`P2-12`) লেখে।** Template, import, reclassification, copy/clone সব এই পথ ধরে Draft বানায়, কখনো auto-post নয়।
- **Post-এর lock ক্রম (সবসময় এই ক্রমে):** journal → approval request → account (`FOR SHARE`, id ক্রমে) → period (`FOR SHARE`) → number series (`FOR SHARE`) → number series counter (`FOR UPDATE`) → শেষে dimension value (`FOR SHARE`, id ক্রমে)। কোনো Phase 6 card এই ক্রম ভাঙতে পারে না।
- **Capability flag দুটো:** `journal_mgmt.enabled`, `dimensions.enabled`। দুটোই default বন্ধ, `Setting` model-এ (group `accounting`, সংরক্ষিত key `flag:journal_mgmt.enabled` ও `flag:dimensions.enabled`), `AccountingFeatureService` দিয়ে পড়া। দুই flag একে অপরের ওপর নির্ভর করে না। Flag বন্ধ থাকলে table থাকে কিন্তু কেউ পড়ে না, নতুন ত্রুটি আসে না, আচরণ Phase 5 শেষের মতো (R6.38)।
- **Maker-Checker:** সংবেদনশীল বদল `P1-04`-এর change request দিয়ে। `ChangeApplier` prefix: `jtype:` (journal type, `P6-02`), `jtemplate:` (template, `P6-06`), `series:` (number series, `P6-03`)। অনুরোধকারী নিজে অনুমোদন করতে পারে না।
- **Write API:** `Idempotency-Key` header নেয়। Error আকার সব জায়গায় `{ "code", "message", "field", "details", "correlation_id" }` (`AccountingError`)।
- **Queue ও job:** 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`।
- **Posted journal পরিবর্তনের সীমা:** comment, tag, সংযুক্তি যোগ হতে পারে, কিন্তু `posted_hash`-এর অংশ নয়; ledger-এর row কখনো বদলায় না। ভুল ঠিক করার একমাত্র পথ Reversal ও Adjustment।
- **DoD-র সাধারণ অংশ:** Service–Repository architecture (interface সহ), Form Request, API Resource; `company_id` tenant scope + IDOR test (অন্য company-র id অনুমান করলে 404); Feature test (happy path, validation, unauthorized/forbidden); audit event `AccountingAudit` দিয়ে; error `AccountingError` enum-এর code দিয়ে; **দুই flag বন্ধ রেখে merge নিরাপদ**; বিদ্যমান ERPFlow ও Phase 1-5 regression সবুজ; Project/Payroll/Asset/Commissioning-এর code অপরিবর্তিত।

**Phase 6 Registry যোগ (Pack-এর "Registry যোগ" অংশের সারাংশ, প্রতিটা card নিজের অংশ আবার বলেছে):**

- নতুন table ৩১টা: 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`, `_versions`, `_lines`, `_line_dimensions`, `_shares`, `_uses` (`P6-06`); Import (3) `acc_journal_import_batches`, `_groups`, `_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 index, `ck_acc_journals_type` সরানো), `acc_accounts` (`default_dimension_rule_id` ও composite FK)।
- নতুন permission ২০টা key: `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 family: `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_*`-এ যোগ: `JRN_DOCUMENT_DATE_REQUIRED`, `JRN_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>`।
- নতুন RC sub-check: `RC1d`, `RC1m`, `RC1n` (P3-13 framework গ্রহণ করে, নতুন RC নম্বর নয়)।
- Audit event-এর পূর্ণ তালিকা প্রতিটা card-এর **Audit** অংশে (Pack-এর registry-র সাথে মিলিয়ে)।

**Phase-স্তরের open point (একাধিক card প্রভাবিত):**

- Phase 6 OD-গুলো (OD-24a, OD-24b, OD-25a, OD-25b, OD-25c, OD-26a, OD-26b, OD-26c, OD-27a, OD-27b, OD-28a, OD-28b, OD-28c, OD-28d, OD-29a, OD-29b, OD-29c) এবং AC6.x, G6.x, R6.x, WF-A..WF-G নম্বরের সংজ্ঞা এই document-এ নেই (Blueprint / Phase 6 Master Plan-এ)। প্রতিটা card শুধু নম্বর ও কার্ডে লেখা baseline বর্ণনা ধরে চলে। Dev কাজ শুরুর আগে OD-র লিখিত সিদ্ধান্ত দেখে নেবে।
- "Entry gate" ও "Master Plan §৬" শব্দ source-এ আছে, কিন্তু সংজ্ঞা এই document-এ নেই। `P6-01` ও `P6-08` "Entry gate-এ মূল কোডে নিশ্চিত করা"-র কথা বলেছে; ধরে নেওয়া হয়েছে Phase 6 শুরুর আগে repo-র বর্তমান কোড দেখে নিশ্চিত করার ধাপ (সংজ্ঞা নিশ্চিত নয়)।
- `P6-02`, `P6-05`, `P6-07`, `P6-12` journal-এর `origin` (`manual`, `import` ...) নামে একটা পার্থক্যসূচক মান ধরেছে, কিন্তু `P1-09`-এর `acc_journals` schema-তে `origin` column নেই (আছে `journal_type`, `source_type`, `source_id`, `source_event`)। `origin`-এর উৎস (column না service-স্তরের পতাকা) source-এ স্পষ্ট নয়।
- `P6-07` ও `P6-12` "`source_active` unique (P2-03)" বলেছে; আসলে `source_active` generated column ও `uq_acc_journals_source_active` `P1-09`-এ সংজ্ঞায়িত (`P2-03` শুধু validator)।
- `P6-15` checklist ধাপ `S11`, `S12` নাম ব্যবহার করেছে, কিন্তু `P1-05`-এ `S11_cashflow_mapping` আগে থেকেই আছে। নতুন ধাপের নম্বর সংঘাত করে।

**Phase 6 নির্ভরতা-ছবি (card-এর "নির্ভর করে" ধরে; বাইরের ইনপুট বাঁ দিকে):**

```
P2-03,P1-10,P1-04,P1-08 ─► P6-01 ─┬─► P6-02 ─┬─► P6-05 ───────────────────────────┐
                                  │          ├─► P6-08 ───────────────────────────┤
                                  │          ├─► P6-06 (P6-09 ও P6-10-ও লাগে) ────┤
                                  │          └─► P6-07 (P6-10-ও লাগে) ────────────┤
                                  └─► P6-03 ─► P6-04 (P2-11,P2-12,P1-05 লাগে) ────┤
P1-10,P3-01,P2-03 ─► P6-09 ─┬─► P6-10 (P2-12 লাগে) ─┬─► P6-12 (P2-14 লাগে) ───────┤
                            │                       ├─► P6-06 / P6-07 (উপরে)       ├─► P6-14 ─► P6-15 ─► Phase 7 Entry gate
                            ├─► P6-11 (P1-12 লাগে) ──────────────────────────────┤
                            └─► P6-13 (P6-10, P3-01..P3-06, P3-13 লাগে) ──────────┘
Build ক্রম: 01, 02, 03, 04, 05, 09, 10, 11, 06, 07, 08, 12, 13, 14, 15
```

---

## ACC-P6-01 — Journal type registry schema ও migration

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema)
Title
Journal type registry schema ও migration — Backend
Branch Name
`feature/accounting-journal-type-registry`
View Order
74
Description
**Summary**
Journal-এর ধরন আর কোডে বেঁধে `CHECK` constraint-এ রাখতে হবে না — প্রতি company-র নিজস্ব `acc_journal_types` registry থাকবে। তাতে পরের Phase-এ accrual, depreciation ইত্যাদি নতুন ধরন migration ছাড়াই চালু করা যাবে, আর আগের journal-গুলোর আচরণ একই থাকবে।

**Task serial**
`ACC-P6-01 — Journal type registry schema ও migration` · Feature · Phase 6, Build ক্রম 1

**Depends on**
- `ACC-P2-03` (Journal validator: SaveValidator / SubmitValidator, Post-মোডে আবার)
- `ACC-P1-10` (DB guard trigger: `journal_type` অপরিবর্তনীয়, `Schema::appendOnly()`)
- `ACC-P1-04` (change request / Maker-Checker, পরে `P6-02` এর `jtype:` prefix-এর ভিত্তি)
- `ACC-P1-08` (Period কাঠামো; source এটাকে নির্ভরতায় রেখেছে, এই card-এ সরাসরি ব্যবহার source-এ লেখা নেই)

**Relation: আগের task**
- `P1-09` `acc_journals` বানিয়েছে `journal_type VARCHAR(20)` ও `ck_acc_journals_type` সহ (মান: `manual`, `opening`, `reversal`, `adjustment`, `event_generated`)। এই task সেই CHECK সরিয়ে composite FK বসায়, তাই বর্তমান 5টা মান হুবহু seed-এ থাকতে হবে।
- `P1-10` trigger আগে থেকেই `journal_type` অপরিবর্তনীয় রাখে (sub-task 5-এ যাচাই)।
- `P2-03` validator এখন journal ধরন নিয়ে কিছু যাচাই করে না; `P6-02` সেখানে hook বসাবে। এই task শুধু তথ্যের কাঠামো দেয়, আচরণ নয়।
- `P1-04` change request দেয়, যা `P6-02` type-এর সংবেদনশীল বদলে ব্যবহার করবে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P6-02` (type আচরণ service ও API): এই task-এর দুটো table পড়ে `JournalTypeRegistry` বানায়।
- `P6-03` (number series): `acc_journal_types.default_series_code`-এর FK `fk_acc_jt_series` সেখানে যোগ হয়; series ও assignment `journal_type_code` ধরে এই table-এ FK রাখে। `P6-03`-এ default series `JV` seed হয় আর `default_series_code = 'JV'` যুক্ত হয়।
- `P6-05` (document date ও backdate): `requires_document_date`, `backdate_days_limit`, `future_days_limit` এই table-এর column।
- `P6-06`, `P6-07` (template, import): `journal_type_code` FK এই table-এ; `import` type `P6-15`-এ সক্রিয় হলে `P6-07` ব্যবহার করে।
- `P6-15` (enablement): চালুর শর্ত "সব company-তে 16 type seed, orphan 0"; `import` type সক্রিয় করে।
- সব ভবিষ্যৎ Phase: Phase 7 (`fx_revaluation`), 8 (`tax_settlement`), 10 (`bank`), 11 (`depreciation`), 12 (`accrual`, `recurring`, `closing`), 15 (`intercompany`, `elimination`, `consolidation`)-এর নতুন journal ধরন এই registry-র seed-করা `phase` row সক্রিয় করে। (Phase 7, 10-15 এই document-এ নেই, অন্য tab-এ)

**Related tables / schema**

**`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` বাদ।

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

| 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 |

**Business rules**
- R6.1 থেকে R6.5 ও R6.38। Seed মান উপরের তালিকা, কোডে বাঁধা নয় (config ফাইল)।
- Seed-এ সব type-এর `default_series_code = 'JV'`, যাতে সব type আগের মতো একই নম্বর গাঁথে (`P2-11`-এর `JV-{CO}-{FY}-{SEQ}` রূপ অপরিবর্তিত)।
- System type সবসময় `is_active = 1`; custom type-এর code `x_` দিয়ে শুরু।
- `import` type `journal_mgmt.enabled` চালুর সময় `P6-15` সক্রিয় করে; তার আগে `is_active = 0`।
- System type-এর `opening` ও `manual` entry-র মান বর্তমান `P2-03` আচরণ থেকে নিশ্চিত করার পর অনুসরণ হবে (source: "Entry gate-এ নিশ্চিত")।
- পুরোনো কোড বদলায় না: flag বন্ধ থাকলে table থাকে কিন্তু কেউ পড়ে না। Migration কোনো ledger lock নেয় না।
- **Open point:** `P1-09`-এ `acc_journals.journal_type` `VARCHAR(20)`, কিন্তু এই registry-র `code` `VARCHAR(30)` এবং composite FK-র জন্য দুই column একই ধরন ও collation লাগে। Source বলেছে "ENUM হলে VARCHAR(30)-এ রূপান্তর"; `VARCHAR(20)` হলে 30-এ বাড়ানোর ধাপ আলাদা করে লেখা নেই, তাই ধাপ (4)-এ 20 থেকে 30 বাড়ানোও ধরতে হবে (নিশ্চিত করা দরকার)।
- **Open point:** OD-24b (phase type কখন seed) ও OD-24a (custom type)-এর লিখিত সিদ্ধান্ত এই document-এ নেই। Card baseline ধরে লেখা: 16টা type একসাথে seed, phase type নিষ্ক্রিয়।
- **Open point:** `acc_journal_types`-এ `kind`, `attachment_rule` `ENUM`, কিন্তু Pack-এর সাধারণ convention "MySQL enum নয়, `VARCHAR(32)` + `CHECK`"। Source Phase 6-এর table-এ ENUM ব্যবহার করেছে; convention-এর সাথে এই পার্থক্য ঠিক করা বা মেনে নেওয়া tech lead-এর সিদ্ধান্ত (এটা Phase 6 জুড়ে সব `ENUM` column-এ খাটে)।

**DB flow**
Migration, এক ক্রমে (প্রতিটা ধাপ আলাদা পুনঃচালুযোগ্য):
1. দুটো table তৈরি (`acc_journal_types`, `acc_journal_type_account_ranges`)।
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 দিয়ে।

**Task scope**
1. `acc_journal_types` ও `acc_journal_type_account_ranges` migration ও model।
2. `JournalTypeSeeder` (`seedCompany`, `seedAll`) ও seed তালিকার config ফাইল (কোডে বাঁধা নয়)।
3. `AccountingSetupService` hook: নতুন company-তে `seedCompany`।
4. `acc_journals`-এ composite FK, index, `ck_acc_journals_type` অপসারণ (online পদ্ধতি সহ)।
5. `P1-10` trigger যাচাই: `journal_type` অপরিবর্তনীয় আগে থেকেই আছে কি না।
6. Artisan command `accounting:journal-types:seed {--company=}`।
7. `JournalTypeMigrationTest`।

**Out of scope**
Type-এর আচরণ (validator hook, manual entry আটকানো, account range যাচাই) ও Admin API: `P6-02`। Number series ও `fk_acc_jt_series` FK: `P6-03`। `document_date` column: `P6-05`। `import` type সক্রিয় করা: `P6-15`। কোনো HTTP endpoint নেই।

**Contract (API)**
`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

**Definition of done**
- সাধারণ DoD (উপরে) পূরণ।
- AC6.1 সবুজ (`JournalTypeMigrationTest`, MySQL-এ)।
- দুই flag বন্ধ রেখে merge নিরাপদ; Phase 5-এর পুরো suite সবুজ।
- Seed idempotent প্রমাণিত; orphan থাকলে migration থামে প্রমাণিত।

**Architecture constraints**
- Expand-only migration; প্রতিটা ধাপ আলাদা পুনঃচালুযোগ্য।
- কোনো ledger lock নয়।
- Seed তালিকা config ফাইলে, কোডে হার্ডকোড নয়।
- `down()`-এ নিরাপত্তা-মন্তব্য বাধ্যতামূলক।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Feature (service + api)
Title
Type আচরণ service ও API — Backend
Branch Name
`feature/accounting-journal-type-behavior`
View Order
75
Description
**Summary**
Journal type-এর flag (manual entry, account range, approval ধরন, auto-reverse ইত্যাদি) শুধু কাগজে না — সত্যিকারের আচরণ হিসেবে কাজ করবে; Admin API দিয়ে দেখা ও বদলানো যাবে। ভুল ধরনের journal Draft, Submit আর Post — তিন জায়গাতেই আটকাবে।

**Task serial**
`ACC-P6-02 — Type আচরণ service ও API` · Feature · Phase 6, Build ক্রম 2

**Depends on**
- `ACC-P6-01` (type ও account range table)
- `ACC-P2-03` (validator, যেখানে hook বসে)
- `ACC-P2-08` (approval request ও সিদ্ধান্ত; Submit-এ approval rule আছে কি না দেখার ভিত্তি)
- `ACC-P1-04` (change request, prefix `jtype:`)

**Relation: আগের task**
- `P6-01` দিয়েছে `acc_journal_types` (flag-গুলো) ও `acc_journal_type_account_ranges` (allow/deny পরিসর) এবং প্রতি company-তে 16 seed type। এই task সেই তথ্য পড়ে আচরণে রূপ দেয়।
- `P2-03` দিয়েছে Save/Submit/Post-মোডের validator কাঠামো (সব ত্রুটি একসাথে `details.errors`-এ); এই task সেখানে hook বসায়।
- `P2-08` দিয়েছে approval request; hook (ঙ) Submit-এ rule মিলে কি না দেখে।
- `P1-04` দিয়েছে change request ও `ChangeApplier` রেজিস্ট্রি; এই task `jtype:` prefix-এর applier নিবন্ধন করে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P6-05` (document date): type-এর `backdate_days_limit`/`future_days_limit` `JournalTypeRegistry` থেকে পড়ে।
- `P6-06` (template): template-এর `journal_type_code` ও Draft তৈরির সময় type যাচাই এই registry দিয়ে।
- `P6-07` (import): import পথ `origin` ভিন্ন হওয়ায় manual-নিষেধ এড়ায়, কিন্তু inactive/range/approval যাচাই এই service-এর।
- `P6-08` (সংযুক্তি): hook (চ) `AttachmentPolicyService` ডাকে; type-এর `attachment_rule` ও `attachment_min_count` এই card পড়ার দরজা দেয়।
- `P6-14` (UI): type তালিকা ও range editor এই API ব্যবহার করে।
- `P6-15`: `import` type activate এই service-এর `activatedBy` পথে।

**Related tables / schema**
নতুন table নেই। `P6-01`-এর table ব্যবহার, `acc_journal_types.version` optimistic check। Cache key `acc:jt:{company}:{code}:{version}`।

নতুন `acc_settings` key (পড়া ও Maker-Checker-এ বদলানো, Pack-এর registry তালিকা অনুযায়ী): `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`।

**Business rules**
- 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`)। `deny` overlap-এ `allow`-কে হারায়।
  - (ঘ) 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 থাকলে সতর্কতা সহ আটকায়।
- **Maker-Checker** (`accounting.journal_type.approve`): `strict` থেকে `standard`-এ নামানো, সংযুক্তি `required` থেকে নরম করা, ranges বদল, backdate/future সীমা বদল। P1-04 change request, prefix `jtype:`।
- **Post চলার সময় type বদলালে** সেই Post পুরোনো snapshot ধরে চলে (R6.5): Type শুধু transaction শুরুর snapshot-এ পড়া, কোনো type lock নেই।
- **Open point:** (খ)-তে `origin` কোথা থেকে আসে তা source-এ বলা নেই ও `P1-09` schema-তে `origin` column নেই (phase-স্তরের open point দেখুন)। নির্দিষ্ট করতে হবে `origin` request-স্তরের context না persisted column।
- **Open point:** `journal_types.allow_custom` চালু বা বন্ধ করতে কোন permission ও Maker-Checker লাগবে তা source-এ বলা নেই (OD-24a, custom type)।
- **Open point:** P2-03-এ ব্যবহৃত নাম `SaveValidator`/`SubmitValidator`, source এখানে "তিন মোড: Draft, Submit, Post" বলেছে। Draft মোড = SaveValidator ধরা হয়েছে (নাম-মিল নিশ্চিত করতে হবে)।

**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 নেই।
- **Type বদল (API), এক transaction:** `UPDATE acc_journal_types SET ..., version = version + 1 WHERE company_id = ? AND code = ? AND version = ?`; ০ সারি বদলালে `JTY_CONFLICT`। তারপর audit, commit-এর পর cache মুছা।

**Task scope**
1. `JournalTypeRegistry` ও `JournalTypeDto` ও cache (invalidation সহ)।
2. Validator hook তিন মোডে (`P2-03`-এ বসে): `assertUsable`, `checkLines`।
3. Controller ও Form Request validation।
4. `jtype:` ChangeApplier prefix (P1-04 রেজিস্ট্রিতে নিবন্ধন)।
5. OpenAPI।
6. `JournalTypeBehaviorTest`।

**Out of scope**
Table তৈরি ও seed (`P6-01`)। সংযুক্তির `AttachmentPolicyService`-এর বাস্তবায়ন (`P6-08`)। Backdate/future সীমার `JournalDateRule` (`P6-05`)। UI (`P6-14`)।

**Contract (API)**
- Service: `JournalTypeRegistry::get(company, code): JournalTypeDto` (cache-সহ; না পেলে `JTY_TYPE_NOT_FOUND` ছোড়ে, `JTY_TYPE_INACTIVE` নয়), `::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

**Definition of done**
- সাধারণ DoD পূরণ।
- AC6.2 সবুজ; AC6.5-এর type অংশ সবুজ।
- `journal_mgmt.enabled` বন্ধে সব hook নিষ্ক্রিয় প্রমাণিত; Phase 5 suite সবুজ।
- Type PATCH দুজন একসাথে করলে `JTY_CONFLICT` প্রমাণিত; company isolation (404) প্রমাণিত।

**Architecture constraints**
- `JournalTypeRegistry` type তথ্যের একমাত্র পড়ার দরজা; অন্য কেউ সরাসরি `acc_journal_types` query করে না।
- Post-এ কোনো type lock নয়; lock ক্রম অপরিবর্তিত।
- কোনো type Approval এড়ায় না।
- Cache invalidation commit-এর পরে।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P6-03 — Number series schema ও service

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service + api)
Title
Number series schema ও service — Backend
Branch Name
`feature/accounting-number-series`
View Order
76
Description
**Summary**
Voucher নম্বর type ও source অনুযায়ী series ধরে বরাদ্দ হবে; gapless নম্বর শুধু Post transaction-এর ভেতরে তৈরি হবে। নম্বরে গর্ত বা নকল থাকবে না, আর আগের `JV-{CO}-{FY}-{SEQ}` রূপ default series `JV` হিসেবে হুবহু থাকবে।

**Task serial**
`ACC-P6-03 — Number series schema ও service` · Feature · Phase 6, Build ক্রম 3

**Depends on**
- `ACC-P6-01` (journal type registry, `journal_type_code` FK ও `default_series_code`)
- `ACC-P2-11` (বিদ্যমান Voucher নম্বর ও `acc_voucher_counters`)
- `ACC-P2-12` (Posting service: Post transaction ও lock ক্রম)

**Relation: আগের task**
- `P6-01` দিয়েছে `acc_journal_types` (code ও `default_series_code` column, যেটা এই task-এ FK পায়)।
- `P2-11` দিয়েছে `acc_voucher_counters` (`company_id`, `fiscal_year`, `series` default `JV`, `last_no`) ও `VoucherService::next` যার counter row-এর lock Post commit পর্যন্ত থাকে। এই task সেই আচরণ ও নম্বরের রূপ হুবহু `JV` series-এ রাখে।
- `P2-12` দিয়েছে Post-এর ধাপ ও transaction সীমা। এই task `assign(journal)` ওই transaction-এর ভেতরে চালায়, lock ক্রমে account/period-এর পর।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P6-04` (voucher cutover): এই task-এর `acc_number_series_counters.cutover_base_no` লেখে ও `NumberSeriesService::assign`-এর ওপর facade বসায়।
- `P6-14` (UI): series তালিকা, format preview, assignment, allocation দেখা।
- `P6-15` (enablement): RC1n (counter ক্রমিক, গর্ত ও নকল নেই) ও checklist শর্ত "প্রতি active type-এ অন্তত একটা series assignment বা default series" এই task-এর table-এর ওপর।

**Related tables / schema**

**`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 স্ক্যান)।

**`acc_journals` (expand):** `series_id BIGINT UNSIGNED NULL` ও composite FK `fk_acc_journals_series (company_id, series_id) → acc_number_series (company_id, id)`।

**Business rules**
- 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)।
- `allocate_on = submit` series (শুধু non-gapless) Submit transaction-এ একই কাজ করে; ব্যর্থ হলে allocation `voided`।
- Gapless নম্বর শুধু Post transaction-এর ভেতরে (`NumberSeriesService::next` Post transaction ছাড়া ডাকলে `NSR_NOT_IN_POST_TX`, প্রোগ্রামিং ত্রুটি)।
- Default series `JV` প্রতি company-তে seed হয়, আর `P6-01`-এর `default_series_code`-এর সাথে যুক্ত হয়।
- **Open point:** `NumberSeriesService::next(company, seriesCode, date)` (series-code ধরে) আর `assign(journal)` (assignment resolver ধরে) দুটো পথ আছে; `next` কোন caller ডাকে তা source বলেনি (সম্ভবত শুধু `assign` ও test)। Counter-key `P2-11`-এর `fiscal_year SMALLINT`-এর সাথে `VARCHAR(10)` `counter_key` কীভাবে ম্যাপ হবে `P6-04`-এ cutover দেখবে।
- **Open point:** OD-25a (gapless default) ও OD-25c (reset ও বছর)-এর সিদ্ধান্ত এই document-এ নেই।
- **Open point:** CHECK `gapless = 1 OR allocate_on IN ('post','submit')` যুক্তিগতভাবে সবসময় সত্য (ENUM-এ শুধু ওই দুই মান); source-এর হুবহু লেখা, উদ্দেশ্য সম্ভবত `gapless = 1 → allocate_on = 'post'`, যা দ্বিতীয় CHECK আদায় করে। হুবহু রাখা হল।

**DB flow**
**`assign(journal)`** (Post transaction-এর ভেতরে, lock ক্রমে journal, approval request, account, period-এর পর):
1. Series বাছাই; শুধু পড়া `acc_number_series_assignments` ও `acc_number_series` `FOR SHARE`।
2. `counter_key` ঠিক করা (yearly = period-এর fiscal_year, monthly = period_key, never = `ALL`)।
3. `INSERT ... ON DUPLICATE KEY UPDATE id = id` দিয়ে counter সারি নিশ্চিত, তারপর `SELECT ... FOR UPDATE` সেই সারি।
4. `last_no + 1`; overflow পরীক্ষা (`padding` অঙ্কের বেশি হলে `NSR_SERIES_EXHAUSTED`)।
5. `UPDATE counters SET last_no = ?`।
6. `INSERT acc_voucher_allocations` (status `used`)।
7. `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 বার তিনেক আবার চালায়।

**Task scope**
1. Migration ও model: চারটা table + `acc_journals` expand।
2. Format parser ও token।
3. `assign`, `next`, `preview`, `void`।
4. Default series `JV` প্রতি company-তে seed ও `P6-01`-এর `default_series_code` সংযোগ (FK `fk_acc_jt_series`)।
5. Assignment resolver (priority ও `effective_from` বাছাই)।
6. API (series, assignments, preview, allocations)।
7. Concurrency test।

**Out of scope**
পুরোনো `acc_voucher_counters` থেকে cutover ও `VoucherService` facade (`P6-04`)। UI (`P6-14`)। Flag চালু (`P6-15`)।

**Contract (API)**
- Service: `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

**Definition of done**
- সাধারণ DoD পূরণ।
- AC6.4, AC6.5, G6.1 সবুজ।
- `acc_voucher_allocations` append-only trigger কাজ করে (শুধু `used` থেকে `voided`)।
- Flag বন্ধে পুরোনো `P2-11` পথ অপরিবর্তিত (ফাইনাল গেট `P6-04`-এ)।

**Architecture constraints**
- Lock ক্রম: series `FOR SHARE` → counter `FOR UPDATE`, dimension value lock-এর আগে। Counter আলাদা transaction-এ বাড়ানো নিষেধ।
- Gapless নম্বর Post transaction ছাড়া তৈরি নয়।
- `acc_voucher_allocations`-এ কখনো DELETE নয়।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P6-04 — Voucher counter migration ও VoucherService facade

Project
ERPFlow
Module
Accounting Module
Type
Feature (command + service)
Title
Voucher counter migration ও VoucherService facade — Backend
Branch Name
`feature/accounting-voucher-cutover-facade`
View Order
77
Description
**Summary**
পুরোনো `acc_voucher_counters` থেকে নতুন series counter-এ নিরাপদ cutover হবে। `VoucherService` facade হয়ে flag অনুযায়ী পুরোনো বা নতুন পথ ডাকবে — পুরোনো নম্বরের রূপ এক, গর্ত/নকল নেই, rollback সম্ভব।

**Task serial**
`ACC-P6-04 — Voucher counter migration ও VoucherService facade` · Feature · Phase 6, Build ক্রম 4

**Depends on**
- `ACC-P6-03` (নতুন series ও counter)
- `ACC-P2-11` (বিদ্যমান `VoucherService`, `acc_voucher_counters`)
- `ACC-P2-12` (Posting service, যে facade ডাকে)
- `ACC-P1-05` (Setup Checklist; S12 ধাপের জন্য)

**Relation: আগের task**
- `P6-03` দিয়েছে `acc_number_series_counters` (`cutover_base_no` column সহ) আর `NumberSeriesService::assign`। এই task পুরোনো `last_no` সেখানে নিয়ে আসে।
- `P2-11` দিয়েছে পুরোনো `VoucherService::next(companyId, fiscalYear)` ও `acc_voucher_counters (company_id, fiscal_year, series, last_no)`। Flag বন্ধে এই কোডই হুবহু চলবে।
- `P2-12` Post-এর ধাপ 11-এ `VoucherService::next` ডাকে; এই task-এর পর সে শুধু facade ডাকে, সরাসরি দুটোর কোনোটি নয়।
- `P1-05` দিয়েছে checklist কাঠামো ও `acc_enablement_log`-এর সাথে যুক্ত নিবন্ধন।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P6-15` (enablement): `journal_mgmt.enabled` চালুর শর্ত "সম্পন্ন cutover, dry-run অমিল 0" ও "production-copy-তে cutover রিহার্সাল"; rollback command চালুর flag বন্ধের পূর্বশর্ত; runbook অধ্যায় এই card জমা দেয় (cutover, rollback, নম্বরে গর্ত সন্দেহ)।

**Related tables / schema**
নতুন table নেই।
- `acc_number_series_counters.cutover_base_no` (`P6-03`) এই card লেখে।
- `acc_voucher_counters` (`P2-11`) অপরিবর্তিত, cutover-এর পর শুধু পড়ার। Legacy table একটা period-close চক্র পর contract migration-এ বাদ (OD-25b প্রস্তাব), তার আগে নয়।
- `acc_enablement_log` (`P5-06`) এ cutover/rollback সারি।
- Legacy write-guard trigger (নতুন): cutover-এর পর `acc_voucher_counters`-এ `UPDATE` ব্যর্থ, শুধু rollback command ছাড়া।

**Business rules**
- 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 ছাড়া)।
- Cutover আবার চালালে একই ফল। Flag চালু থাকলে cutover আবার চললে `NSR_CUTOVER_PRECONDITION`।
- Flag চালু আলাদা ধাপ (Maker-Checker, `P1-04`), cutover-এর সাথে একই transaction নয়।
- `--confirm` ছাড়া CLI সবসময় dry-run।
- HTTP-তে শুধু dry-run; আসল cutover HTTP-তে নেই।
- **Open point:** `P2-11`-এ `VoucherService::next(companyId, fiscalYear)`, কিন্তু এই card বলছে `VoucherService::next(company, journal)` এবং "স্বাক্ষর অপরিবর্তিত"। দুটো সঙ্গতিপূর্ণ নয়; facade নতুন স্বাক্ষর নিলে `P2-12` ও পুরোনো test বদলাবে, নইলে `journal` অন্যভাবে পৌঁছাতে হবে। Dev আগে `P2-11`-এর বাস্তব স্বাক্ষর নিশ্চিত করবে।
- **Open point:** `P2-11`-এর counter `last_no INT UNSIGNED`, series counter `BIGINT UNSIGNED`; এবং `P2-11`-এর `voucher_format` setting (বাধ্যতামূলক) কীভাবে default series `JV`-এর `format` হয়ে যায় তা source-এ স্পষ্ট নয় (শুধু "রূপ হুবহু" বলা আছে)।
- **Open point:** OD-25b (পুরোনো counter-এর ভাগ্য) সিদ্ধান্ত অনুযায়ী legacy table কবে বাদ যাবে।
- **Open point:** "backup নিশ্চিত নয়" ও "maintenance window চালু আছে কি" কীভাবে system যাচাই করে (flag/setting/ops confirmation) source-এ বলা নেই।

**DB flow**
- **Dry-run:** শুধু `SELECT`, কোনো lock নয়।
- **Cutover (প্রতি company এক transaction):**
  1. Maintenance window চালু আছে কি যাচাই।
  2. `SELECT ... FROM acc_voucher_counters WHERE company_id = ? FOR UPDATE` (চলমান পুরোনো-পথ Post commit পর্যন্ত অপেক্ষা করে, তাই কোনো নম্বর হারায় না)।
  3. প্রতি `(fiscal_year, series JV)`: নতুন counter `INSERT ... ON DUPLICATE KEY UPDATE last_no = GREATEST(last_no, VALUES(last_no)), cutover_base_no = VALUES(last_no)`।
  4. দুই পক্ষ তুলনা: নতুন `last_no` = পুরোনো `last_no`, নইলে rollback ও `NSR_CUTOVER_PRECONDITION`।
  5. `acc_enablement_log`-এ সারি।
  6. commit।
- **Rollback:** flag আগে বন্ধ, তারপর একই lock ক্রমে নতুন counter `FOR UPDATE`, পুরোনো `last_no = GREATEST(পুরোনো, নতুন)`, এক transaction।
- **Lock ক্রম:** পুরোনো counter তারপর নতুন counter (কোনো Post-পথ দুটো একসাথে নেয় না, তাই চক্র নেই)।

**Task scope**
1. Facade ও feature check (`VoucherService::next`)।
2. Cutover command ও dry-run।
3. Rollback command।
4. Legacy write-guard trigger।
5. `P2-12` integration (facade ডাকা)।
6. `acc_enablement_log` লেখা।
7. Concurrency test ও production-copy রিহার্সাল।

**Out of scope**
Series schema/assign (`P6-03`)। Flag চালু (`P6-15`)। Legacy table বাদ দেওয়া (পরের contract migration, OD-25b)।

**Contract (API)**
- 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` স্বাক্ষর অপরিবর্তিত (উপরের open point দেখুন)।

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

**Definition of done**
- সাধারণ DoD পূরণ।
- AC6.3, AC6.4, G6.2 সবুজ।
- Production-copy-তে cutover রিহার্সাল সফল ও rollback প্রমাণিত।
- Flag বন্ধে `P2-11` ও `P2-12`-এর পুরোনো test অপরিবর্তিত পাস।

**Architecture constraints**
- Cutover প্রতি company এক transaction; flag চালু আলাদা ধাপ।
- `--confirm` ছাড়া কোনো লেখা নয়।
- আসল cutover HTTP-তে নেই।
- Trigger বা unique constraint-এর `down()` production-এ চালানো নিষেধ।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P6-05 — Document date, posting date ও backdate

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service)
Title
Document date, posting date ও backdate — Backend
Branch Name
`feature/accounting-document-date-backdate`
View Order
78
Description
**Summary**
`journal_date` (posting date) আর `document_date` (মূল কাগজের তারিখ) আলাদা থাকবে; type অনুযায়ী backdate ও future সীমা খাটবে। মূল কাগজের তারিখ সংরক্ষিত থাকবে, অযৌক্তিক তারিখ Submit/Post-এ আটকাবে।

**Task serial**
`ACC-P6-05 — Document date, posting date ও backdate` · Feature · Phase 6, Build ক্রম 5

**Depends on**
- `ACC-P6-02` (type আচরণ ও `JournalTypeRegistry`)
- `ACC-P2-03` (validator)
- `ACC-P1-10` (DB guard trigger)

**Relation: আগের task**
- `P6-02` দিয়েছে `JournalTypeRegistry::get` (type-এর `requires_document_date`, `backdate_days_limit`, `future_days_limit`, cache-সহ) ও validator hook-এর কাঠামো। এই task সেই hook-এ `JournalDateRule` যোগ করে।
- `P2-03` দিয়েছে `journal_date` যাচাই (cutover-এর আগে নয়, `backdate_limit_days`-এর মধ্যে, period-এ পড়ে) যা company-স্তরের; এই task type-স্তরের সীমা যোগ করে, কিন্তু company-স্তরের সীমা শিথিল করে না।
- `P1-10` দিয়েছে trigger, যার অপরিবর্তনীয় column তালিকায় এই task `document_date` ও `series_id` যোগ করে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P6-06` (template): `create-draft` `journalDate` নেয় এবং Draft-মোডে এই rule-সহ validator চালায়।
- `P6-07` (import): কলাম `document_date` ও `journal_date` এই rule দিয়ে যাচাই।
- `P6-14` (UI): journal form-এ document date field ও `warnings[]` প্রদর্শন।

**Related tables / 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` যোগ।
- Setting: `journal.document_date_policy` (`allow`/`warn`/`block`), type-স্তরের `type.backdate_days_limit`, `type.future_days_limit`।

**Business rules**
- R6.18, R6.19।
- `document_date` বাদ দিলে `journal_date` বসে। বিদ্যমান সারিতে NULL; পড়ার সময় `COALESCE(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-এ সতর্কতা (সফল, response-এ `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 একইভাবে (`future_days_limit`)।
- `today` কোম্পানির timezone-এ।
- Type সীমা `PeriodGuard` কখনো শিথিল করে না; `PeriodGuard` আগে চলে।
- সীমা কমালে আগের Draft-এর Submit-এ স্পষ্ট ত্রুটি।
- System type-এর default সীমা `NULL` (OD-26b (ক))।
- Policy বদল: `accounting.journal_type.manage` + Maker-Checker (`block` থেকে `allow`-এ নামানোর জন্য)।
- **Open point:** OD-26b (backdate সীমা) ও OD-26c (document date পরে হলে)-র সিদ্ধান্ত লিখিত নেই; `JTY_BACKDATE_LIMIT`-এর future-সীমা লঙ্ঘনের ত্রুটি কোড আলাদা কি না source বলেনি (এখানে একই কোড ধরা হয়েছে, নিশ্চিত নয়)।
- **Open point:** `P2-03`-এ `backdate_limit_days` setting আছে (company-স্তর, শুধু Submit-এ); type-স্তরের সীমা Post-এ চলবে কি না source "Submit ও Post-এ `JournalDateRule::check` আবার" বলেছে, কিন্তু `P2-03`-এর backdate Post-এ "না"। দুই নিয়মের সম্পর্ক (কোনটা কঠোর) নিশ্চিত করতে হবে।

**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)`)।

**Task scope**
1. Column migration (`document_date`) ও index।
2. `P1-10` trigger হালনাগাদ (`document_date`, `series_id` অপরিবর্তনীয়)।
3. `JournalDateRule` class।
4. Draft/Submit/Post hook।
5. API field `document_date` ও response `warnings[]`।
6. `JournalDateRuleTest`।

**Out of scope**
Type registry (`P6-01/02`)। Period বন্ধ/lock (`PeriodGuard`, `P3-12`)। UI (`P6-14`)।

**Contract (API)**
- `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

**Definition of done**
- সাধারণ DoD পূরণ।
- AC6.2, G6.8 সবুজ।
- `document_date` Post-এর পর trigger-এ অপরিবর্তনীয় প্রমাণিত।
- Flag বন্ধে আচরণ Phase 5 শেষের মতো।

**Architecture constraints**
- `PeriodGuard` সবসময় আগে; type সীমা তাকে শিথিল করতে পারে না।
- Migration শুধু nullable column যোগ (online); বিদ্যমান সারি পড়তে `COALESCE`।
- কোনো নতুন lock নয়।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---
## ACC-P6-09 — Dimension framework schema ও service

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service + job)
Title
Dimension framework schema ও service — Backend
Branch Name
`feature/accounting-dimension-framework`
View Order
79
Description
**Summary**
Cost center-এর বাইরেও project, department, location ইত্যাদি analytical dimension প্রতি line-এ বসানো যাবে — হিসাব একাধিক দৃষ্টিকোণ থেকে কাটা যাবে, পুরোনো `cost_center_id` সংযোগ ঠিক থাকবে।

**Task serial**
`ACC-P6-09 — Dimension framework schema ও service` · Feature · Phase 6, Build ক্রম 6

**Depends on**
- `ACC-P1-10` (DB guard: Posted line-এর trigger, `Schema::appendOnly()`)
- `ACC-P3-01` (Ledger query layer: posted line পড়ার ভিত্তি)
- `ACC-P2-03` (validator ও line লেখার পথ)

**Relation: আগের task**
- `P1-10` দিয়েছে trigger কাঠামো (Posted journal-এর line বদলানো/মোছা আটকানো) আর `Schema::appendOnly()` helper। এই task `acc_journal_line_dimensions`-এ একই ধরনের trigger বসায় (backfill session চিহ্ন ছাড়া INSERT-ও আটকায়) এবং `acc_dimension_value_history`-কে append-only করে।
- `P3-01` দিয়েছে `LedgerQueryService` (posted ও reversed journal-এর line থেকে হিসাব)। Backfill শেষে RC1m সবুজ থাকতে হবে; সেই যাচাই ledger-এর সাথে dimension সারি মেলায়।
- `P2-03` দিয়েছে journal save/validator পথ; এই task `DimensionLineWriter`-কে সেই পথে line লেখার অংশ করে।
- `P1-09` দিয়েছে `acc_journal_lines.cost_center_id` (Configuration FK); এই task তার প্রতিফলন dimension সারিতে রাখে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P6-10` (rule, default, validation, balancing): `acc_dimensions`/`acc_dimension_values` পড়ে rule ও default গড়ে; `acc_accounts`-এ rule set যুক্ত করে।
- `P6-11` (value lifecycle ও mirror): এই task-এর table-গুলোর ওপর value বদল, move, delete ও mirror sync; `acc_dimensions.mirror_synced_at` লেখে।
- `P6-06` (template): `acc_journal_template_line_dimensions`-এর FK `acc_dimensions`/`acc_dimension_values`-এ।
- `P6-12` (reclassification): `acc_journal_line_dimensions`-এর ওপর নিট balance হিসাব।
- `P6-13` (report filter ও cache): `idx_ald_value` ও `acc_journal_line_dimensions` ব্যবহার করে filter/group-by।
- `P6-14`, `P6-15`: UI ও `dimensions.enabled` চালুর শর্ত "Backfill সম্পূর্ণ, RC1m সবুজ"।

**Related tables / schema**

**`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 চিহ্ন ছাড়া)।

**`acc_dimension_backfill_state`**: backfill cursor (company, last_line_id)। Column-এর পূর্ণ তালিকা source-এ দেওয়া নেই।

`acc_journal_lines` বদলায় না।

**Business rules**
- R6.27, R6.28, R6.29, R6.30, R6.33।
- `dimension.max_count` সীমা (`DIM_LIMIT_EXCEEDED`); প্রতি line-এ সর্বোচ্চ ৪টা dimension (`DIM_LINE_LIMIT`, 422)।
- `cost_center` dimension (built-in) মুছা বা বন্ধ যায় না (`DIM_BUILTIN_PROTECTED`)। `cost_center` dimension `is_balancing = 0` সবসময়।
- Dimension `code` ছোট হরফ ও `_`; rename অনুমতি, ইতিহাস `acc_dimension_value_history`-তে।
- Value tree: গভীরতা ≤ `dimension.max_depth`, `path` materialized (`/1/5/9/`)।
- এক line-এ এক dimension-এ এক value (PK `uq`)।
- Dimension-এর `kind`: `builtin`, `manual`, `mirror`।
- **Backfill শেষে RC1m সবুজ থাকতে হবে** (আগে `dimensions.enabled` নয়)।
- **সব write পথে `cost_center_id` ও dimension সারি একই মানে থাকে** (`cost_center` dimension-এর মান = `cost_center_id`, অমিলে rollback)।
- **Open point:** `acc_dimension_backfill_state`-এর column তালিকা, `DIM_COST_CENTER_MISMATCH` এ কোন লেখার পথে অমিল ধরা পড়ে (service পথ vs trigger) source-এ স্পষ্ট নয়।
- **Open point:** OD-28b (hierarchy ইতিহাস: roll-up বর্তমান গাছ ধরে) ও OD-28c (mirror আচরণ)-র লিখিত সিদ্ধান্ত এই document-এ নেই।
- **Open point:** `acc_journal_line_dimensions.journal_line_id` নাম "Master Plan §৬" থেকে; সেই Master Plan এই document-এ নেই।

**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 নেই।

**Task scope**
1. Migration (৪ table + backfill state) ও built-in `cost_center` seed।
2. `P1-10` trigger হালনাগাদ (`acc_journal_line_dimensions`)।
3. `DimensionLineWriter` ও `cost_center_id` mirror rule।
4. Value tree service (`path`, `depth`, cycle/depth যাচাই)।
5. Backfill job ও cursor।
6. Health endpoint (`GET /dimensions/health`)।
7. API।
8. Test ও benchmark।

**Out of scope**
Rule/default/validation/balancing (`P6-10`)। Value rename/move/delete/mirror sync (`P6-11`)। Reclassification (`P6-12`)। Report filter ও `acc_dimension_balances` (`P6-13`)। UI (`P6-14`)।

**Contract (API)**
- Service: `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

**Definition of done**
- সাধারণ DoD পূরণ।
- AC6.8, G6.5 সবুজ।
- Backfill পুনঃচালুযোগ্য, নকলহীন; benchmark সংখ্যা লিখিত।
- `dimensions.enabled` বন্ধ রেখে merge নিরাপদ।

**Architecture constraints**
- Composite FK ও `uq (company_id, id)` নিয়ম।
- Posted line-এর dimension সারি বদলানো বা মোছা কোনো পথেই নয়; INSERT শুধু backfill session চিহ্ন সহ।
- `cost_center_id` ও `cost_center` dimension সারি সবসময় একই মান।
- কোনো ledger lock নয়।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P6-10 — Dimension rule, default, validation, balancing

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service + api)
Title
Dimension rule, default, validation, balancing — Backend
Branch Name
`feature/accounting-dimension-rules`
View Order
80
Description
**Summary**
কোন account-এ কোন dimension বাধ্যতামূলক, optional বা নিষিদ্ধ — সেটা ঠিক করা যাবে; default মান বসবে; Draft, Submit ও Post-এ যাচাই হবে (চাইলে dimension-level balancing সহ)। বিশ্লেষণের তথ্য অসম্পূর্ণ বা ভুল থেকে যাবে না।

**Task serial**
`ACC-P6-10 — Dimension rule, default, validation, balancing` · Feature · Phase 6, Build ক্রম 7

**Depends on**
- `ACC-P6-09` (dimension ও value table)
- `ACC-P2-03` (validator)
- `ACC-P2-12` (Posting service ও lock ক্রম)

**Relation: আগের task**
- `P6-09` দিয়েছে `acc_dimensions` (`is_balancing`, `kind`), `acc_dimension_values` (active, effective dates) ও `acc_journal_line_dimensions`। Rule ও validation সেগুলোর ওপর দাঁড়ায়।
- `P2-03` দিয়েছে validator কাঠামো (Draft/Submit/Post, সব ত্রুটি একসাথে); এই task `validateLines` ও `applyDefaults` সেখানে বসায়।
- `P2-12` দিয়েছে Post-এর ধাপ ও lock ক্রম; এই task Post-এ `lockAndRecheck` যোগ করে (source যাকে "ধাপ 11a" বলেছে, voucher/counter ধাপের পরে, lock ক্রমের একদম শেষে)।
- `P1-06` দিয়েছে `acc_accounts` (এখানে `default_dimension_rule_id` expand হয়) ও account group/type চেইন।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P6-06` (template): template Draft তৈরিতে `applyDefaults` সহ dimension default ও যাচাই চলে।
- `P6-07` (import): validate ধাপে `validateLines` সহ পূর্ণ Draft-মোড validator dry-run।
- `P6-11` (value lifecycle): value deactivate/delete-এ rule-এর `default_value_id` ও Post-এর `FOR SHARE` এর সাথে সামঞ্জস্য।
- `P6-12` (reclassification): reclass journal-এর dimension যাচাই এই validation দিয়ে; `forbidden/mandatory` নিয়মের সাথে মেলে।
- `P6-13` (report filter): `RC1d`, `dimension.default_requirement` ও balancing-এর তথ্য।
- `P6-14` (UI): rule set editor ও dry-run পর্দা।
- `P6-15`: `dimensions.enabled` চালুর শর্ত "Rule conflict শূন্য, `dimension.default_requirement` dry-run-এ ব্যর্থ Draft তালিকা পর্যালোচিত, RC1d সবুজ (balancing চালু থাকলে)"।

**Related tables / schema**

**`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)`।

**`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)`।

Setting: `dimension.default_requirement`, `dimension.allow_balancing`, `dimension.<code>.is_balancing`, `dimension_rule.<set>`।

**Business rules**
- 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। `dimension.allow_balancing` বন্ধ থাকলে `is_balancing` চালু হয় না।
- **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`)।
- `mandatory` বা `forbidden` যোগ এবং balancing বদলে `.approve` লাগে, Maker ≠ Checker।
- **Open point:** OD-28a (balancing) ও OD-28d (reversal-এ inactive value)-র baseline লিখিত নেই; "valueহীন line আলাদা এক বাঁধা" বাক্যটির সঠিক অর্থ (valueহীন line-গুলোর সমষ্টি আলাদাভাবে Debit = Credit) অনুমানভিত্তিক।
- **Open point:** `P6-10` এর "ধাপ 11a" নামে কোনো ধাপ `P2-12`-এর ধাপ-তালিকায় নেই (আছে ধাপ 1 থেকে 17); ধরা হয়েছে voucher/counter (ধাপ 11)-এর পরে, `posted_hash` (ধাপ 12)-এর আগে একটা নতুন ধাপ। Dev `P2-12` কোডে সঠিক জায়গা ঠিক করে নিশ্চিত করবে।
- **Open point:** `P2-12` এর lock ক্রম বর্ণনা (journal → request → counter) আর Phase 6-এর ক্রম (journal, request, account, period, series, counter, dimension value) এক নয়; Pack ১০.৩ জানিয়েছে account ও period `FOR SHARE` যোগ হয়েছে, এই ক্রমের কোড-স্তরের বাস্তবায়ন `P2-12` বা `P3-12` কার দায়িত্ব স্পষ্ট নয়।

**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 নেই।

**Task scope**
1. Migration: দুই table + `acc_accounts` expand।
2. `DimensionRuleResolver` ও cache (key `acc:dr:{company}:{rule_version}`)।
3. `validateLines`, `applyDefaults`।
4. `lockAndRecheck` ও `P2-12` hook।
5. Balancing (journal-পর্যায়ে value-ভিত্তিক Debit = Credit)।
6. Rule API ও dry-run।
7. Concurrency ও performance test।

**Out of scope**
Dimension/value তৈরি (`P6-09`)। Value deactivate/move/delete ও mirror (`P6-11`)। Report (`P6-13`)। UI (`P6-14`)।

**Contract (API)**
- Service: `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 (শুধু `DIM_CONFLICT` 409)।

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

**Definition of done**
- সাধারণ DoD পূরণ।
- AC6.8, AC6.9, G6.3, G6.4 সবুজ।
- Deadlock 0 stress test, 150ms benchmark সংখ্যা লিখিত।
- `dimensions.enabled` বন্ধে নিয়ম নিষ্ক্রিয়, Phase 5 suite সবুজ।

**Architecture constraints**
- `lockAndRecheck` lock ক্রমের একদম শেষে; value id সবসময় ক্রমে (deadlock এড়াতে)।
- Draft/Submit-এ কোনো lock নয়, শুধু Post-এ `FOR SHARE`।
- Post ব্যর্থ হলে পুরো transaction rollback (counter সহ)।
- Balancing শুধু নতুন journal-এ।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P6-11 — Dimension value lifecycle ও mirror

Project
ERPFlow
Module
Accounting Module
Type
Feature (service + job + api)
Title
Dimension value lifecycle ও mirror — Backend
Branch Name
`feature/accounting-dimension-value-mirror`
View Order
81
Description
**Summary**
Dimension value নিরাপদে তৈরি, rename, সরানো, বন্ধ ও মোছা যাবে। ERPFlow-এর master data (cost center, project, department, location) থেকে mirror dimension idempotent sync হবে — হাতে দুই জায়গায় তালিকা রাখতে হবে না, চলমান Post-এর সাথে সংঘাত হবে না।

**Task serial**
`ACC-P6-11 — Dimension value lifecycle ও mirror` · Feature · Phase 6, Build ক্রম 8

**Depends on**
- `ACC-P6-09` (dimension ও value table)
- `ACC-P1-12` (tenancy, audit, error-এর সাধারণ ব্যবস্থা)
- `P5 alert` (source এভাবেই লিখেছে: `acc_alert_state`, Phase 5 `ACC-P5-03` Monitoring ও alert)

**Relation: আগের task**
- `P6-09` দিয়েছে `acc_dimensions` (`kind = mirror`, `source_type`, `mirror_synced_at`), `acc_dimension_values` (`path`, `depth`, `source_type/id/hash`, `version`) ও `acc_dimension_value_history`। এই task সেগুলোর ওপর service বসায়।
- `P1-12` দিয়েছে `AccountingAudit`, `AccountingError`, company scope। 
- `P5-03` দিয়েছে `acc_alert_state` (`alert_key`, `state`: `ok`/`firing`, ঝড়-নিয়ন্ত্রণ); এই task `dimension_mirror_stale` alert তার ওপর লেখে।
- (source-এ নির্ভরতা না থাকলেও) `P6-10` rule set-এর `default_value_id` ও Post-এর `FOR SHARE` এই task-এর delete/deactivate নিয়মের সীমা ঠিক করে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P6-14` (UI): value গাছ, mirror অবস্থা পর্দা।
- `P6-15` (enablement): `dimensions.enabled` চালুর শর্ত "Mirror প্রথম sync সফল"; runbook "mirror stale"।
- (পরোক্ষ) `P6-13` report-এ roll-up বর্তমান গাছ ধরে, তাই move এই task-এ গাছ বদলালে report বদলায়।

**Related tables / schema**
নতুন table নেই (`P6-09`-এর table)। `acc_dimensions.mirror_synced_at` লেখা হয়। `acc_dimension_value_history` (append-only) INSERT। `acc_alert_state` (`P5-03`) alert `dimension_mirror_stale`।
Setting: `dimension.mirror_sync_interval_min` (default 15), `dimension.max_depth`।

**Business rules**
- 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।
- মুছা: কোনো `acc_journal_line_dimensions` row-এ ব্যবহৃত নয়, কোনো rule-এর `default_value_id` নয়, mirror নয়, নইলে `DIM_VALUE_IN_USE`।
- Sync-এ source-এ না পেলে value inactive (মোছা নয়)।
- **Open point:** `dimension_mirror_stale` alert-key `P5-03`-এর alert তালিকায় (recon_failed, consumer_lag, held_events, ...) নেই; এই card-এ নতুন key যোগ করতে হবে, `P5-03`-এর health API-তে দেখা যাওয়ার জন্য।
- **Open point:** OD-28c (department ও location source কোন master data থেকে আসবে) source তালিকার ব্যাখ্যা লিখিত নেই; `MasterDataReader` adapter-এর বাস্তব সংযোগ এই কারণে অনিশ্চিত।
- **Open point:** "Cycle বা গভীরতা ভঙ্গ করলে সারি ডিঙিয়ে alert" কোন alert key-তে যাবে (stale নাকি আলাদা) স্পষ্ট নয়।

**DB flow**
- **Value বদল (rename, deactivate):** এক transaction, value সারি `FOR UPDATE`, `version` মেলানো, `acc_dimension_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।
- **Lock:** কোনো ledger lock নেই।

**Task scope**
1. Value service (`create/rename/move/deactivate/delete`) ও history।
2. Move ও `path`/`depth` পুনর্গণনা।
3. Delete নিয়ম।
4. `MasterDataReader` interface ও adapter (শুধু পড়া)।
5. Sync job ও diff।
6. Alert (`dimension_mirror_stale`, `acc_alert_state`)।
7. API।
8. Test।

**Out of scope**
Dimension কাঠামো ও backfill (`P6-09`)। Rule/default (`P6-10`)। UI (`P6-14`)। `MasterDataReader`-এর বাস্তব সংযোগের চূড়ান্ত সিদ্ধান্ত (OD-28c)।

**Contract (API)**
- Service: `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

**Definition of done**
- সাধারণ DoD পূরণ।
- AC6.11 সবুজ।
- Sync idempotent প্রমাণিত (পুনরায় চালালে পরিবর্তন শূন্য)।
- `MasterDataReader` শুধু পড়ে; master data কোথাও লেখা হয় না।

**Architecture constraints**
- Move-এ lock ক্রম: dimension → value (id ক্রমে)। Post-এর সাথে সামঞ্জস্য রাখতে value lock সবসময় id ক্রমে।
- Mirror value-র code/name/parent/active শুধু sync-এ বদলায়।
- Posted line কখনো বদলায় না (inactive করলেও)।
- কোনো ledger lock নয়।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---
## ACC-P6-06 — Journal template

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service + api)
Title
Journal template — Backend
Branch Name
`feature/accounting-journal-template`
View Order
82
Description
**Summary**
বারবার হওয়া journal-এর জন্য parameter-সহ template থাকবে; template থেকে সবসময় Draft (কখনো সরাসরি Post নয়)। Template বদল নতুন version আর দ্বিতীয় অনুমোদন দিয়ে হবে — একই ধরনের entry দ্রুত, বারবার একই ভুল ছাড়া।

**Task serial**
`ACC-P6-06 — Journal template` · Feature · Phase 6, Build ক্রম 9

**Depends on**
- `ACC-P6-02` (type আচরণ ও registry)
- `ACC-P6-09` (dimension কাঠামো, template line-এ dimension)
- `ACC-P2-03` (validator, Draft মোডে)
- `ACC-P1-04` (change request, prefix `jtemplate:`)
- (অন্য card-এর তালিকা অনুযায়ী যোগ: `ACC-P6-10` তাকে "অবরুদ্ধ করে" বলেছে, `ACC-P6-05`-ও; নিচে open point)

**Relation: আগের task**
- `P6-02` দিয়েছে `JournalTypeRegistry` (template-এর `journal_type_code` যাচাই ও Draft-এর type-আচরণ hook)।
- `P6-09` দিয়েছে dimension ও value table; template line-dimension সেগুলোর FK।
- `P6-10` (build ক্রমে আগে) দিয়েছে `DimensionService::applyDefaults` ও `validateLines`, যা create-draft-এ চলে।
- `P6-05` দিয়েছে `JournalDateRule` (create-draft-এর `journalDate`-এ প্রযোজ্য)।
- `P2-03` দিয়েছে Draft মোডের validator যা create-draft-এর সব নিয়ম চালায়।
- `P1-04` দিয়েছে change request ও `ChangeApplier`; এই task `jtemplate:` prefix নিবন্ধন করে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P6-14` (UI): template তালিকা, সম্পাদক (expression ও parameter), সম্পাদনার অনুরোধের অবস্থা, Draft তৈরির পর্দা।
- Phase 12 (recurring): recurring journal এই template কাঠামোর ওপর। (Phase 12 এই document-এ নেই, অন্য tab-এ)
- `P6-15`: ফ্লো-সমূহ UAT (WF-A থেকে WF-G)-তে template থেকে Draft একটা ধাপ।

**Related tables / schema**
`acc_journals` বদলায় না; ব্যবহারের সূত্র `acc_journal_template_uses`-এ।

**`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)`।

Setting: `template.edit_requires_approval`।

**Business rules**
- 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 অক্ষত।
- ব্যবহৃত version-এর সারি কখনো UPDATE হয় না (trigger শুধু `status` বদলাতে দেয়)।
- Company scope-এ নেওয়া ও সম্পাদনা অনুমোদনে সম্পাদক ≠ অনুমোদক।
- Template থেকে Post সরাসরি অসম্ভব (সবসময় Draft)।
- **Open point:** `P6-10` তার অবরুদ্ধ-তালিকায় `P6-06` রেখেছে এবং `P6-05`-ও রেখেছে, কিন্তু এই card-এর "নির্ভর করে" তালিকায় `P6-10` ও `P6-05` নেই (আছে `P6-02`, `P6-09`, `P2-03`, `P1-04`)। Build ক্রমে দুটো আগেই শেষ হওয়ায় বাস্তবে সমস্যা নেই; কিন্তু dependency graph-এ যোগ করা উচিত কি না tech lead ঠিক করবে।
- **Open point:** OD-26a (সম্পাদনার অনুমোদন) সিদ্ধান্ত লিখিত নেই; "Template ব্যবহার Draft-এ সীমিত: সব আবশ্যিক field (tax, কর, ইত্যাদি) validator-এ" বাক্যে tax field কোন Phase-এর (Phase 8) তা স্পষ্ট নয়, এখন শুধু বিদ্যমান validator-এর নিয়ম ধরতে হবে।
- **Open point:** `params` JSON-এর `type` মানের তালিকা (যেমন number, account, dimension value) source-এ দেওয়া নেই; `account_param`/`value_param` কোন type-এর parameter নেয় আর তার যাচাই কী হবে নির্দিষ্ট করতে হবে।

**DB flow**
- **create-draft (একটা transaction):**
  1. `_versions` পড়া (শুধু `active`, lock নেই)।
  2. Parameter যাচাই ও `ExpressionEvaluator` (DB ছোঁয় না)।
  3. `JournalService::createDraft` (`P2-03`, Draft মোডে validator সব নিয়ম চালায়, dimension default `applyDefaults` সহ)।
  4. `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` বদল।

**Task scope**
1. Migration (৬ table) ও trigger (ব্যবহৃত version-এর সারি অপরিবর্তনীয়, শুধু `status` বদল)।
2. `ExpressionEvaluator` (নিজের parser, sandbox)।
3. Service: create, edit, approve, retire, createDraft।
4. `jtemplate:` ChangeApplier prefix।
5. Share ও দৃশ্যতা scope।
6. API।
7. Test (`JournalTemplateTest`, `TemplateExpressionTest`)।

**Out of scope**
Recurring schedule (Phase 12)। Template থেকে Post। UI (`P6-14`)।

**Contract (API)**
- Service: `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

**Definition of done**
- সাধারণ DoD পূরণ।
- AC6.6, G6.9 সবুজ।
- Expression injection test সবুজ (কোনো function call/property access চলে না)।
- Template থেকে আসা Draft সাধারণ approval ও Post পথে যায়।

**Architecture constraints**
- `eval` ব্যবহার নিষেধ; নিজের parser।
- সব গণনা BCMath/DECIMAL(20,4), float নয়।
- Template সবসময় `JournalService::createDraft` দিয়ে Draft; `acc_journals.status` কখনো লেখে না।
- ব্যবহৃত version immutable।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P6-07 — Bulk journal import

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + job + api)
Title
Bulk journal import — Backend
Branch Name
`feature/accounting-journal-bulk-import`
View Order
83
Description
**Summary**
CSV বা xlsx থেকে অনেক journal staging-এ আসবে, যাচাই হবে, তারপর আংশিক বা পুরো commit — ফল শুধু Draft। বড় entry হাতে না বসিয়ে নিরাপদে ঢোকানো যাবে; auto-post হবে না।

**Task serial**
`ACC-P6-07 — Bulk journal import` · Feature · Phase 6, Build ক্রম 10

**Depends on**
- `ACC-P6-02` (type আচরণ, `import` type)
- `ACC-P6-10` (dimension যাচাই)
- `ACC-P2-03` (validator, `validateDraft(payload)` dry-run)
- `ACC-P1-12` (tenancy, audit, error)
- (অন্য card অনুযায়ী যোগ: `ACC-P6-05`; open point দেখুন)

**Relation: আগের task**
- `P6-02` দিয়েছে type hook (inactive, account range, approval rule ইত্যাদি)। `origin = import` হওয়ায় "manual নিষেধ" পরীক্ষা এড়ায়, কিন্তু বাকি hook চলে।
- `P6-10` দিয়েছে `DimensionService::validateLines`; import-এর `dim:<code>` কলাম এই যাচাইয়ে যায়।
- `P6-05` দিয়েছে `document_date` ও `JournalDateRule`; import কলাম `journal_date`/`document_date` সেই নিয়মে।
- `P2-03` দিয়েছে validator; import Draft-মোড validator-এর dry-run (`validateDraft(payload)`, কিছু লেখে না) ডাকে।
- `P1-12` দিয়েছে `AccountingError` ও audit কাঠামো।
- `P1-09` দিয়েছে `source_active` unique (এটা source-এ P2-03 বলা হয়েছে), যা এক group-এর দ্বিতীয় Draft ঠেকায়।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P6-14` (UI): import wizard (upload, ফাইল ত্রুটি, group তালিকা, সারির ত্রুটি, accept মোড, ফলাফল)।
- Phase 16 (data migration): বড় import এই কাঠামোর ওপর। (Phase 16 এই document-এ নেই, অন্য tab-এ)
- `P6-15`: `import` journal type সক্রিয় (`JournalTypeRegistry::activate('import', by: phase 6)`), যার পর import চালু হয়।

**Related tables / schema**

**`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)`।

Setting: `journal_import.max_rows` (default 5,000), `journal_import.accept_mode`, `attachment.max_bytes` (import-সীমা 20 MB)।

**Business rules**
- 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 আটকায় (`JTY_IMPORT_MODE_VIOLATION`)।
- 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-এ `=`/`+`/`-`/`@` আগে `'`।
- Auto-post কোথাও নেই (static ও runtime test)।
- **Open point:** `GET /journal-imports/sample-file?format=csv...` endpoint-এর Contract লাইন source-এ কাটা (ফরম্যাট মান সম্ভবত `csv|xlsx`, প্রতিটা কাটা অবস্থায় আছে)। নির্ধারণ করতে হবে সঠিক query মান।
- **Open point:** OD-27a (সীমা ও মোড) সিদ্ধান্ত লিখিত নেই। `P6-05` ও `P6-07`-এর নির্ভরতা: `P6-05` তার অবরুদ্ধ-তালিকায় `P6-07` রেখেছে কিন্তু এই card-এর "নির্ভর করে"-তে `P6-05` নেই।
- **Open point:** `error_codes JSON` এ কী ধরনের কোড যায় (`JTY_IMPORT_ROW_INVALID` ছাড়াও `DIM_*`, `JRN_*`) এবং সারির ত্রুটি বার্তার ভাষা source-এ বলা নেই।
- **Open point:** ZIP আনপ্যাক সীমা ও অনুপাত xlsx (যা ZIP কনটেইনার) এর জন্য; csv-র আলাদা zip নয়, তা ধরা হয়েছে।

**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 একই group-এর দ্বিতীয় Draft আটকায়। ব্যর্থ group `failed`, অন্যরা এগোয়। শেষে batch `completed` বা `partially_accepted`।
- **Lock ক্রম প্রতি group-এ:** group → journal (নতুন) → বাকি ledger lock আসে না (শুধু Draft)।

**Task scope**
1. Migration (৩ table)।
2. Upload ও ফাইল যাচাই (extension, MIME sniff, আকার, ZIP bomb সীমা, encoding)।
3. Parse job।
4. Validate (dry-run validator hook `P2-03`-এ `validateDraft`)।
5. Accept ও commit job।
6. Sample file (CSV ও xlsx) ও কলাম বর্ণনা।
7. Security test।
8. Benchmark (৫,০০০ সারি যাচাই ৬০ সেকেন্ডের মধ্যে)।

**Out of scope**
Auto-post। Phase 16-এর data migration/opening import। UI (`P6-14`)। `import` type সক্রিয় করা (`P6-15`)।

**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...` (source-এ কাটা, উপরের open point)

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

**Definition of done**
- সাধারণ DoD পূরণ।
- AC6.7, G6.7 সবুজ।
- Security test (xlsm, ZIP bomb, encoding) ও benchmark সংখ্যা লিখিত।
- কোড বিশ্লেষণে (static) auto-post নেই প্রমাণিত।

**Architecture constraints**
- Journal তৈরি শুধু `JournalService::createDraft`; status কখনো import code লেখে না।
- ফাইল webroot-এর বাইরে; সরাসরি ডাউনলোড নয়।
- প্রতি group আলাদা transaction; একটা ব্যর্থ হলে অন্যরা আটকায় না।
- `import` type সক্রিয় না হওয়া পর্যন্ত কেউ import চালাতে পারে না।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + job + api)
Title
Attachment policy, comment, tag, copy, clone, filter, search — Backend
Branch Name
`feature/accounting-journal-attachment-comment-search`
View Order
84
Description
**Summary**
সংযুক্তির নিয়ম ও malware scan, journal-এ comment/tag, copy/clone, saved filter আর search — সব থাকবে। প্রমাণপত্র নিরাপদ, journal খুঁজে পাওয়া ও সহযোগিতা সহজ; ledger বা `posted_hash` কোনোভাবে বদলাবে না।

**Task serial**
`ACC-P6-08 — Attachment policy, comment, tag, copy, clone, filter, search` · Feature · Phase 6, Build ক্রম 11

**Depends on**
- `ACC-P6-02` (type আচরণ ও attachment hook (চ))
- `ACC-P2-02` (Draft তৈরি ও সম্পাদনা, বিদ্যমান attachment, copy)
- `ACC-P2-03` (validator)
- `ACC-P0-02` (repo convention: file/upload নিয়ম, scanner বা ব্যবস্থা)
- `ACC-P1-12` (tenancy, audit, error)

**Relation: আগের task**
- `P6-02` দিয়েছে type-এর `attachment_rule`/`attachment_min_count` ও Submit/Post-এ attachment hook; এই task সেই hook-এর পেছনে `AttachmentPolicyService` বসায়।
- `P2-02` দিয়েছে `POST /{id}/attachments`, `DELETE /{id}/attachments/{aid}`, `POST /{id}/copy`; বিদ্যমান `acc_journal_attachments` (`P1-09`) অপরিবর্তিত থাকে, স্ক্যান অবস্থা আলাদা `acc_attachment_scans`-এ। (source: attachment table P2-02-তে যে নামে, Entry gate-এ মূল কোডে নিশ্চিত করা।)
- `P0-02` দিয়েছে ERPFlow-এর বিদ্যমান file upload নিয়ম ও notification/scanner সম্পর্কিত আবিষ্কার।
- `P2-03` দিয়েছে `JRN_ATTACHMENT_REQUIRED`-এর বিদ্যমান নিয়ম; এই task সেখানে scan অবস্থা যোগ করে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P6-14` (UI): সংযুক্তি scan অবস্থা, comment, tag, copy ও clone, saved filter ও search পর্দা।
- `P6-15` (enablement): runbook অধ্যায় "scan backlog"; UAT workflow।

**Related tables / schema**
সাতটা নতুন table। বিদ্যমান journal সংযুক্তি table (`acc_journal_attachments`) অপরিবর্তিত।

**`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।

Setting: `attachment.scan_required`, `attachment.max_bytes`, `attachment.allowed_ext`, `attachment.retention_years`, `journal.comment_edit_minutes`।

**Business rules**
- R6.7, R6.24, R6.25, R6.26।
- এগুলো ledger নয়, `posted_hash`-এর অংশ নয়।
- 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` পর্যন্ত মুছা যায় না (`JTY_ATTACH_RETAINED`)।
- 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।
- Tag/comment Posted journal-তে যোগ হয়।
- **Open point:** OD-27b (malware scanner) সিদ্ধান্ত নেই; scanner adapter interface বানাতে হবে, বাস্তব সংযোগ OD অনুযায়ী। Scanner ছাড়া `scan_required = true` হলে সবকিছু `pending` থাকবে (fail-closed), তাই সিদ্ধান্ত না হওয়া পর্যন্ত default `scan_required`-এর মান source-এ বলা নেই।
- **Open point:** `P2-02`-এ copy-তে "attachment কপি হয় না" কিন্তু এখানে clone-এ সংযুক্তির সূত্র থাকে; `POST /{id}/copy` (P2-02) আর এই card-এর `POST /journals/{id}/copy` একই endpoint কি না (path prefix `/accounting/journals` বনাম `/journals`) source-এ স্পষ্ট নয়, ধরা হয়েছে এই card বিদ্যমান copy-কে সম্প্রসারিত করে।
- **Open point:** Posted journal-এ সংযুক্তি যোগ করা যাবে কি না: `P1-10` trigger `acc_journal_attachments`-এ Posted journal-এর INSERT আটকায়, অথচ `P6-14` বলেছে Posted journal-এ সংযুক্তি যোগের বোতাম থাকে। Trigger হালনাগাদ লাগবে কি না source বলেনি।
- **Open point:** Search-এ `ngram` parser MySQL-এ আছে, কিন্তু `ft_acc_journals_narration` বিদ্যমান বড় table-এ online যোগ কীভাবে হবে source বলেনি (online DDL ধরা হয়েছে)।

**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 স্থানান্তর) বা scanner বন্ধে `pending` থেকে backoff।
- **Submit:** শুধু পড়া (`clean` গণনা)।
- **Post:** আবার পড়া, সাধারণ snapshot, কোনো নতুন lock নেই।
- **Comment/tag/filter:** নিজেদের table-এ INSERT।
- **Copy/clone:** `JournalService::createDraft` + `acc_journal_copy_links` INSERT, এক transaction।

**Task scope**
1. Migration (৭ table) + FULLTEXT index।
2. Policy resolver ও upload hook।
3. Scan job ও scanner adapter interface।
4. Submit/Post gate (`clean` গণনা)।
5. Download signed URL (60 সেকেন্ড)।
6. Comment, tag।
7. Copy, clone।
8. Saved filter ও search।
9. Test।

**Out of scope**
Scanner-এর বাস্তব সংযোগ (OD-27b)। UI (`P6-14`)। Type-স্তরের attachment-rule সংজ্ঞা (`P6-01/02`)।

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

**Definition of done**
- সাধারণ DoD পূরণ।
- AC6.13 সবুজ।
- `posted_hash` অপরিবর্তিত প্রমাণিত (comment/tag/সংযুক্তি যোগে)।
- Scanner বন্ধে fail-closed আচরণ প্রমাণিত।

**Architecture constraints**
- Fail-closed: scan সবুজ ছাড়া সংযুক্তি গণনা বা download নয়।
- Saved filter কখনো company/permission scope এড়ায় না।
- Download শুধু signed 60-সেকেন্ডের URL।
- Comment কখনো মোছা যায় না।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---
## ACC-P6-12 — Dimension reclassification

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service + api)
Title
Dimension reclassification — Backend
Branch Name
`feature/accounting-dimension-reclass`
View Order
85
Description
**Summary**
Posted ledger না ছুঁয়ে এক dimension value থেকে অন্যটিতে নিট balance সরানো যাবে — ভুল বা বদলানো বিভাজন ঠিক হবে, ইতিহাস অক্ষত। ফলাফল সাধারণ `adjustment` Draft; approval ও Post-এর স্বাভাবিক পথেই যাবে।

**Task serial**
`ACC-P6-12 — Dimension reclassification` · Feature · Phase 6, Build ক্রম 12

**Depends on**
- `ACC-P6-10` (dimension rule ও validation)
- `ACC-P3-01` (Ledger query layer, `balancesAsOf`)
- `ACC-P2-14` (Opening journal-এর Post; source এভাবে লিখেছে)
- `ACC-P2-03` (validator)

**Relation: আগের task**
- `P6-10` দিয়েছে `validateLines` (reclass Draft-এর dimension যাচাই) ও rule; reclass শুধু এই dimension বদলায়, বাকি dimension মূলের সাথে বহাল।
- `P3-01` দিয়েছে `LedgerQuery::balancesAsOf(company, asOf, dimension filter)`; নিট হিসাব ও `balance_version` এখান থেকে।
- `P2-14` (Opening journal-এর Post) source নির্ভরতায় রেখেছে; কেন, তা source-এ ব্যাখ্যা নেই (reclass `adjustment` type-এর Draft বানায়, যার Adjustment কাঠামো `P2-13`-এ)। নিচে open point দেখুন।
- `P2-03` দিয়েছে `SubmitValidator`, যেখানে এই task `DIM_RECLASS_STALE` যাচাই বসায়।
- `P6-09` ও `P6-02`-এর `adjustment` type: `allow_manual_entry = 0`, তবে reclass পথে (origin ভিন্ন) এই নিষেধ খাটে না।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P6-14` (UI): reclassification preview ও Draft পর্দা।
- `P6-15` (enablement): UAT workflow ও gate রিপোর্ট।

**Related tables / schema**

**`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)`।

**Business rules**
- R6.35, OD-29b (ক)।
- As-of তারিখ open period-এ; বন্ধ হলে `JRN_PERIOD_LOCKED`।
- From ও to একই dimension, from ≠ to। From-এ নিট শূন্য হলে `DIM_RECLASS_EMPTY`।
- একই account, নিট শূন্য journal (account-ভিত্তিক Debit ও Credit সমান পরিমাণ)।
- Draft-এ প্রতি line-এর আর সব dimension মূলের সাথে বহাল (শুধু এই dimension বদল)।
- ঐচ্ছিক account তালিকা (`account_filter`; বাকিগুলো বাদ)।
- Reclass করা ব্যক্তি সেই journal Post করতে পারে না (SoD)।
- পুরোনো posted line কখনো বদলায় না।
- Submit-এ `SubmitValidator` ledger আবার হিসাব করে `snapshot_hash` তুলনা করে; অমিল হলে `DIM_RECLASS_STALE` ও request `superseded`।
- `snapshot_hash` = SHA-256(account id, নিট, `balance_version` তালিকা)।
- Preview কিছু লেখে না ledger-এ (শুধু request সারি `previewed`)।
- **Open point:** OD-29b (reclassification-এর ধরন) লিখিত সিদ্ধান্ত নেই; baseline (ক) ধরা, যার বর্ণনা শুধু "নিট balance সরানো, `adjustment` Draft"।
- **Open point:** "একই account, নিট শূন্য journal" বাক্যটি পরিষ্কার নয়: প্রতি account-এ দুটো line (to value-তে নিট, from value-তে উল্টো) তৈরি হয় বলে journal-এর মোট Debit = Credit ও account-ভিত্তিক নিট শূন্য, এই অর্থ ধরা হয়েছে।
- **Open point:** `source_type = dimension_reclass` একটা নতুন `source_type` মান; `acc_journals.source_type` নিয়ন্ত্রণ করে এমন কোনো registry/CHECK আছে কি না source বলেনি (`P1-09`-এ শুধু VARCHAR)।
- **Open point:** `snapshot_hash`-এ কোন `balance_version` (`P3-02`-এর `acc_period_balances`-এর না `P6-13`-এর `acc_dimension_balances`-এর) যায়, তা source-এ সুনির্দিষ্ট নয়।
- **Open point:** নির্ভরতায় `P2-14` কেন আছে (বরং `P2-13` Reversal ও Adjustment হওয়ার কথা) source ব্যাখ্যা করেনি; tech lead নিশ্চিত করবে এটা `P2-13` কিনা।

**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 ক্রম অপরিবর্তিত।

**Task scope**
1. Migration (`acc_dimension_reclass_requests`)।
2. Balance হিসাব ও snapshot hash।
3. Draft তৈরি (`createDraft`)।
4. Submit-এ stale যাচাই (validator hook)।
5. API।
6. Test (`DimensionReclassTest`)।

**Out of scope**
Posted ledger সরাসরি বদল (কখনো নয়)। UI (`P6-14`)। Dimension rule (`P6-10`)।

**Contract (API)**
- Service: `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

**Definition of done**
- সাধারণ DoD পূরণ।
- AC6.10, G6.6 সবুজ।
- Preview ledger-এ কিছু লেখে না প্রমাণিত।
- Reclass Draft সাধারণ approval ও Post পথে, SoD সহ।

**Architecture constraints**
- Posted ledger কখনো বদলায় না; সংশোধন শুধু নতুন `adjustment` Draft।
- Draft সবসময় `JournalService::createDraft`; status শুধু `PostingService`।
- Preview ও stale যাচাইয়ে কোনো lock নয়।
- একটা request-এ একটা সক্রিয় Draft (DB unique)।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service)
Title
Report filter, group-by, cache, index, security hook — Backend
Branch Name
`feature/accounting-dimension-report-filter`
View Order
86
Description
**Summary**
Trial Balance, GL, P&L ও Balance Sheet-এ dimension filter ও group-by (derived cache সহ) আর `DimensionAccessGuard` hook থাকবে — বিশ্লেষণ দ্রুত ও নির্ভুল; পরে dimension ধরে data-নিরাপত্তা বসানো যাবে।

**Task serial**
`ACC-P6-13 — Report filter, group-by, cache, index, security hook` · Feature · Phase 6, Build ক্রম 13

**Depends on**
- `ACC-P6-09` (dimension কাঠামো, `idx_ald_value`)
- `ACC-P6-10` (rule/balancing; RC1d)
- `ACC-P3-01` (Ledger query layer)
- `ACC-P3-03`, `ACC-P3-04`, `ACC-P3-05`, `ACC-P3-06` (General Ledger, Trial Balance, P&L, Balance Sheet; source "P3-01 থেকে P3-06" লিখেছে)
- `ACC-P3-13` (Reconciliation framework)

**Relation: আগের task**
- `P6-09` দিয়েছে `acc_journal_line_dimensions` ও তার `idx_ald_value (company_id, dimension_id, value_id, journal_line_id)`, যা এই task-এর filter query-র ভিত্তি।
- `P6-10` দিয়েছে `is_balancing` dimension ও balancing নিয়ম, যার ওপর `RC1d` দাঁড়ায়।
- `P3-01` দিয়েছে `LedgerQueryService` (`balancesAsOf`, `movements`, `accountRows`, `netProfit`); এই task সেখানে `->withDimensions(filters)->groupByDimension(codes)` যোগ করে।
- `P3-03` থেকে `P3-06`: GL, TB, P&L, BS endpoint যেগুলোয় ঐচ্ছিক parameter যোগ হয়।
- `P3-02` (Derived balance cache, `balance_version`): `acc_dimension_balances` তার `balance_version` রীতি অনুসরণ করে (source "P3-02-এর মত" বলেছে, যদিও নির্ভরতায় নেই)।
- `P3-13` দিয়েছে RC framework (`acc_reconciliation_runs.check_code VARCHAR(8)`); `RC1m (গ)` (cache = ledger) এই task-এর cache-এর পরীক্ষা।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P6-14` (UI): Report পর্দায় dimension filter ও group-by।
- `P6-15` (enablement): `dimensions.enabled` চালুর শর্ত "P6-13 benchmark সংখ্যা লিখিত"; RC1m (গ) cache = ledger।
- Phase 13 (data-নিরাপত্তা বিধি, OD-29a): `DimensionAccessGuard` hook-এর বাস্তব নিয়ম সেখানে বসবে। (Phase 13 এই document-এ নেই, অন্য tab-এ)

**Related tables / schema**

**`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)`।

বিদ্যমান `LedgerQuery`-র query বদল; নতুন table নয় (একটা ছাড়া)।

**Business rules**
- 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।
- Response সারি: `{dimension_values: {code: {id, code, name}}, debit, credit, balance}`, সবসময় `unassigned` সারি আলাদা।
- `DimensionAccessGuard::allowedValues(user, dimension): ValueSet` hook। Guard দিয়ে value সীমিত করলে সারি লুকায়, মোট আলাদা চিহ্নিত।
- **Open point:** Source-এর Business rules ঘরের শেষাংশ কাটা (`DimensionAccessGuard::allowedValues(user, dimension): ValueSet` এর পরের বাক্য নেই)। Guard-এর default আচরণ (কোনো বিধি না থাকলে সব value খোলা) ও কোন report-এ প্রয়োগ হবে তা কাটা অংশে ছিল কি না জানা নেই, তাই default "সব value অনুমোদিত" ধরা হয়েছে, নিশ্চিত নয়।
- **Open point:** OD-29c (projection table: লক্ষ্য না মিললে OD-29c (খ) সিদ্ধান্ত নথিভুক্ত) ও OD-29a (data security বিধি, Phase 13-এ) লিখিত নেই।
- **Open point:** `DIM_REPORT_UNAVAILABLE` (503) কখন ঠিক ছোড়ে: "flag বন্ধ বা backfill অসম্পূর্ণ", কিন্তু backfill-সম্পূর্ণ ফ্ল্যাগ কোথায় সংরক্ষিত (`acc_dimension_backfill_state`) সেটা সুনির্দিষ্ট নয়।

**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।

**Task scope**
1. `acc_dimension_balances` migration।
2. `LedgerQuery` filter ও group-by।
3. Endpoint parameter ও validation (TB, GL, P&L, BS)।
4. Listener ও rebuild job।
5. `DimensionAccessGuard` interface ও hook।
6. Index যাচাই (`EXPLAIN`) ও benchmark।
7. Test।

**Out of scope**
Guard-এর আসল data-security বিধি (Phase 13, OD-29a)। UI (`P6-14`)। Dimension কাঠামো/rule (`P6-09`, `P6-10`)।

**Contract (API)**
- `LedgerQuery`-এ `->withDimensions(filters)->groupByDimension(codes)`।
- বিদ্যমান endpoint: `P3-03` থেকে `P3-06`-এর TB, GL, P&L, BS-এ ঐচ্ছিক parameter (`dim[<code>]`, `dim_descendants`, `dim_unassigned`, `group_by`)।
- উত্তরে সারি: `{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

**Definition of done**
- সাধারণ DoD পূরণ।
- AC6.12, G6.10 সবুজ।
- Benchmark রিপোর্টে সংখ্যা লিখিত (লক্ষ্য না মিললে OD-29c (খ) নথিভুক্ত)।
- `EXPLAIN` প্রমাণ: প্রতি filter query-তে `idx_ald_value`।

**Architecture constraints**
- সত্যের উৎস সবসময় posted line; cache কখনো সত্য নয়।
- Query-তে `company_id` সবার আগে।
- Cache লেখা Post-কে ধীর বা ব্যর্থ করতে পারে না (afterCommit, retry)।
- `value_id = 0` জেনেশুনে FK-হীন; RC1m (গ) সেটা ধরে।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P6-14 — Admin ও journal form UI

Project
ERPFlow
Module
Accounting Module
Type
Feature (ui)
Title
Admin ও journal form UI — Frontend
Branch Name
`feature/accounting-journal-mgmt-dimension-ui`
View Order
87
Description
**Summary**
Phase 6-এর সব ক্ষমতা (type, series, template, import, dimension, rule, form, search, reclass, report filter) — এর জন্য UI পর্দা থাকবে। API-র শক্তি ব্যবহারকারীর কাছে বোধগম্য ও নিরাপদ; আসল বাধা সবসময় সার্ভারেই।

**Task serial**
`ACC-P6-14 — Admin ও journal form UI` · Feature (UI) · Phase 6, Build ক্রম 14

**Depends on**
- `ACC-P6-02` থেকে `ACC-P6-13`-এর API (type, series, voucher dry-run, date, template, import, সংযুক্তি/comment/search, dimension, rule, mirror, reclass, report filter)। Source-এর ভাষায়: "P6-02 থেকে P6-13-এর API"। `P6-01` ও `P6-04`-এর শুধু dry-run API আছে।

**Relation: আগের task**
সব backend card UI-র API দিয়েছে। বিশেষ করে: `P6-02` (type API), `P6-03` (series API ও preview, allocation), `P6-04` (cutover dry-run), `P6-05` (`document_date` ও `warnings[]`), `P6-06` (template API), `P6-07` (import API), `P6-08` (comment, tag, copy/clone, filter, search, download), `P6-09`/`P6-11` (dimension, value, backfill, health, mirror), `P6-10` (rule set ও dry-run), `P6-12` (reclass), `P6-13` (report filter)। UI এদের `field`-ভিত্তিক ত্রুটি ও permission সরাসরি ব্যবহার করে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P6-15` (enablement): E2E ও UAT script এই UI দিয়ে চলে; error catalog-এ `DIM_*`, `JTY_*`, `NSR_*`-এর বাংলা বার্তা এই card জমা দেয়।

**Related tables / schema**
নেই (শুধু API ডাকে)। কোনো DB write সরাসরি নয়।

**Business rules**
- প্রতি পর্দা ফিচার flag বন্ধ থাকলে লুকানো (route guard ও menu)।
- Permission ছাড়া বোতাম নেই, কিন্তু সার্ভারের বাধাই আসল।
- সব ত্রুটি API-র `field` ধরে ফর্মের জায়গায় দেখানো।
- বাংলা ও ইংরেজি label। অ্যাক্সেসযোগ্য (কিবোর্ড, ARIA)।
- Posted journal-এ comment, tag, সংযুক্তি যোগের বোতাম থাকে, সম্পাদনা নেই।
- বড় তালিকা cursor pagination।
- 500 line-এর form ধীর হয় না (virtualized)।
- **Open point:** Posted journal-এ সংযুক্তি যোগের বোতাম আছে বলা হয়েছে, কিন্তু `P1-10` trigger Posted journal-এর `acc_journal_attachments` INSERT আটকায় (`P6-08` open point); UI এই বোতাম কোন API-তে বাঁধবে সিদ্ধান্ত লাগবে।
- **Open point:** Source বলেছে "OD: নেই (সব OD-র baseline ধরে)"; OD বদলালে কোন পর্দা বদলাবে তা বলা নেই।

**DB flow**
নেই (শুধু API ডাকে)।

**Task scope**
**পর্দা:**
1. Journal type তালিকা ও সম্পাদনা, account range editor।
2. Series তালিকা, format preview, assignment, allocation দেখা, cutover dry-run ফলাফল।
3. Template তালিকা, সম্পাদক (expression ও parameter), সম্পাদনার অনুরোধের অবস্থা, Draft তৈরির পর্দা।
4. Import wizard: upload, ফাইল ত্রুটি, group তালিকা, সারির ত্রুটি, accept মোড, ফলাফল।
5. Dimension admin: dimension, value গাছ, mirror অবস্থা, backfill অগ্রগতি, health।
6. Rule set editor ও dry-run।
7. Journal form: document date, প্রতি line-এ dimension picker (mandatory চিহ্ন, default আগে থেকে বসা, inactive ধূসর), সংযুক্তি scan অবস্থা, comment, tag, copy ও clone।
8. Saved filter ও search।
9. Reclassification preview ও Draft।
10. Report পর্দায় dimension filter ও group-by।

**Out of scope**
Backend লজিক। Error code সংজ্ঞা (প্রতিটা backend card)। Flag চালু (`P6-15`)।

**Contract (API)**
UI কোনো নতুন API দেয় না; উপরের card-গুলোর API ব্যবহার করে।

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

**Definition of done**
- সাধারণ DoD (UI-র জন্য প্রযোজ্য অংশ) পূরণ: Feature test, permission-ভিত্তিক দৃশ্যমানতা, flag বন্ধে menu/route নেই।
- AC6.2 থেকে AC6.13-এর UI অংশ সবুজ; E2E সবুজ।
- সব `DIM_*`, `JTY_*`, `NSR_*` কোডের বাংলা বার্তা আছে।

**Architecture constraints**
- UI কখনো business নিয়ম আলাদা করে বানায় না; সার্ভারের বাধাই আসল।
- Flag বন্ধে route guard ও menu দুটোই।
- বড় তালিকায় cursor pagination, form-এ virtualization।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P6-15 — Enablement, RC sub-check, runbook, UAT

Project
ERPFlow
Module
Accounting Module
Type
Release (`release`)
Title
Enablement, RC sub-check, runbook, UAT
Branch Name
`feature/accounting-journal-mgmt-enablement`
View Order
88
Description
**Summary**
দুটো capability flag (`journal_mgmt.enabled`, `dimensions.enabled`) — সব precondition সবুজ হলেই চালু হবে। RC sub-check তিনটা, runbook, UAT, benchmark ও gate রিপোর্ট প্রস্তুত থাকবে — Phase 6 নিরাপদে চালু, Phase 7 Entry gate পার।

**Task serial**
`ACC-P6-15 — Enablement, RC sub-check, runbook, UAT` · Release · Phase 6, Build ক্রম 15

**Depends on**
- `ACC-P6-01` থেকে `ACC-P6-14` সব

**Relation: আগের task**
- `P6-01`: 16 type seed, orphan 0 (S11 শর্ত); `import` type এখানে সক্রিয় হয়।
- `P6-03`/`P6-04`: series, সম্পন্ন cutover, dry-run অমিল 0 (S12), RC1n এর ভিত্তি, runbook (cutover, rollback, নম্বরে গর্ত সন্দেহ)।
- `P6-07`/`P6-08`/`P6-11`/`P6-12`/`P6-13`: runbook অধ্যায় (scan backlog, mirror stale, dimension backfill) ও benchmark।
- `P6-09`/`P6-10`: backfill, RC1m, RC1d ভিত্তি।
- `P6-14`: UI, যার ওপর E2E ও UAT script (WF-A থেকে WF-G) চলে।
- `P5-06` (`acc_enablement_log`), `P1-05` (checklist), `P1-04` (Maker-Checker) ও `P3-13` (RC framework): এই task এদের ব্যবহার ও সম্প্রসারণ করে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- Phase 7 শুরু (Entry gate): Phase 7 (Multi-Currency ও FX) এই card সবুজ না হলে শুরু হয় না। (Phase 7 এই document-এ নেই, অন্য tab-এ)
- Phase 7-17 এর সব card (P17 সহ): `ACC-P17-01` নির্ভর করে "Phase 6 থেকে 16-এর প্রতিটা exit gate" সবুজ হওয়ার ওপর, যার মধ্যে এই card।

**Related tables / schema**
নতুন table নেই। `acc_enablement_log`-এ (`P5-06`/`P1-05` সূত্রে) দুটো নতুন সারি-ধরন (`journal_mgmt` ও `dimensions` flag বদল ও cutover/rollback)।
- Setup Checklist: নতুন ধাপ **S11**, **S12** (নাম source-এর)।
- RC sub-check: `RC1d`, `RC1m`, `RC1n` (P3-13 framework-এ নিবন্ধন)।
- `acc_alert_state` (`P5-03`): RC fail হলে alert।

**Business rules**
- **`journal_mgmt.enabled` চালুর শর্ত (সব সবুজ):**
  1. S11 (সব company-তে 16 type seed, orphan 0)।
  2. S12 (cutover dry-run অমিল 0, সম্পন্ন cutover)।
  3. RC1n সবুজ।
  4. প্রতি active journal type-এ কমপক্ষে একটা series assignment বা default series।
  5. Production-copy-তে cutover রিহার্সাল সফল।
  6. `P1-10` trigger হালনাগাদ প্রমাণ।
  7. `P2-03`, `P2-11`, `P2-12` বদল merge।
- **`dimensions.enabled` চালুর শর্ত:**
  1. Backfill সম্পূর্ণ, RC1m সবুজ।
  2. Mirror প্রথম sync সফল।
  3. Rule conflict শূন্য; `dimension.default_requirement` dry-run-এ ব্যর্থ Draft তালিকা পর্যালোচিত।
  4. RC1d সবুজ (balancing চালু থাকলে)।
  5. `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`।
- `import` type সক্রিয়: `JournalTypeRegistry::activate('import', by: phase 6)`।
- **Open point (সংঘাত):** `P1-05` ইতিমধ্যে `S11_cashflow_mapping` সংজ্ঞায়িত করেছে (আর `I01..I03` আলাদা group)। এই card-এর S11 ("type seed") ও S12 ("cutover") নাম সংঘাত করে। নতুন ধাপের আসল ID (যেমন `S12_journal_types`, `S13_numbering_cutover`) tech lead ঠিক করবে, নয়তো `P1-05` এর `S01..S10` সবুজ শর্তের সাথে এগুলোর সম্পর্ক ভুল বোঝা যাবে।
- **Open point:** `P3-13`-এর `check_code VARCHAR(8)` ও "প্রতিটা run-এ ৯টা row" নিয়ম RC1d/RC1m/RC1n-এর জন্য কীভাবে বাড়ে (run-প্রতি সারি সংখ্যা ৯ থেকে ১২) এবং RC1m-এর (ক/খ/গ) তিন উপ-অংশ এক সারি না তিন সারি, source-এ বলা নেই।
- **Open point:** "সব OD-24a থেকে OD-29c-র baseline লিখিত সম্মতি" কে দেবে (accountant/Finance owner) source-এ বলা নেই। UAT workflow WF-A থেকে WF-G-এর সংজ্ঞা এই document-এ নেই।
- **Open point:** `acc_enablement_log.flag VARCHAR(60)`-এর বৈধ মান `P5-06`-এ তিনটা flag ও তিনটা gate; এখানে নতুন মান (`journal_mgmt.enabled`, `dimensions.enabled`) যোগ হবে, কিন্তু এই log-এ সারির `action` মান নতুন কিছু লাগবে কি (cutover, rollback) source-এ স্পষ্ট নয় (`P6-04` cutover সারি লেখে)।

**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)`।

**Task scope**
1. S11, S12 checklist (`P1-05`) নতুন ধাপ।
2. Flag precondition (দুই flag)।
3. RC1d, RC1m, RC1n।
4. Static test।
5. Runbook।
6. UAT ও rehearsal।
7. Benchmark।
8. Gate রিপোর্ট।

**Out of scope**
Phase 7 কাজ। Business নিয়ম (সংশ্লিষ্ট card-এ)। Production-এ আসল চালু (`P5-08`-ধরনের rollout নিয়ম)।

**Contract (API)**
- Checklist ধাপ S11, S12 (`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 (`accounting.reconcile.run`)।

**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 রিপোর্ট

**Definition of done**
- সাধারণ DoD পূরণ।
- AC6.14 ও AC6.1 থেকে AC6.13 সব সবুজ।
- দুই flag-এর precondition ও `SET_INCOMPLETE` তালিকা প্রমাণিত; flag বন্ধে Phase 5 suite সবুজ।
- Gate রিপোর্ট জমা; OD baseline-এর লিখিত সম্মতি আছে; Phase 7 Entry gate পার।

**Architecture constraints**
- Flag চালু শুধু Maker-Checker পথে, `ChangeApplier` এক transaction-এ precondition আবার যাচাই করে।
- RC sub-check শুধু পড়া ও snapshot isolation।
- `journal_mgmt` বন্ধ করতে rollback command আগে।
- Phase 1-5 card-এ বদলের merge প্রমাণ বাধ্যতামূলক।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

<!-- ===== END: ACC-Phase-6-Tasks.md ===== -->


<!-- ===== BEGIN: ACC-Phase-8-Tasks.md ===== -->

# Accounting Module — Phase 8 Tasks (Tax Management — কর ব্যবস্থাপনা)

> এখানকার প্রতিটা task আলাদা করে একজন কাজ করতে পারবে।
> এই document-এর P8-01 থেকে P8-05 শেষ হলে থাকবে: কর-কর্তৃপক্ষ, registration, return box, tax code ও তারিখ-ভিত্তিক rate, group, account map, আর একটাই গণনা engine (`TaxService`)। `tax.enabled` বন্ধ থাকলে আচরণ Phase 7 শেষের মতোই — tax table খালি, কেউ পড়ে না।
> কাজের ক্রম: jurisdiction ও box → tax code ও rate → group → account map → গণনা engine।
> Estimate, Owner, Sprint ইচ্ছা করে ফাঁকা রাখা হয়েছে।
> নোট: Phase 8-এ মোট P8-01 থেকে P8-18 আছে; এই document-এ শুধু P8-01–P8-05। P8-06–P8-18, Phase 8-এর Registry যোগ ও Build ক্রমের সারি আলাদা উপ-tab-এ; Phase 7 (Multi-Currency ও FX) অন্য tab-এ — তাই সেগুলো এখানে নেই।

## সব Phase 8 task-এ প্রযোজ্য সাধারণ নিয়ম (প্রতিটা task নিজের DoD-তে এগুলো ধরে নেয়)

- Database: MySQL, InnoDB, `utf8mb4`। সব নতুন table-এর prefix `acc_tax_`; ব্যতিক্রম shared table `acc_journal_line_taxes` (Phase 8-এর অন্য card-এ, এই document-এ নেই)। Phase 1-এর সাধারণ নিয়ম (PK ধরন `ACC-P0-02`-এর Convention Sheet অনুযায়ী, `company_id NOT NULL`, composite FK `(company_id, x_id)` ও `uq (company_id, id)`, `version INT` দিয়ে optimistic check, `ON DELETE RESTRICT`) Phase 8-এও খাটে।
- Capability flag `tax.enabled` (default বন্ধ)। বন্ধ থাকলে আচরণ Phase 7 শেষের মতোই; table খালি ও কেউ পড়ে না।
- **সব write API `Idempotency-Key` নেয়।** সব job `AccountingJob` base ধরে।
- **Journal তৈরি হয় শুধু `JournalService::createDraft`-এ; status বদলায় শুধু `PostingService`।** Tax গণনা কখনো journal বা ledger-এ লেখে না (`TaxService` pure)।
- **কোডে কোনো কর-account বা rate বাঁধা নেই।** সব configuration (code, rate, group, account map) তারিখ-ভিত্তিক; পুরোনো লেনদেনে কখনো পরিবর্তন হয় না (posted line-এ `account_id` সরাসরি লেখা, rate snapshot সংরক্ষিত)।
- **সংবেদনশীল বদল Maker-Checker:** rate সক্রিয় (`taxrate:` prefix), account map (`taxmap:` prefix) `P1-04`-এর change request ও `ChangeApplier` দিয়ে। প্রস্তাবকারী নিজে অনুমোদন করতে পারে না।
- **প্রতিটা configuration বদলের ইতিহাস** `acc_tax_config_history` (append-only, `P8-02`)-এ।
- **Permission:** `accounting.tax.view` (পড়া), `accounting.tax.manage` (তৈরি ও বদল), `accounting.tax.approve` (দ্বিতীয় অনুমোদন)।
- **Error আকার:** `{ "code", "message", "field", "details", "correlation_id" }` (`AccountingError`)। Error prefix `TAX_*`।
- **DoD-র সাধারণ অংশ:** Service–Repository architecture (interface সহ), Form Request, API Resource; `company_id` tenant scope + IDOR test; Feature test (happy path, validation, unauthorized/forbidden); audit event `AccountingAudit` দিয়ে; error `AccountingError` enum-এর code দিয়ে; `tax.enabled` বন্ধ রেখে merge নিরাপদ; বিদ্যমান ERPFlow ও Phase 1-7 regression সবুজ।

**Phase-স্তরের open point (একাধিক card প্রভাবিত):**

- OD-37, OD-39a, OD-39b, OD-40, OD-41, OD-42, OD-44b এবং AC8.x, G8.x, R8.x নম্বরের সংজ্ঞা এই document-এ নেই। প্রতিটা card শুধু নম্বর ও card-এ লেখা baseline ধরে চলে। একই `OD-41` `P8-01`-এ "return period ও frequency" আর `P8-02`-এ "tax point তারিখ" নামে এসেছে (দুই নামে একই নম্বর); কোনটা ঠিক, নিশ্চিত করতে হবে।
- `P8-02` ও `P8-03` ইত্যাদি card `acc_tax_determination_rules`, `acc_journal_line_taxes`, `tax_date`-এর ওপর নির্ভর করে, যেগুলোর table `P8-06` বা পরের card-এ তৈরি (এই document-এ নেই)।
- Settings key `tax.rounding_mode`, `tax.default_price_basis` card-এ উল্লেখ আছে, কিন্তু সংজ্ঞা (মান, default, lock) Phase 8-এর Registry-তে, যা এই document-এ নেই।

**Phase 8 নির্ভরতা-ছবি (এই document-এর P8-01 থেকে P8-05; বাইরের নির্ভরতা বন্ধনীতে):**

```
P1-04,P1-10,P2-03 ─► P8-01 ─┬─► P8-02 ─┬─► P8-03 ──┐
                            │          │           │
                            │          └─► P8-04 ──┼─► P8-05 ─► P8-06..P8-09, P8-17 (অন্য tab)
                            └──────────────► P8-04 ┘      ▲
       P1-04 + CoA (acc_accounts) ───────────────┘          └── P7 (FxService::convert)
P8-01 ─► P8-11, P8-16 (অন্য tab)   P8-02 ─► P8-06, P8-07, P8-11 (অন্য tab)   P8-04 ─► P8-13, P8-16 (অন্য tab)
```

---

## ACC-P8-01 — Jurisdiction, registration ও return box schema

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema)
Title
Jurisdiction, registration ও return box schema — Backend
Branch Name
`feature/accounting-tax-jurisdiction-schema`
View Order
89
Description
**Summary**
কর-কর্তৃপক্ষ, company registration আর return-এর box — এগুলো কাঠামো মতো DB-তে থাকতে হবে; পরের সব tax কাজ (code, rate, return) এই ভিত্তির ওপর দাঁড়াবে। কোনো দেশের নিয়ম আগে থেকে table-এ seed করা যাবে না।

**Task serial**
`ACC-P8-01 — Jurisdiction, registration ও return box schema` · Feature · Phase 8, প্রথম card

**Depends on**
- `ACC-P1-04` (change request / দ্বিতীয় অনুমোদন)
- `ACC-P1-10` (DB guard ও append-only helper)
- `ACC-P2-03` (Journal validator; source নির্ভরতায় রেখেছে, কারণ এই card-এ ব্যাখ্যা নেই)

**Relation: আগের task**
- `P1-04` দিয়েছে দ্বিতীয় অনুমোদনের কাঠামো (Deactivate ও box বদলে `accounting.tax.approve` ধরনের Maker-Checker)।
- `P1-10` দিয়েছে `Schema::appendOnly()` helper; এই card তা ব্যবহার করে না (নিচে ব্যাখ্যা), কিন্তু ইতিহাস `P8-02`-র append-only `acc_tax_config_history`-তে যায়।
- `P2-03` দিয়েছে validator; Phase 8-এর পরের card-এ tax line যাচাইয়ের জন্য লাগবে, এই card-এ সরাসরি ব্যবহার নেই।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P8-02` (tax code, rate): `acc_tax_codes.jurisdiction_id` FK `acc_tax_jurisdictions`-এ; `box_code_base`/`box_code_tax` `acc_tax_return_boxes`-এর `box_code`-এ।
- `P8-04` (account map): `acc_tax_account_map.jurisdiction_id` FK এখানে; map বদলে jurisdiction সারি `FOR UPDATE`।
- `P8-03` (group): source এই card-কে `P8-03`-এর নির্ভরতায় রাখেনি, কিন্তু `acc_tax_groups.jurisdiction_id` এই table-এ FK এবং member code একই jurisdiction হতে হয়।
- `P8-11`, `P8-16` (অন্য tab-এ, এই document-এ নেই): `P8-11` return box ব্যবহার করে বলে ধরা যায়; `P8-16`-এ উদাহরণ import file (Bangladesh VAT, TDS ইত্যাদি) এই table-এ import হয়।
- Phase 9 (party data): `TaxRegistrationValidator` hook ব্যবহার করবে। (Phase 9 অন্য tab-এ)

**Related tables / schema**

**`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)`।

**Business rules**
- R8.1, R8.27, R8.38, R8.41।
- কিছুই seed হয় না। Bangladesh VAT বা TDS-এর মতো উদাহরণ import file হিসেবে থাকে (`P8-16`), table-এ আগে থেকে বসানো থাকে না।
- `country_code` শুধু তথ্য, কোনো logic-এ ব্যবহার নয়।
- `tax_id_pattern` শুধু format যাচাই (regex); registration-এর সত্যতা যাচাই নয়।
- `rounding_mode` NULL হলে `tax.rounding_mode` setting।
- jurisdiction প্রতি একটাই primary registration (generated unique)।
- ব্যবহৃত jurisdiction deactivate আটকায় (`TAX_JURISDICTION_IN_USE`)।
- Deactivate ও box বদলে দ্বিতীয় অনুমোদন (`accounting.tax.approve`)।
- ইতিহাস `acc_tax_config_history`-তে (`P8-02`), এই table-গুলো নিজে বদলায় (`Schema::appendOnly()` লাগে না)।
- **Open point:** DB flow-এ runtime write `acc_tax_config_history`-তে INSERT করে, কিন্তু সেই table `P8-02`-এ তৈরি (এবং `P8-02` এই card-এর ওপর নির্ভরশীল)। অর্থাৎ এই card-এ history লেখা সম্ভব নয় যতক্ষণ না `P8-02`-এর history migration আগে বা একই সাথে যায়। সমাধান (history table এই card-এ আনা, না history লেখা `P8-02`-এ বিলম্বিত) tech lead ঠিক করবে।
- **Open point:** Deactivate ও box বদলে "দ্বিতীয় অনুমোদন" কোন `ChangeApplier` prefix দিয়ে হবে (যেমন `taxrate:`/`taxmap:` আছে) source বলেনি। Registration বদলে (`PUT /tax/registrations/{id}`) অনুমোদন লাগে কি না তাও স্পষ্ট নয়।
- **Open point:** OD-37 (কোন jurisdiction আগে), OD-41 (return period ও frequency) লিখিত সিদ্ধান্ত নেই; `filing_frequency` বদল কী প্রভাব ফেলে (পুরোনো return) বলা নেই।
- **Open point:** `is_primary` "generated unique" ঠিক কীভাবে (nullable generated column যা non-primary সারিতে NULL) source-এ বলা নেই; Phase 6-এর `source_active` রীতি অনুসরণ করা যুক্তিসংগত।

**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`।

**Task scope**
1. Migration ও model: তিন table।
2. Service ও `version` check (`TaxJurisdictionService`)।
3. Registration ও pattern validator (`TaxRegistrationValidator`)।
4. Box কাঠামো service (পুরো তালিকা `version` সহ বদল)।
5. API ও permission।
6. Test।

**Out of scope**
Tax code, rate, group (`P8-02`, `P8-03`)। Account map (`P8-04`)। কোনো jurisdiction/box seed বা উদাহরণ import (`P8-16`, অন্য tab)। Return তৈরি (পরের card)।

**Contract (API)**
- Service: `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

**Definition of done**
- সাধারণ DoD পূরণ।
- AC8.1 সবুজ।
- `tax.enabled` বন্ধ রেখে merge নিরাপদ; কোনো seed নেই প্রমাণিত।
- অন্য company-র id composite FK-তে সরাসরি SQL-এ আটকায় (test প্রমাণ)।

**Architecture constraints**
- কোনো দেশ বা কর-নিয়ম table-এ আগে থেকে seed করা যাবে না।
- Composite FK নিয়ম; `uq (company_id, id)` সব table-এ।
- Ledger lock নয়; jurisdiction `FOR UPDATE` শুধু নিজের table-এ।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P8-02 — Tax code, rate ও config history

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service)
Title
Tax code, rate ও config history — Backend
Branch Name
`feature/accounting-tax-code-rate`
View Order
90
Description
**Summary**
সাত ধরনের tax code আর তারিখ-ভিত্তিক rate লাগবে; rate বদল এমনভাবে হবে যে পুরোনো লেনদেন কখনো বদলাবে না। প্রতিটি configuration বদল append-only ইতিহাসে (`acc_tax_config_history`) — অডিটে দেখা যাবে কোন তারিখে কোন rate প্রযোজ্য ছিল।

**Task serial**
`ACC-P8-02 — Tax code, rate ও config history` · Feature · Phase 8, দ্বিতীয় card

**Depends on**
- `ACC-P8-01` (jurisdiction ও return box)
- `ACC-P1-04` (change request, prefix `taxrate:`)
- `ACC-P1-10` (DB guard trigger ও `Schema::appendOnly()`)

**Relation: আগের task**
- `P8-01` দিয়েছে `acc_tax_jurisdictions` (`acc_tax_codes.jurisdiction_id` FK) ও `acc_tax_return_boxes` (code-এর `box_code_base`, `box_code_tax`)। 
- `P1-04` দিয়েছে change request ও `ChangeApplier` রেজিস্ট্রি; এই card `taxrate:` prefix নিবন্ধন করে।
- `P1-10` দিয়েছে trigger কাঠামো ও `Schema::appendOnly()`; এই card `acc_tax_rates`-এ বিশেষ trigger বসায় এবং `acc_tax_config_history`-কে append-only করে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P8-03` (group): member হিসেবে tax code ও তার rate resolver ব্যবহার করে।
- `P8-04` (account map): `acc_tax_account_map.tax_code_id` FK `acc_tax_codes`-এ।
- `P8-05` (গণনা engine): `TaxRateResolver::resolve` ও `rate_epoch` cache ব্যবহার করে।
- `P8-06`, `P8-07`, `P8-11` (অন্য tab-এ, এই document-এ নেই): source এদের এই card-এর ওপর নির্ভরশীল বলেছে।
- সব পরের Phase 8 card: `acc_tax_config_history` লেখে (entity: jurisdiction, registration, box, code, rate, group, account_map, rule)।

**Related tables / schema**

**`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-এ নিষিদ্ধ।

**Business rules**
- R8.2 থেকে R8.7, R8.11, R8.15, R8.16, R8.17, R8.41।
- Rate তারিখ-ভিত্তিক: একই code-এ `effective_from` নতুন সারি আগের সব সারির `effective_from`-এর পরে; overlap আটকায় (`TAX_RATE_OVERLAP`)।
- **Retroactive নিষেধ:** নতুন rate-এর `effective_from <= ওই code-এর সর্বশেষ posted `tax_date` হলে `TAX_RATE_RETROACTIVE`।
- ব্যবহৃত rate-এর `rate` ইত্যাদি অপরিবর্তনীয় (trigger, `TAX_RATE_IMMUTABLE`)। `tax_type` ব্যবহৃত হলে অপরিবর্তনীয়।
- প্রস্তাবকারী ≠ অনুমোদনকারী (`approved_by ≠ proposed_by`)।
- অনুমোদনে আগের open সারির `effective_to = new.effective_from - 1 দিন` (trigger-অনুমোদিত একমাত্র বদল)।
- `rate_epoch` প্রতি অনুমোদনে +1; resolver cache key।
- `partial` recoverable হলে `recoverable_pct` বাধ্যতামূলক; withholding হলে `withholding_side` বাধ্যতামূলক।
- `calc_method = fixed_amount` হলে rate-এর বদলে `fixed_amount` (দুটোর ঠিক একটা)।
- **Open point:** Retroactive পরীক্ষা `acc_journal_line_taxes` (shared table, Phase 8-এর পরের card, এই document-এ নেই) এ `tax_date` পড়ে। সেই table না থাকা পর্যন্ত পরীক্ষা কী করবে (খালি ধরে এগোনো, না আটকানো) source বলেনি; আপাতত "table না থাকলে পরীক্ষা সফল" ধরা যায়, নিশ্চিত নয়।
- **Open point:** OD-41 এখানে "tax point তারিখ" (`P8-01`-এ "return period ও frequency")। `acc_tax_rates`-এ কোন তারিখ (tax_date বনাম invoice/payment) ধরে rate নির্বাচন হবে, `TaxRateResolver::resolve(company, codeId, date)` শুধু `date` নেয়; সঠিক তারিখ নির্ধারণ OD-41।
- **Open point:** OD-44b (reverse charge) এ `reverse_charge` type-এর rate ও পরিচালনা কীভাবে হবে `P8-05` ধাপ (৭)-এ আছে; এই card-এ শুধু type ENUM।
- **Open point:** `rate BETWEEN 0 AND 100` CHECK হলেও `fixed_amount` এর সীমা নেই; ঋণাত্মক `fixed_amount` আটকানো source-এ বলা নেই।

**DB flow**
**Rate তৈরি (প্রস্তাব), এক transaction:**
1. `acc_tax_codes` সারি `FOR UPDATE` (শুধু নিজের table)।
2. `version` মেলানো।
3. নতুন `acc_tax_rates` সারি `status = pending`, `effective_from` > সব বিদ্যমান সারির `effective_from`।
4. Retroactive পরীক্ষা: `SELECT MAX(tax_date) FROM acc_journal_line_taxes` ওই code-এ, posted journal join, কোনো lock ছাড়া পড়া। `effective_from <= সর্বশেষ tax_date` হলে `TAX_RATE_RETROACTIVE`।
5. Change request `taxrate:<id>` (`P1-04`), history INSERT।

**অনুমোদনে (আলাদা transaction):** code `FOR UPDATE`; (4) আবার; আগের open সারির `effective_to = new.effective_from - 1 দিন` (trigger-অনুমোদিত একমাত্র বদল); নতুন সারি `active`; `acc_tax_codes.rate_epoch` +1; history; audit।

Ledger lock নেই। ব্যর্থ হলে সব rollback।

**Task scope**
1. Migration ও model: `acc_tax_codes`, `acc_tax_rates`, `acc_tax_config_history`।
2. Rate trigger (শুধু `effective_to`, `status`, `first_used_at` বদল; DELETE নিষিদ্ধ)।
3. `TaxCodeService`, `TaxRateService`, `taxrate:` ChangeApplier prefix।
4. `TaxRateResolver` ও cache (`rate_epoch`)।
5. Retroactive পরীক্ষা।
6. History writer।
7. API।
8. Test।

**Out of scope**
Jurisdiction (`P8-01`)। Group (`P8-03`)। Account map (`P8-04`)। গণনা (`P8-05`)। `first_used_at` বসানো (posting-সময়ের পরের card, এই document-এ নেই)।

**Contract (API)**
- Service: `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

**Definition of done**
- সাধারণ DoD পূরণ।
- AC8.2, AC8.3 সবুজ।
- Trigger ও append-only প্রমাণ সরাসরি SQL-এ (UPDATE/DELETE ব্যর্থ)।
- `tax.enabled` বন্ধ রেখে merge নিরাপদ।

**Architecture constraints**
- Rate কখনো in-place বদলায় না; সবসময় নতুন সারি + পুরোনোর `effective_to`।
- `acc_tax_config_history` কখনো UPDATE/DELETE নয়।
- Ledger lock নয়।
- Retroactive পরীক্ষা lock ছাড়া পড়া (পড়ার পরও অনুমোদনে আবার)।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P8-03 — Tax group ও component

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service)
Title
Tax group ও component — Backend
Branch Name
`feature/accounting-tax-group`
View Order
91
Description
**Summary**
এক লেনদেনে একাধিক কর (base-এ একটা, compound-এ আগের করসহ আরেকটা) সামলাতে tax group — একাধিক component ও `apply_on` ক্রম। group নিজে rate রাখে না; member code-এর rate-ই ব্যবহার হবে।

**Task serial**
`ACC-P8-03 — Tax group ও component` · Feature · Phase 8, তৃতীয় card

**Depends on**
- `ACC-P8-02`

**Relation: আগের task**
- `P8-02` দিয়েছে `acc_tax_codes` (member হবে) ও active/rate সংজ্ঞা, `TaxRateResolver`, এবং `acc_tax_config_history` (entity `group` এখানে লেখা হয়)।
- `P8-01` (সরাসরি নির্ভরতায় নেই, কিন্তু `P8-02`-এর মাধ্যমে) দিয়েছে `acc_tax_jurisdictions`: group-এর `jurisdiction_id` FK এখানে; সব member একই jurisdiction-এর হতে হয়।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P8-05` (গণনা engine): `TaxInput`-এ `tax_group_id` নিলে group-এর member ক্রমে (`seq_no`, `apply_on`) component গণনা করে।
- `P8-06` (অন্য tab-এ, এই document-এ নেই): source এই card-এর ওপর নির্ভরশীল বলেছে।

**Related tables / schema**

**`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`।

**Business rules**
- R8.8, R8.9, R8.10।
- Group নিজে rate রাখে না; member code-এর rate ব্যবহার হয়।
- Member code একই jurisdiction-এর ও active হতে হবে (`TAX_GROUP_JURISDICTION_MIX`, `TAX_CODE_INACTIVE`)।
- একই code দুইবার একই group-এ নয় (`uq (company_id, group_id, tax_code_id)`)।
- `seq_no` 1 থেকে ক্রমিক; seq 1-এ `apply_on` অবশ্যই `base`; পরের সারিতে `base` বা `base_plus_previous` (compound)।
- ব্যবহৃত group-এর member বদলালে সেটা সম্ভব নয় (`TAX_GROUP_IN_USE`): নতুন group বানাতে হবে।
- `first_used_at` প্রথম posted ব্যবহারে বসে (পরের card-এ)।
- `rounding_mode` NULL হলে jurisdiction, তারপর `tax.rounding_mode` setting।
- **Open point:** `TAX_GROUP_INVALID` ঠিক কোন কোন শর্তে (সদস্যসংখ্যা শূন্য, `seq_no` ফাঁক, seq 1-এ `base_plus_previous`) তালিকা source-এ নেই; আনুমানিক এই তিনটা।
- **Open point:** OD-44b (reverse charge group) ও OD-39b (partial recovery) এই card-এর OD, কিন্তু card-এর নিয়মে এদের আলাদা প্রয়োগ নেই (শুধু code-স্তরে `reverse_charge`/`partial`)। Group-স্তরে reverse charge কীভাবে প্রতিফলিত হবে (যেমন দুই member: output ও input) `P8-05` ধাপ (৭) সম্পর্কিত, লিখিত নয়।
- **Open point:** DB flow-এ "group `version` +1" বলে কিন্তু "ব্যবহৃত group-এর member বদলালে নতুন group `version` সারি নয়" বাক্যটির অর্থ অস্পষ্ট (ধরা হয়েছে: ব্যবহৃত group-এ in-place member বদল নেই; নতুন group বানাও)।

**DB flow**
Group তৈরি বা বদল এক transaction:
1. Group `FOR UPDATE` (নিজের table)।
2. Member code-গুলো একই jurisdiction ও active কি পড়া (lock নেই)।
3. সব member সারি delete-insert (draft group, অর্থাৎ এখনো ব্যবহৃত নয়)।
4. History (`acc_tax_config_history`, entity `group`)।
5. `rate_epoch` কোনো member code-এ নয়, বরং group `version` +1।

ব্যবহৃত group-এর member বদলালে `TAX_GROUP_IN_USE`: নতুন group বানাও। ব্যর্থ হলে rollback।

**Task scope**
1. Migration: `acc_tax_groups`, `acc_tax_group_members`।
2. Service ও validation (`TaxGroupService`)।
3. API।
4. Test।

**Out of scope**
Tax code/rate (`P8-02`)। গণনা (`P8-05`)। Group নির্ধারণ rule (`P8-06`, অন্য tab)। `first_used_at` বসানো।

**Contract (API)**
- Service: `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

**Definition of done**
- সাধারণ DoD পূরণ।
- AC8.4 সবুজ।
- অন্য company-র code member হিসেবে বসানো composite FK-তে আটকায়।
- `tax.enabled` বন্ধ রেখে merge নিরাপদ।

**Architecture constraints**
- Group কখনো নিজে rate রাখে না।
- ব্যবহৃত group immutable (member তালিকা)।
- Composite FK ও `uq (company_id, id)`।
- Ledger lock নয়।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P8-04 — Tax account map

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service)
Title
Tax account map — Backend
Branch Name
`feature/accounting-tax-account-map`
View Order
92
Description
**Summary**
কোন tax কোন ledger account-এ যাবে — সেটা তারিখ-ভিত্তিক map; কোডে কোনো account বা rate বাঁধা নেই। map বদললেও posted line-এ `account_id` আগের মতোই থাকবে (সরাসরি লেখা)।

**Task serial**
`ACC-P8-04 — Tax account map` · Feature · Phase 8, চতুর্থ card

**Depends on**
- `ACC-P8-01` (jurisdiction)
- `ACC-P8-02` (tax code)
- `ACC-P1-04` (change request, prefix `taxmap:`)
- বিদ্যমান CoA (`acc_accounts`, `ACC-P1-06`)

**Relation: আগের task**
- `P8-01` দিয়েছে `acc_tax_jurisdictions` (map-এর `jurisdiction_id` FK, বদলে `FOR UPDATE`)।
- `P8-02` দিয়েছে `acc_tax_codes` (`tax_code_id` FK) ও `acc_tax_config_history`।
- `P1-04` দিয়েছে change request ও `ChangeApplier`; এই card `taxmap:` prefix নিবন্ধন করে।
- `P1-06` দিয়েছে `acc_accounts` (active, `is_group`, type, `uq (company_id, id)`) ও `AccountService`, যাতে এই card CoA hook বসায়।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P8-05` (গণনা engine): `TaxAccountMapService::resolve(company, jurisdictionId, taxCodeId, role, date): accountId` ব্যবহার করে প্রতি component-এর account বের করে (`TAX_ACCOUNT_MAP_MISSING` সহ)।
- `P8-13`, `P8-16` (অন্য tab-এ, এই document-এ নেই): source এই card-এর ওপর নির্ভরশীল বলেছে।
- `P1-06`-এর `AccountService` (বিদ্যমান): এই card-এর CoA hook mapped account নিষ্ক্রিয় বা type বদল আটকায়।

**Related tables / schema**

**`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`।

**Business rules**
- R8.18, R8.41।
- Account যাচাই (সাধারণ read, ledger lock নেই): active, group নয়, posting অনুমোদিত, type role-এর সাথে মেলে (`TAX_ACCOUNT_TYPE_MISMATCH`)।
- Map বদলালে পুরোনো লেনদেনে কিছু বদলায় না, কারণ posted line-এ `account_id` সরাসরি লেখা।
- Account পরে নিষ্ক্রিয় বা type বদল হলে CoA service map পড়ে আটকায় (`TAX_ACCOUNT_IN_USE`)।
- Role ফাঁকা থাকলে calculate `TAX_ACCOUNT_MAP_MISSING`।
- নিজের প্রস্তাব নিজে অনুমোদন আটকায় (Maker-Checker)।
- jurisdiction-স্তরের role (`net_tax_payable`, `tax_refundable`, `rounding`) `tax_code_id` NULL; বাকি role code-ভিত্তিক।
- **Open point:** "type role-এর সাথে মেলে" এর role → অনুমোদিত account type মানচিত্র (যেমন `tax_payable` → liability, `tax_receivable` → asset, `non_recoverable_expense` → expense) source-এ তালিকা নেই, শুধু test (ক) "payable role-এ asset account আটকায়" আছে। পূর্ণ মানচিত্র ঠিক করতে হবে।
- **Open point:** DB flow-এ "jurisdiction-এর config epoch +1 (cache বাতিল)" বলা হয়েছে, কিন্তু `acc_tax_jurisdictions` table-এ epoch column নেই (`P8-01`-এ শুধু `version`)। Epoch কোথায় থাকবে (jurisdiction `version` ব্যবহার, নতুন column, না cache key) সিদ্ধান্ত লাগবে।
- **Open point:** OD-39a (non-recoverable posting) সিদ্ধান্ত লিখিত নেই; role `non_recoverable_expense` কীভাবে ব্যবহার হবে তা `P8-05` ধাপ (৬)-এ আছে।
- **Open point:** CoA hook `P1-06`-এর `AccountService` পরিবর্তন করে (নতুন error `TAX_ACCOUNT_IN_USE` যোগ); এটি Phase 1 code-এ পরিবর্তন, তাই regression ঝুঁকি আছে।

**DB flow**
**বদলের অনুরোধ, এক transaction:**
1. Jurisdiction সারি `FOR UPDATE` (শুধু নিজের table)।
2. `version` মেলানো।
3. Account যাচাই (সাধারণ read, ledger lock নেই): active, group নয়, posting অনুমোদিত, type role-এর সাথে মেলে।
4. নতুন সারি `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।

**Task scope**
1. Migration (`acc_tax_account_map`) ও trigger।
2. Account যাচাই।
3. `taxmap:` ChangeApplier।
4. Resolver ও cache।
5. CoA service hook (`P1-06`-এর `AccountService`)।
6. API।
7. Test।

**Out of scope**
Jurisdiction ও code (`P8-01`, `P8-02`)। গণনা engine (`P8-05`)। Determination rule (`P8-06`, অন্য tab)। উদাহরণ import (`P8-16`, অন্য tab)।

**Contract (API)**
- Service: `TaxAccountMapService::propose/activate`, `::resolve(company, jurisdictionId, taxCodeId, role, date): accountId`।
- API `GET/PUT /tax/account-map` (পুরো তালিকা, `version` সহ)।

**Permission**
view, manage, approve (দ্বিতীয় ব্যক্তি): `accounting.tax.view`, `accounting.tax.manage`, `accounting.tax.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

**Definition of done**
- সাধারণ DoD পূরণ।
- AC8.5 সবুজ।
- `P1-06`-এর বিদ্যমান CoA test সবুজ (hook regression নেই)।
- Trigger প্রমাণ: `account_id`/`role`/`effective_from` `pending` ছাড়া বদলায় না।

**Architecture constraints**
- কোডে কোনো account বাঁধা নেই; সব map থেকে।
- Posted line-এর `account_id` কখনো map দেখে বদলায় না।
- Ledger lock নয়; jurisdiction `FOR UPDATE` শুধু নিজের table-এ।
- Composite FK ও `uq (company_id, id)`।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Feature (service)
Title
TaxService, গণনা engine ও rounding — Backend
Branch Name
`feature/accounting-tax-calculation-engine`
View Order
93
Description
**Summary**
সব tax হিসাব এক জায়গায় — `TaxService`, pure function; একই input ও config epoch-এ একই ফল, কোনো lock/write নেই। preview, journal, পরের Phase (যেমন Phase 14) সবাই এটাই ডাকবে; কর-গণনায় অমিল হবে না।

**Task serial**
`ACC-P8-05 — TaxService, গণনা engine ও rounding` · Feature · Phase 8, পঞ্চম card

**Depends on**
- `ACC-P8-02` (tax code, rate, resolver)
- `ACC-P8-03` (group ও component)
- `ACC-P8-04` (account map)
- `P7` (`FxService::convert`, base amount আনতে) — Phase 7 অন্য tab-এ, এই document-এ নেই

**Relation: আগের task**
- `P8-02` দিয়েছে `TaxRateResolver::resolve(company, codeId, date)` (তারিখ-ভিত্তিক rate, `rate_epoch` cache) এবং code-এর `tax_type`, `calc_method`, `price_basis`, `recoverable_mode/pct`, `withholding_side`, `box_code_*`।
- `P8-03` দিয়েছে group ও member (`seq_no`, `apply_on`) যা compound গণনার ভিত্তি।
- `P8-04` দিয়েছে `TaxAccountMapService::resolve(...)` যা প্রতি component-এর `account_id` বের করে।
- `P7` দিয়েছে `FxService::convert` যা foreign currency amount base currency-তে আনে (OD-42)। `TaxInput.lines[].amount` base currency-তে; তাই caller বা এই service-কে আগে convert করতে হয় (সুনির্দিষ্ট দায়িত্ব নিচে open point)।
- `P8-01` (পরোক্ষ) দিয়েছে jurisdiction (`rounding_mode`)।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P8-06` থেকে `P8-09`, `P8-17` (অন্য tab-এ, এই document-এ নেই): source এই card-কে এদের ভিত্তি বলেছে (determination rule `P8-06`; journal-এ কর বসানো ও posting এর পরের card)।
- Journal লেখার card (Phase 8-এর পরের), Phase 14 (posting rules engine): `TaxService::calculate` ডাকে, যাতে একই ফল পায়। (Phase 14 অন্য tab-এ)

**Related tables / schema**
নতুন table নেই। Cache: process-এর ভেতর, key `(company, code_id, rate_epoch, date)`।

পড়ে (কোনো write নেই): `acc_tax_codes`, `acc_tax_rates`, `acc_tax_groups`, `acc_tax_group_members`, `acc_tax_account_map`, `acc_tax_determination_rules` (শেষটা `P8-06`-এ তৈরি, এই document-এ নেই)।

**Business rules**
- R8.6, R8.9 থেকে R8.17, R8.40।
- **Pure function:** একই input ও config epoch হলে একই ফল; কোনো lock, কোনো write নয়।
- Transaction লাগে না (repeatable-read snapshot caller-এর transaction থেকে)। ব্যর্থ হলে exception; কিছু লেখা হয়নি।
- **গণনা ধাপ:**
  1. প্রতি line-এর code বা group ঠিক (সরাসরি, নইলে `P8-06` determination rule)।
  2. প্রতি component-এর rate `TaxRateResolver` থেকে তারিখ ধরে।
  3. **Exclusive:** component ক্রমে `tax = (base + আগের কর, apply_on অনুযায়ী) × rate ÷ 100`।
  4. **Inclusive:** `base = gross ÷ f`, যেখানে `f` = base 1 ধরে exclusive গণনার `1 + Σ tax`। এতে additive ও compound দুটোই সামলায়।
  5. **Rounding:** `line` মোডে প্রতি component পূর্ণসংখ্যায় (precision, method)। `document` মোডে code ধরে মোট একবার round; ফারাক line-এ largest remainder পদ্ধতিতে, tie-এ কম `line_no`। Inclusive-এ `base = gross − Σ tax` যাতে মোট ঠিক gross হয়।
  6. **Recoverable ভাগ:** full সব, none শূন্য, partial `round(tax × pct ÷ 100)`, বাকিটা non-recoverable।
  7. **Reverse charge:** সমান অঙ্কের output ও input জোড়া।
  8. **Zero-rated, exempt, out of scope:** `tax_amount = 0` সারি, base সহ।
  9. **Withholding:** `withholding_side` অনুযায়ী role।
  10. **Account map থেকে account।**
  11. **Invariant:** `tax_amount >= 0`, `Σ components + base = gross` (inclusive), hash (`calculation_hash`)।
- **Open point:** `TaxInput.lines[].amount` "base currency" বলা, কিন্তু base amount আনার দায়িত্ব (caller নাকি `TaxService`-এর ভেতরে `FxService::convert`) source-এ দ্ব্যর্থক; নির্ভরতায় `P7 (FxService::convert, base amount আনতে)` থাকলেও `TaxInput`-এ currency বা rate field নেই। OD-42 (foreign currency)-এ সিদ্ধান্ত লাগবে। Phase 7 এই document-এ না থাকায় `FxService` স্বাক্ষর অজানা।
- **Open point:** `P8-06` (determination rule, `acc_tax_determination_rules`) না থাকলে ধাপ (১)-এ শুধু সরাসরি `tax_code_id`/`tax_group_id` চলে; `determination{...}` পথ, `TAX_RULE_NO_MATCH`, `TAX_RULE_AMBIGUOUS` এই document-এ পরীক্ষা করা যাবে না (table-এর অনুপস্থিতিতে stub দরকার)।
- **Open point:** OD-40 (rounding), OD-39a, OD-39b, OD-44b লিখিত নেই; Golden G8.1 থেকে G8.5, G8.8 (G8.6, G8.7 নয়) এর সংখ্যা এই document-এ দেওয়া নেই (Blueprint-এ), তাই golden fixture ফাইল লিখতে Blueprint দরকার।
- **Open point:** `TAX_BASIS_INVALID`, `TAX_ROUNDING_CONFLICT` কখন ছোড়া হয় source-এ ব্যাখ্যা নেই (সম্ভবত `price_basis` মান অজানা/line-এর মধ্যে মিশ্র basis; rounding `line` বনাম `document` বিরোধ)।
- **Open point:** "precision, method" বলে rounding method (half_up বনাম half_even) test-এ উল্লেখ আছে; সংজ্ঞা `tax.rounding_*` setting-এ সম্ভবত, যা Registry-তে (এই document-এ নেই)।

**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, কিছু লেখা হয়নি।

**Task scope**
1. `TaxInput`/`TaxResult` DTO।
2. Component engine (exclusive, inclusive factor)।
3. `TaxRounding` (line, document, largest remainder)।
4. Recoverable ভাগ।
5. Reverse charge ও withholding।
6. Account resolution।
7. Hash (`calculation_hash`)।
8. API preview (`POST /tax/calculate`)।
9. Golden ও property test।

**Out of scope**
কোনো journal, line বা ledger-এ লেখা। Determination rule table/engine (`P8-06`)। `acc_journal_line_taxes` পূরণ। Return তৈরি। FX conversion-এর বাস্তবায়ন (Phase 7)।

**Contract (API)**
- `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)।

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

**Definition of done**
- সাধারণ DoD পূরণ (audit ব্যতীত, কারণ pure)।
- AC8.6, AC8.7, AC8.8 সবুজ; golden ও property test সবুজ।
- কোনো table-এ লেখা হয় না প্রমাণিত (preview-র আগে ও পরে row count অপরিবর্তিত)।
- `tax.enabled` বন্ধ রেখে merge নিরাপদ।

**Architecture constraints**
- `TaxService` pure: কোনো lock, write, side effect নয়; একই input ও epoch → একই `calculation_hash`।
- সব টাকার গণনা BCMath/DECIMAL, float নয়।
- সব tax হিসাবের একমাত্র জায়গা: journal, preview, Phase 14 কেউ নিজে কর হিসাব করবে না।
- কোডে rate বা account বাঁধা নয়।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

<!-- ===== END: ACC-Phase-8-Tasks.md ===== -->


<!-- ===== BEGIN: ACC-Phase-17-Tasks.md ===== -->

# Accounting Module — Phase 17 Tasks (Enterprise UAT, Parallel Run ও Full Release)

> এখানকার প্রতিটি task আলাদা করে একজন কাজ করতে পারবে।
> Phase 17-এ কোনো নতুন table নেই। এই অংশের card-গুলো test ও অপারেশন: UAT কৌশল, capability UAT, cross-module end-to-end, negative ও isolation, data migration মহড়া, parallel run, scale ও performance, failover ও DR drill। কোথাও ব্যর্থতা মিললে সংশোধন সংশ্লিষ্ট Phase-এর card-এ, এই Phase-এ নয়।
> ক্রম: UAT কৌশল ও catalog → তিন দলের capability UAT → cross-module E2E ও negative/security → migration মহড়া → parallel run → scale/performance → DR drill।
> Estimate, Owner, Sprint ইচ্ছা করে ফাঁকা রাখা হয়েছে।
> নোট: Phase 17-এর আসলে P17-01 থেকে P17-20 আছে, কিন্তু এই document-এ শুধু P17-01 থেকে P17-10 আছে; P17-11 থেকে P17-20 (release control: go/no-go, wave rollout, hypercare, rollback, governance, penetration test, accessibility, training, support, handover ইত্যাদি), Blueprint ২৭.৯-এর scenario ও traceability table, এবং Phase 7, 9-16-এর card অন্য tab-এ, তাই সেগুলো এখানে বানানো হয়নি।

## সব Phase 17 task-এ প্রযোজ্য সাধারণ নিয়ম (প্রতিটা task নিজের DoD-তে এগুলো ধরে নেয়)

- **Phase 17-এ কোনো product code বা নতুন table নেই।** কাজ হলো পরীক্ষা, প্রমাণ সংগ্রহ ও অপারেশন। কোথাও ব্যর্থতা মিললে সংশোধন সংশ্লিষ্ট Phase-এর card-এ, এই Phase-এ নয়।
- **R17.1 UAT ও production আলাদা।** UAT পরিবেশ = production-copy, মাস্ক করা। সাধারণ API দিয়ে `PostingService` হয়ে লেখা; সরাসরি SQL insert নেই।
- **R17.2 expected result আগে লেখা** (run-এর আগে)। **R17.3 প্রমাণ ছাড়া pass নেই।** **R17.4 প্রতিটা scenario-তে debit = credit নিশ্চিত।**
- **ব্যর্থ হলে ledger হাতে ঠিক করা নিষেধ;** ঠিক করার পথ Reversal বা Adjustment, অথবা snapshot restore (শুধু UAT পরিবেশে)। কোনো পরীক্ষায় trigger বা guard বন্ধ করা যাবে না (Phase 3-এর test পরিবেশের নির্দিষ্ট ব্যতিক্রম ছাড়া)।
- **Scenario ID ফরম্যাট:** `UAT-{phase}.{n}`, `UAT-E{n}`, `UAT-N{n}`, `MIG-{n}`, `PF-{n}`, `DR-{n}`, `PT-{n}`। এগুলোর পূর্ণ তালিকা ও traceability table Blueprint ২৭.৯-এ (এই document-এ নেই)।
- **Defect severity:** sev1 থেকে sev4 (সংজ্ঞা `P17-01`-এ)। Capability UAT-এর শেষে কোনো sev1 বা sev2 খোলা থাকা চলবে না।
- **Persona (seed role):** accountant, approver, AP clerk, AR clerk, treasury, auditor, admin, read-only। Scenario নিজে permission বাদ দেয় না; সব persona আলাদা login।
- **Permission:** `accounting.release.view` (দেখা), `accounting.release.manage` (cycle চালানো); `accounting.release.approve` (accepted exception, `P17-08`)। এগুলো এই document-এর §২.৩ তালিকায় নেই (Enterprise Registry-তে, যা অন্য tab-এ)।
- **Error:** `REL_PRECONDITION_FAILED` (পরিবেশ বা data প্রস্তুত নয়), `REL_PARALLEL_RUN_OPEN` (parallel run exit শর্ত অপূর্ণ)। বাকি সব error সংশ্লিষ্ট capability-র prefix।
- **Audit prefix:** `accounting.release.*` (`uat_cycle_started`, `uat_cycle_signed`, `uat_scenario_result`, `migration_rehearsal_done`, `parallel_run_compared`, `discrepancy_resolved`, `scale_test_done`, `dr_drill_done`)।
- **প্রমাণ জমা:** `acc_reconciliation_runs` (RC1 থেকে RC18), `acc_report_exports`; scenario-র ফল আলাদা সংখ্যা সহ লগ।
- **Phase 5-এর সাথে সম্পর্ক:** এই Phase বিদ্যমান `P5-04` (rollback), `P5-06` (release gate) ও `P5-08` (pilot rollout) বাড়ায়, বদলায় না। Phase 5-এর UAT ফল কোনো DB table-এ নয় (QA tool-এ), কিন্তু এই Phase-এর প্রমাণ `acc_reconciliation_runs` ও `acc_report_exports`-এ।
- **DoD-র সাধারণ অংশ:** scenario-র ফল সংখ্যাসহ লগ; প্রতি scenario-র expected result run-এর আগে লিখিত; sign-off (নির্ধারিত ভূমিকার সই) জমা; defect তালিকা ও triage; কোনো sev1/sev2 খোলা নেই বা অনুমোদিত exception; পরিবেশ আবার snapshot-এ ফেরানো যায়।

**Phase-স্তরের open point (একাধিক card প্রভাবিত):**

- OD-105, OD-105a, OD-105b, OD-107, OD-108, OD-108b, OD-114, OD-115, OD-119, এবং AC17.x (card-এর ভেতরে লেখা), R17.x (R17.9, R17.10, R17.11, R17.18-এর বিষয়বস্তু source-এ লেখা নেই), G9, G17.1 থেকে G17.4, Blueprint ২৭.৯ক থেকে ২৭.৯চ-এর scenario তালিকা এই document-এ নেই।
- RC1 থেকে RC9 `P3-13`-এ সংজ্ঞায়িত; RC10 থেকে RC18 (Phase 7 থেকে 16-এর check) এই document-এ নেই। `RC1d`, `RC1m`, `RC1n` Phase 6-এর sub-check।
- `P17-05` "অবরুদ্ধ করে P17-07" বলেছে কিন্তু `P17-07` তার "নির্ভর করে"-তে `P17-05` রাখেনি (রেখেছে `P17-04`, P16 import card, `P5-02`)।
- `P17-09`-এর lock ক্রম বর্ণনা ("journal, approval, account, period, voucher counter") Phase 6-এর চূড়ান্ত ক্রমের (number series `FOR SHARE`, counter `FOR UPDATE`, শেষে dimension value `FOR SHARE`) চেয়ে ছোট।

**Phase 17 নির্ভরতা-ছবি (এই document-এর P17-01 থেকে P17-10; বাইরের নির্ভরতা বন্ধনীতে):**

```
P5-01,P5-02 + P6..P16 exit gate ─► P17-01 ─┬─► P17-02 (+সব P6..P10) ─┐
                                           ├─► P17-03 (+সব P11..P13) ─┼─► P17-05 ─┬─► P17-08 (+P17-07)
                                           └─► P17-04 (+সব P14..P16) ─┤           ├─► P17-09 (+P17-07, P16 partitioning/archive)
                                                    │                  └─► P17-06  │
                                                    └─► P17-07 (+P16 import, P5-02) ─┘   P17-09 ─► P17-10 (+P16 DR, P5-04)
P17-01 ─► P17-11, P17-12  P17-02..P17-10 ─► P17-15  P17-06 ─► P17-11  P17-07 ─► P17-16  P17-10 ─► P17-19  (সব অন্য tab-এ)
```

---

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

Project
ERPFlow
Module
Accounting Module
Type
Operations + Test (`ops` + `test`)
Title
UAT কৌশল, পরিবেশ ও scenario catalog
Branch Name
`feature/accounting-uat-strategy-catalog`
View Order
94
Description
**Summary**
UAT শুরু করার আগে একটা কাঠামো দরকার: সব capability একই নিয়মে, একই পরিবেশে, একই প্রমাণের ছাঁচে। scenario catalog, persona, severity, snapshot-নিয়ম — পরের card (দল ক/খ/গ, E2E, negative) আলাদা কাজ হলেও ফল তুলনীয় ও প্রমাণযোগ্য থাকবে।

**Task serial**
`ACC-P17-01 — UAT কৌশল, পরিবেশ ও scenario catalog` · Ops + Test · Phase 17, প্রথম card

**Depends on**
- `ACC-P5-01` (UAT pack ও সই: 14টা পরিস্থিতি U01 থেকে U14)
- `ACC-P5-02` (Cutover মহড়া)
- Phase 6 থেকে 16-এর প্রতিটা exit gate (সব P6 থেকে P16 card সবুজ)। এর মধ্যে এই pack-এর Phase 6 (`ACC-P6-15` enablement ও gate রিপোর্ট) এবং Phase 8 (P8-01 থেকে P8-05) অন্তর্ভুক্ত; বাকি phase অন্য tab-এ।

**Relation: আগের task**
- `P5-01` দিয়েছে Phase 1-5-এর UAT-র পদ্ধতি (ধাপ-ধাপ script, প্রমাণ, defect তালিকা, সই) ও 14টা পরিস্থিতি; এই card সেই পদ্ধতিকে সব capability-র জন্য একটা কাঠামোয় বাড়ায়। 
- `P5-02` দিয়েছে cutover মহড়ার পদ্ধতি (staging, বারবার চালানো, সময় মাপা), যা এই card-এর পরিবেশ ও snapshot-নিয়মের উদাহরণ।
- Phase 6 থেকে 16-এর exit gate: প্রতিটা Phase-এর নিজস্ব test সবুজ হওয়ার প্রমাণ (যেমন `ACC-P6-15`-এর gate রিপোর্ট, RC1d/RC1m/RC1n সবুজ)। সেগুলো না থাকলে UAT শুরু করা যায় না।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P17-02`, `P17-03`, `P17-04` (capability UAT): এই card-এর scenario ছাঁচ, ID নিয়ম, persona/role, পরিবেশ ও severity সংজ্ঞা ব্যবহার করে।
- `P17-05` থেকে `P17-12`: E2E, negative, migration মহড়া, parallel run, scale, DR ও পরের release-control card (P17-11, P17-12 অন্য tab-এ) একই পরিবেশ ও catalog ধরে।
- `P17-15` (অন্য tab-এ): source এই card-কে নির্ভরতায় রেখেছে।

**Related tables / schema**
নতুন table নেই।
- পড়া: সব `acc_` table (UAT পরিবেশ = production-copy, মাস্ক করা)।
- প্রমাণ জমে: `acc_reconciliation_runs` (RC1 থেকে RC18) ও `acc_report_exports`।

**Business rules**
- R17.1 (UAT ও production আলাদা), R17.2 (expected result আগে লেখা), R17.3 (প্রমাণ ছাড়া pass নেই)।
- লেখা শুধু সাধারণ API দিয়ে, `PostingService` হয়ে। সরাসরি SQL insert নেই।
- প্রতিটা UAT cycle শুরুতে snapshot restore। ব্যর্থ হলে cycle থামে, snapshot-এ ফেরা হয়, ledger-এ আংশিক অবস্থা থাকে না।
- 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}`।
- Persona-র জন্য seed করা role: accountant, approver, AP clerk, AR clerk, treasury, auditor, admin, read-only।
- Defect severity সংজ্ঞা sev1 থেকে sev4 ও blocking নিয়ম (কোনটা gate আটকায়)।
- Traceability: Phase 6 থেকে 16-এর প্রতিটা AC কোনো না কোনো scenario-তে ঢাকা (traceability table-এ খালি ঘর শূন্য)।
- **Open point:** OD-105a, OD-105b (data volume), OD-108 লিখিত সিদ্ধান্ত নেই; "Cycle calendar (কতটা cycle, কখন)" সংখ্যা source-এ নেই (পরের card "Cycle 3"-এর কথা বলে, মানে অন্তত ৩ cycle ধরা হয়েছে)।
- **Open point:** Masking নিয়ম (কোন field কীভাবে মাস্ক), পরিবেশের আকার ও snapshot restore পদ্ধতি source-এ নির্দিষ্ট নেই; সাব-টাস্ক ১ এ ঠিক করতে হবে।
- **Open point:** Permission `accounting.release.view`/`.manage` ও persona role seed কীভাবে (`P1-02`-এর seed কাঠামো বাড়িয়ে নাকি আলাদা) করা হবে source-এ নেই; এগুলো Phase 17-এ "নতুন code নেই" নিয়মের সাথে সংঘাত করতে পারে (seed/config বাদে)।
- **Open point:** Phase 5-এর UAT (QA tool-এ) ও Phase 17-এর প্রমাণ (DB table `acc_reconciliation_runs`, `acc_report_exports`)-এর সমন্বয় কীভাবে (Phase 5-এর 14 পরিস্থিতি catalog-এ ঢুকবে কি না) source বলেনি।

**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-এ আংশিক অবস্থা থাকে না।

**Task scope**
1. পরিবেশ ও data masking নিয়ম।
2. Persona ও role seed।
3. Scenario ছাঁচ ও ID নিয়ম।
4. Defect severity ও blocking নিয়ম।
5. Cycle calendar (কতটা cycle, কখন)।
6. Traceability table ভরা (Phase 6 থেকে 16-এর প্রতিটা AC → scenario)।

**Out of scope**
আসল scenario script লেখা ও চালানো (`P17-02` থেকে `P17-04`)। E2E ও negative (`P17-05`, `P17-06`)। Release-control, go/no-go (`P17-11` ইত্যাদি, অন্য tab)।

**Contract (API)**
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}`। (নতুন HTTP endpoint source-এ নেই।)

**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 ভরা

**Definition of done**
- সাধারণ DoD পূরণ।
- AC17.1, AC17.2 সবুজ; traceability table-এ খালি ঘর শূন্য।
- UAT পরিবেশ production থেকে আলাদা প্রমাণিত; snapshot restore একবার পরীক্ষিত।
- Persona দিয়ে আলাদা login কাজ করে।

**Architecture constraints**
- UAT পরিবেশ production-এর সাথে কখনো মেশে না; data মাস্ক করা।
- সরাসরি SQL insert নয়; সব লেখা API ও `PostingService` দিয়ে।
- কোনো product code বা table নয়।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Test (`test`)
Title
Capability UAT script, দল ক (journal, FX, tax, subledger, bank)
Branch Name
`feature/accounting-uat-group-a`
View Order
95
Description
**Summary**
দল ক: journal management ও dimensions, multi-currency, tax, subledger, bank — প্রতিটার scripted scenario লেখা ও চালানো (expected ledger line ও RC সহ)। production-এর মতো UAT পরিবেশে এই ক্ষমতাগুলো প্রমাণিত হবে।

**Task serial**
`ACC-P17-02 — Capability UAT script, দল ক (journal, FX, tax, subledger, bank)` · Test · Phase 17, দ্বিতীয় card

**Depends on**
- `ACC-P17-01`
- সব P6, P7, P8, P9, P10 card। এই pack-এ: Phase 6 (`ACC-P6-01` থেকে `ACC-P6-15`) ও Phase 8-এর P8-01 থেকে P8-05; Phase 7, 9, 10 এবং P8-06 থেকে P8-18 অন্য tab-এ।

**Relation: আগের task**
- `P17-01` দিয়েছে পরিবেশ, scenario ছাঁচ, ID নিয়ম, persona/role, severity সংজ্ঞা ও cycle calendar।
- Phase 6 (journal management ও dimensions): `P6-15` gate সবুজ, দুই flag (`journal_mgmt.enabled`, `dimensions.enabled`) চালু করার সক্ষমতা। UAT-6.x scenario এই card-এ লেখা হয়; Phase 6 card-গুলোর Test অংশ ও G6.x/AC6.x ওই scenario-র উৎস।
- Phase 8 (tax): `P8-01` থেকে `P8-05`: jurisdiction, code, rate, group, account map ও `TaxService`। UAT-8.x এগুলো ও পরের Phase 8 card (অন্য tab) চালায়।
- Phase 7, 9, 10: অন্য tab-এ; এদের সংজ্ঞা এই document-এ নেই।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P17-05` (cross-module E2E): দল ক-এর scenario পাস না হলে E2E-র জোড়া পরীক্ষা অর্থহীন।
- `P17-06` (negative ও isolation): source `P17-06` কে এই card-এর ওপর নির্ভরশীল রেখেছে (P17-02, 03, 04 তিনটাই)।
- `P17-15` (অন্য tab-এ)।

**Related tables / schema**
নতুন table নেই। পড়া:
- `acc_journals`, `acc_journal_lines`, `acc_journal_line_dimensions` (`P6-09`), `acc_journal_line_taxes` (Phase 8-এর শেয়ার্ড table, এই document-এ নেই), `acc_period_balances`।
- RC চালানো: RC10, RC12, RC13, RC14 (Phase 7-10-এর check, সংজ্ঞা এই document-এ নেই) এবং প্রযোজ্য হলে RC1 থেকে RC9, `RC1d`, `RC1m`, `RC1n`।

**Business rules**
- R17.2, R17.3, R17.4 (প্রতিটা scenario-তে debit সমান credit নিশ্চিত)।
- প্রতিটা scenario: precondition data API দিয়ে বসানো, ক্রিয়া চালানো, তারপর পড়া ও তুলনা: লিখিত expected line বনাম ledger।
- কোনো লেখা সরাসরি নয়।
- ব্যর্থ হলে defect রেকর্ড; ledger হাত দিয়ে ঠিক করা নয়, Reversal বা Adjustment দিয়ে।
- Negative case-এ প্রত্যাশিত error code capability-র prefix-সহ (JTY_, DIM_, FX_, TAX_, PTY_, OPI_, BNK_)।
- Scenario নিজেই permission বাদ দেয় না; persona role অনুযায়ী।
- **Open point:** UAT-6.x থেকে UAT-10.x scenario তালিকা Blueprint ২৭.৯ক-এ; এই document-এ নেই, তাই scenario-র ঠিক সংখ্যা ও expected সংখ্যা এখানে নেই।
- **Open point:** Error prefix `FX_`, `PTY_`, `OPI_`, `BNK_` Phase 7, 9, 10-এর (অন্য tab), সংজ্ঞা অজানা। RC10, RC12, RC13, RC14 কোন capability-র তা সংজ্ঞা-অভাবে অনুমান (Phase 7-10)।
- **Open point:** "কোনো sev1 বা sev2 খোলা নেই" এ অনুমোদিত exception-এর প্রক্রিয়া (কে অনুমোদন দেবে) source-এ নেই।

**DB flow**
প্রতিটা scenario:
1. Precondition data API দিয়ে বসানো।
2. ক্রিয়া চালানো।
3. তারপর পড়া: `acc_journals`, `acc_journal_lines`, `acc_journal_line_dimensions`, `acc_journal_line_taxes`, `acc_period_balances`।
4. তুলনা: লিখিত expected line বনাম ledger।
5. RC10, RC12, RC13, RC14 চালানো।

কোনো লেখা সরাসরি নয়। ব্যর্থ হলে defect রেকর্ড, ledger হাত দিয়ে ঠিক করা নয়, reversal বা adjustment দিয়ে।

**Task scope**
1. Phase 6 scenario (journal type, numbering/gapless, backdate, template, import, সংযুক্তি, dimension, reclass, report filter)।
2. Phase 7 scenario (multi-currency ও FX)।
3. Phase 8 scenario (tax)।
4. Phase 9 scenario (subledger AR/AP ও party)।
5. Phase 10 scenario (bank ও cash)।
6. Defect triage ও পুনরায় চালানো।

**সহায়ক ছক (এই pack-এর card থেকে বের করা, source-এর UAT তালিকা নয়):** Phase 6/8 scenario লেখার সময় নিচের card-এর Test, AC ও golden-কে উৎস ধরা যায়।

| Capability | উৎস card | AC / golden |
| --- | --- | --- |
| Journal type ও আচরণ | `P6-01`, `P6-02` | AC6.1, AC6.2, AC6.5 |
| Number series, gapless, cutover | `P6-03`, `P6-04` | AC6.3, AC6.4, AC6.5, G6.1, G6.2 |
| Document/backdate | `P6-05` | AC6.2, G6.8 |
| Template | `P6-06` | AC6.6, G6.9 |
| Bulk import | `P6-07` | AC6.7, G6.7 |
| সংযুক্তি, comment, tag, copy, search | `P6-08` | AC6.13 |
| Dimension কাঠামো, rule, balancing | `P6-09`, `P6-10` | AC6.8, AC6.9, G6.3, G6.4, G6.5 |
| Dimension value ও mirror | `P6-11` | AC6.11 |
| Reclassification | `P6-12` | AC6.10, G6.6 |
| Report filter | `P6-13` | AC6.12, G6.10 |
| Tax jurisdiction, code, rate | `P8-01`, `P8-02` | AC8.1, AC8.2, AC8.3 |
| Tax group, account map | `P8-03`, `P8-04` | AC8.4, AC8.5 |
| Tax গণনা | `P8-05` | AC8.6, AC8.7, AC8.8 |

**Out of scope**
Scenario catalog কাঠামো (`P17-01`)। দল খ ও গ (`P17-03`, `P17-04`)। Cross-module পথ (`P17-05`)। Defect-এর কোড সংশোধন (সংশ্লিষ্ট Phase-এর card)।

**Contract (API)**
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 ও পুনরায় চালানো

**Definition of done**
- সাধারণ DoD পূরণ।
- AC17.3 সবুজ: সব scenario pass বা অনুমোদিত exception; sev1/sev2 খোলা নেই।
- প্রতি scenario-র expected line run-এর আগে লিখিত ও debit = credit যাচাই।
- Accountant-এর সই জমা।

**Architecture constraints**
- ledger হাতে ঠিক করা নিষেধ; Reversal/Adjustment বা snapshot restore।
- সরাসরি SQL লেখা নেই।
- Scenario permission এড়ায় না।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Test (`test`)
Title
Capability UAT script, দল খ (fixed asset, period-end ও year-end, budgeting ও reporting)
Branch Name
`feature/accounting-uat-group-b`
View Order
96
Description
**Summary**
দল খ: fixed asset, recurring/deferral/allocation, close checklist, year-end, budget, forecast, report designer, KPI — scripted scenario, expected result, চালানো। period-end থেকে management reporting পর্যন্ত সব ঢাকা পড়বে।

**Task serial**
`ACC-P17-03 — Capability UAT script, দল খ (fixed asset, period-end ও year-end, budgeting ও reporting)` · Test · Phase 17, তৃতীয় card

**Depends on**
- `ACC-P17-01`
- সব P11, P12, P13 card (Fixed asset, period-end/recurring/year-end, budgeting/forecast/management reporting; সবই অন্য tab-এ, এই document-এ নেই)

**Relation: আগের task**
- `P17-01` দিয়েছে পরিবেশ, ছাঁচ, persona, severity, snapshot-নিয়ম (year-end scenario আলাদা snapshot-এ)।
- Phase 11 (Fixed Asset Accounting), Phase 12 (Period-End, Recurring, Year-End), Phase 13 (Budgeting, Forecast, Management Reporting): এই card-এর মূল ইনপুট; তাদের কোনো card এই document-এ নেই। Phase 12-এর recurring Phase 6-এর template (`ACC-P6-06`) ব্যবহার করে বলে `P6-06` এর ফল পরোক্ষে এই card-এর recurring scenario-র ভিত্তি (source-এ সরাসরি বলা নেই)।
- `P3-11` (Period বন্ধ), `P3-12` ও `acc_period_events` (Phase 3): close checklist ও period events পড়ার ভিত্তি।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P17-05` (E2E): E3 পথে (asset acquisition, depreciation, disposal) ও E1 পথে (period close, year-end) এই দলের ক্ষমতা জোড়া লাগে।
- `P17-06` (negative) ও `P17-15` (অন্য tab-এ)।

**Related tables / schema**
নতুন table নেই। পড়া: `acc_journals`, `acc_journal_lines`, `acc_period_balances`, `acc_periods`, `acc_period_events`। RC চালানো: RC11, RC16, RC17 (Phase 11, 12, 13-এর check; সংজ্ঞা এই document-এ নেই) ও RC1।

**Business rules**
- R17.2, R17.4, R17.5 (year-end scenario সবসময় snapshot-এ)।
- চালানো সব API ও job দিয়ে (`AccountingJob`)।
- Year-end scenario আলাদা snapshot-এ, কারণ ফেরানো যায় না।
- ব্যর্থ হলে snapshot restore, ledger হাতে ঠিক নয়।
- Year-end carry-forward-এর পর opening balance = আগের closing balance (RC1)।
- Negative case-এ প্রত্যাশিত error prefix: FA_, PER_, REC_, DEF_, ALC_, YER_, BUD_, RPD_।
- Year-end scenario দুইবার চালানো (একবার snapshot থেকে, ফল একই কিনা)।
- **Open point:** UAT-11.x থেকে UAT-13.x scenario তালিকা Blueprint ২৭.৯ক-এ, এই document-এ নেই। "OD: কোনোটি নয়" লিখিত।
- **Open point:** RC11, RC16, RC17 ও error prefix-গুলো (FA_, PER_, REC_ ইত্যাদি) অন্য tab-এ সংজ্ঞায়িত; এই card-এ অনুমানে কিছু ধরা নিষেধ।
- **Open point:** Year-end scenario "ফেরানো যায় না" (production-এ), তাই snapshot-ভিত্তিক; দ্বিতীয়বার চালানোর "একই ফল" তুলনার মানদণ্ড (কোন সংখ্যা/hash) source-এ নেই।

**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 হাতে ঠিক নয়।

**Task scope**
1. Phase 11 scenario (fixed asset)।
2. Phase 12 scenario (recurring, deferral, allocation, close checklist); year-end আলাদা snapshot-এ।
3. Phase 13 scenario (budget, forecast, report designer, KPI)।
4. Defect triage ও পুনরায় চালানো।

**Out of scope**
দল ক ও গ (`P17-02`, `P17-04`)। Cross-module E2E (`P17-05`)। Production year-end কার্যক্রম।

**Contract (API)**
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 ও পুনরায় চালানো

**Definition of done**
- সাধারণ DoD পূরণ।
- AC17.3 সবুজ; year-end carry-forward-এর RC1 সবুজ।
- Year-end দুইবার চালিয়ে ফল অভিন্ন।
- Accountant ও controller-এর সই জমা।

**Architecture constraints**
- Year-end scenario কখনো স্থায়ী পরিবেশে নয়; সবসময় আলাদা snapshot।
- ledger হাতে ঠিক করা নিষেধ।
- Job চালানো `AccountingJob` base ধরে।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Test (`test`)
Title
Capability UAT script, দল গ (posting rules, integration, consolidation, compliance, migration)
Branch Name
`feature/accounting-uat-group-c`
View Order
97
Description
**Summary**
দল গ: posting rule engine, source integration (Billing, Procurement, Payroll, Inventory, Project, Commissioning, Asset event), consolidation, auditor access, audit chain, import, archive, API ও webhook — scripted scenario ও চালানো। প্রমাণ: কোনো source event সরাসরি ledger-এ লেখে না; Draft → approval → Post পথে যায়।

**Task serial**
`ACC-P17-04 — Capability UAT script, দল গ (posting rules, integration, consolidation, compliance, migration)` · Test · Phase 17, চতুর্থ card

**Depends on**
- `ACC-P17-01`
- সব P14, P15, P16 card (posting rules ও source integrations; multi-entity/intercompany/consolidation; compliance, audit, data lifecycle, migration ও API; সবই অন্য tab-এ)

**Relation: আগের task**
- `P17-01` দিয়েছে পরিবেশ, ছাঁচ, persona (auditor ও API client role সহ)।
- Phase 4-এর Finance Event Inbox (`P4-02`), consumer job (`P4-03`), Event থেকে Draft Journal (`P4-06`), RC8 (`P4-10`) এই document-এ আছে: Asset event-এর পথ। Phase 14 এই কাঠামো বাকি source-এ বাড়ায় (অন্য tab)।
- Phase 14, 15, 16: অন্য tab-এ; এদের সংজ্ঞা এই document-এ নেই।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P17-05` (E2E): E1 (Project event) ও E2 (Payroll event) ও E3 (Asset event) পথের সুস্থ উৎস।
- `P17-07` (data migration মহড়া): source এই card-কে নির্ভরতা রেখেছে; Phase 16 import UAT এখানে।
- `P17-06` (negative: malformed webhook ও API token) ও `P17-15` (অন্য tab-এ)।

**Related tables / schema**
নতুন table নেই। পড়া: `acc_finance_event_inbox`, `acc_finance_event_journal_links`, `acc_journals`, `acc_journal_lines`। RC চালানো: RC15, RC18 (Phase 14-16-এর check; সংজ্ঞা এই document-এ নেই)।

**Business rules**
- R17.2, R17.4, R17.6 (event replay-এ নতুন journal নয়, শুধু লিংক পুনরুদ্ধার)।
- প্রতিটা source module থেকে নমুনা event inbox-এ আসে, Draft journal তৈরি হয়, মানুষ approve করে, post হয়।
- Event ফেরত চালানো (replay) স্বাভাবিক সংখ্যায় একই ফল দেয় কিনা দেখা (idempotency)।
- কোনো source event সরাসরি ledger-এ লেখে না (G9 ধরে)।
- প্রতিটা source module-এর অন্তত একটা সুস্থ ও একটা ভুল event।
- Negative case error prefix: RUL_, EVC_, GRP_, ICO_, CON_, AUD_, IMP_, ARC_, API_।
- **Open point:** UAT-14.x থেকে UAT-16.x scenario তালিকা Blueprint-এ, এই document-এ নেই।
- **Open point:** "G9" কী (সম্ভবত Phase 9 বা একটা golden/invariant) source-এ সংজ্ঞা নেই; "OD-105b" কেন এখানে (volume) লিখিত নেই।
- **Open point:** Source module-গুলোর মধ্যে এই document-এ শুধু Asset event reader (`P4-04`) আছে; Billing, Procurement, Payroll, Inventory, Project, Commissioning-এর event কাঠামো Phase 14-এ (অন্য tab), তাই scenario-র input ছাঁচ অজানা।

**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 চালানো।

**Task scope**
1. Phase 14 scenario, প্রতি source আলাদা (Billing, Procurement, Payroll, Inventory, Project, Commissioning, Asset event)।
2. Phase 15 scenario (consolidation, intercompany, elimination)।
3. Phase 16 scenario (auditor access, audit chain, import, archive, API ও webhook)।
4. Defect triage ও পুনরায় চালানো।

**Out of scope**
দল ক ও খ। E2E (`P17-05`)। Data migration মহড়া বাস্তব volume-এ (`P17-07`)। Pen test (`P17-11` ইত্যাদি, অন্য tab)।

**Contract (API)**
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 ও পুনরায় চালানো

**Definition of done**
- সাধারণ DoD পূরণ।
- AC17.3 সবুজ; কোনো source event সরাসরি ledger-এ লেখে না প্রমাণিত।
- Replay-এ নতুন journal নয় প্রমাণিত।
- Tech lead ও accountant-এর সই।

**Architecture constraints**
- Event কখনো ledger-এ সরাসরি লেখে না; সবসময় Draft → approval → Post।
- Replay শুধু লিংক পুনরুদ্ধার করে, নতুন journal বানায় না।
- ledger হাতে ঠিক করা নিষেধ।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Test (`test`)
Title
Cross-module end-to-end scenario ও golden test
Branch Name
`feature/accounting-uat-cross-module-e2e`
View Order
98
Description
**Summary**
চারটা cross-module পথ (E1–E4) শুরু থেকে শেষ — প্রতি ধাপে ledger ও RC যাচাই। এক capability আলাদায় ঠিক থাকলেও জোড়ায় কোথায় ভাঙছে, E2E-তে ধরা পড়বে।

**Task serial**
`ACC-P17-05 — Cross-module end-to-end scenario ও golden test` · Test · Phase 17, পঞ্চম card

**Depends on**
- `ACC-P17-02`
- `ACC-P17-03`
- `ACC-P17-04`

**Relation: আগের task**
- `P17-02` (দল ক): journal, FX, tax, subledger, bank-এর আলাদা প্রমাণ; E1 এর open item ও bank match, tax return, E4 এর foreign currency ও revaluation এর ভিত্তি।
- `P17-03` (দল খ): fixed asset, period-end, year-end ও reporting-এর আলাদা প্রমাণ; E1 (period close, year-end), E3 (capitalization, depreciation, disposal)।
- `P17-04` (দল গ): posting rules ও source integration, consolidation; E1 (Project event, consolidation journal), E2 (Payroll event), E3 (Asset event)।
- এই তিন card পাস না হলে E2E-র ব্যর্থতা কোন capability-র তা আলাদা করা যায় না।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P17-07` (data migration মহড়া), `P17-08` (parallel run), `P17-15` (অন্য tab-এ): source এই card-কে এদের "অবরুদ্ধ করে"-তে রেখেছে। বাস্তবে `P17-08` ও `P17-09` এই card-কে নির্ভরতা বলেছে; `P17-07` বলেনি (phase-স্তরের open point)।
- `P17-09` (scale ও performance): E2E পাস হওয়া কাঠামোর ওপর load।

**Related tables / schema**
নতুন table নেই। পথ জুড়ে ব্যবহৃত: `acc_finance_event_inbox`, `acc_journals`, `acc_journal_lines`, open item (`acc_` receivable table, নাম Phase 9-এ), bank statement line match ও reconciliation (Phase 10), tax return (Phase 8), `acc_periods`, `acc_period_events`, year-end ও consolidation journal। RC1 থেকে RC18-এর প্রযোজ্যগুলো।

**Business rules**
- R17.2, R17.4, R17.7 (E2E-তে পরের ধাপ আগের ধাপের posted ফলের উপর, কখনো test data হাতে বসিয়ে নয়)।
- প্রতি ধাপের পর RC1 থেকে RC18-এর প্রযোজ্যগুলো চলে।
- প্রতিটা পথ একটা snapshot থেকে শুরু; ব্যর্থ হলে ওই ধাপ থেকে আবার নয়, snapshot থেকে শুরু।
- সব persona আলাদা login; এক ব্যবহারকারী পুরো পথ নয় (SoD সত্যিকারের মতো)।
- E1 পথের হাতে চালানো UAT-ও (accountant দেখে)।
- **চার পথ:**
  - **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।
- **Open point:** UAT-E1 থেকে E4 ও G17.1 থেকে G17.4-এর সংখ্যা-সহ expected result Blueprint ২৭.৯গ ও ২৭.৯ঙ-এ; এই document-এ নেই। OD-105b (data volume) সিদ্ধান্ত লিখিত নেই।
- **Open point:** E1 একই পথে এত ধাপ (Phase 4, 8, 9, 10, 12, 15) জুড়ে হওয়ায়, কোনো একটা Phase-এর feature flag বন্ধ থাকলে পথ কীভাবে চলবে, এবং সব flag চালু অবস্থার রাখা পরিবেশে কীভাবে আনা হবে source-এ নেই।

**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 থেকে শুরু।

**Task scope**
1. E1 পথ।
2. E2 পথ।
3. E3 পথ।
4. E4 পথ।
5. Golden test স্বয়ংক্রিয় করা (G17.1 থেকে G17.4, MySQL-এ)।
6. প্রতি ধাপের RC প্রমাণ।

**Out of scope**
আলাদা capability UAT (`P17-02` থেকে `P17-04`)। Negative/security (`P17-06`)। Volume/performance (`P17-09`)।

**Contract (API)**
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 প্রমাণ

**Definition of done**
- সাধারণ DoD পূরণ।
- AC17.4, AC17.5 সবুজ; G17.1 থেকে G17.4 MySQL-এ স্বয়ংক্রিয় সবুজ।
- E1 হাতে চালানো UAT accountant দেখেছে।
- Accountant ও finance owner-এর সই।

**Architecture constraints**
- পরের ধাপ সবসময় আগের ধাপের posted ফলের ওপর; test data হাতে বসানো নিষেধ।
- ব্যর্থ হলে ধাপ নয়, snapshot থেকে পুরো পথ আবার।
- SoD সত্যিকারের: আলাদা ব্যবহারকারী।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P17-06 — Negative, security ও isolation scenario

Project
ERPFlow
Module
Accounting Module
Type
Test (`test`)
Title
Negative, security ও isolation scenario
Branch Name
`feature/accounting-uat-negative-security-isolation`
View Order
99
Description
**Summary**
Negative ও security: ভুল input বা অনুমতি ছাড়া চেষ্টায় ledger অপরিবর্তিত, error `{code, message, field, details, correlation_id}` ঠিক মতো (stack trace বা অন্য company-র তথ্য নয়), company isolation — এক company-র token দিয়ে অন্যের id 404।

**Task serial**
`ACC-P17-06 — Negative, security ও isolation scenario` · Test · Phase 17, ষষ্ঠ card

**Depends on**
- `ACC-P17-02`
- `ACC-P17-03`
- `ACC-P17-04`

**Relation: আগের task**
- `P17-02`, `P17-03`, `P17-04` পাস হয়ে প্রতিটা capability-র সুস্থ পথ প্রমাণিত; এই card তাদের ভুল পথ পরীক্ষা করে।
- Phase 1-এর `P1-12` (tenancy, audit, error-এর সাধারণ ব্যবস্থা) ও composite FK নিয়ম (AC1.1) এবং `P2-10` SoD service, `P2-12` posting service: এগুলোর বাধা এখানে সরাসরি পরীক্ষিত হয়।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P17-11` (অন্য tab-এ, এই document-এ নেই) ও `P17-15` (অন্য tab-এ): source এদের এই card-এর ওপর নির্ভরশীল বলেছে।
- এই card-এর "security সই" পরের release-control gate-এ প্রমাণ হিসেবে যায়।

**Related tables / schema**
নতুন table নেই। পড়া: `activity_logs`, `acc_journals` (গণনা আগে ও পরে), `acc_idempotency_keys`।

**Business rules**
- R17.8 (negative scenario-র পাস মানে ফল অপরিবর্তিত ও error আকার সঠিক), R17.9 (বিষয়বস্তু source-এ লেখা নেই)।
- প্রতিটা negative scenario-তে প্রত্যাশা: ledger-এ কোনো বদল নেই; `acc_idempotency_keys`-এ প্রবেশ বা না-প্রবেশ নিয়ম মতো; audit-এ ব্যর্থ চেষ্টার রেকর্ড।
- **Isolation:** দুই company-তে একই code ও id; এক company-র token দিয়ে অন্যের id ডাকলে 404।
- Error আকার `{code, message, field, details, correlation_id}`; কখনো stack trace বা অন্য company-র তথ্য নয়।
- অনুমতিহীন persona ইচ্ছা করে বানানো হয়।
- সবগুলো Cycle 3-এ হাতে ও স্বয়ংক্রিয়ে।
- **Open point:** UAT-N1 থেকে N14 তালিকা Blueprint ২৭.৯ঘ-এ, এই document-এ নেই। OD-107 লিখিত নেই (সম্ভবত security/penetration সংক্রান্ত)।
- **Open point:** R17.9 কী বলে অজানা; "Cycle 3" মানে P17-01 cycle calendar-এর তৃতীয় cycle কিনা নিশ্চিত নয়।
- **Open point:** 404 বনাম `ACC_COMPANY_FORBIDDEN` (403): §২.৩ তালিকায় `ACC_COMPANY_FORBIDDEN` আছে, অথচ এই card isolation-এ 404 চায় (অন্য company-র id অনুমান করলে 404, `P6`-এর IDOR নীতি অনুযায়ী)। কোন পথে কোন কোড আসবে তা মেলাতে হবে।

**DB flow**
- প্রতিটা negative scenario-তে প্রত্যাশা: ledger-এ কোনো বদল নেই; `acc_idempotency_keys`-এ প্রবেশ বা না-প্রবেশ নিয়ম মতো; audit-এ ব্যর্থ চেষ্টার রেকর্ড।
- Isolation: দুই company-তে একই code ও id; এক company-র token দিয়ে অন্যের id ডাকলে 404।
- পড়া: `activity_logs`, `acc_journals` গণনা আগে ও পরে।

**Task scope**
1. অনুমতি ও SoD।
2. Company isolation।
3. Duplicate, locked period, ভুল mapping।
4. Malformed webhook ও API token।
5. Audit chain ছেঁড়া।
6. ব্যর্থ posting ও retry।

**Out of scope**
Penetration test (`PT-n`, `P17-11` ইত্যাদি অন্য tab)। সুস্থ পথের UAT। Accessibility।

**Contract (API)**
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

**Definition of done**
- সাধারণ DoD পূরণ।
- AC17.6, AC17.7 সবুজ; `ReleaseNegativeTest` ও `CompanyIsolationTest` MySQL-এ সবুজ।
- প্রতিটা negative-এ ledger গণনা অপরিবর্তিত ও error আকার সঠিক।
- Security সই জমা।

**Architecture constraints**
- Negative পাস মানে কেবল error নয়: ফল অপরিবর্তিত থাকা ও audit রেকর্ড থাকা।
- Error কখনো stack trace বা অন্য company-র তথ্য দেখায় না।
- অনুমতিহীন persona ইচ্ছা করে বানানো, scenario permission বাদ দিতে পারে না।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Operations + Test (`ops` + `test`)
Title
Data migration মহড়া, বাস্তব volume-এ
Branch Name
`feature/accounting-uat-migration-rehearsal`
View Order
100
Description
**Summary**
Migration মহড়া: প্রতি company-র বাস্তব data দিয়ে import তিনবার (dry-run, copy+মিল, সময়+rollback) — সময়, control total, ত্রুটির হার মাপা। production cutover-এ surprise নেই; শেষ মহড়ার সময় cutover জানালার ভেতরে।

**Task serial**
`ACC-P17-07 — Data migration মহড়া, বাস্তব volume-এ` · Ops + Test · Phase 17, সপ্তম card

**Depends on**
- `ACC-P17-04`
- সব P16 import card (Phase 16: Migration ও import, অন্য tab-এ)
- `ACC-P5-02` (Cutover মহড়া)

**Relation: আগের task**
- `P17-04` (দল গ) Phase 16-এর import ও migration UAT প্রমাণ করেছে; এই card বাস্তব volume-এ সেটাকে সময় মেপে চালায়।
- Phase 16 `ImportService` (dry-run, copy) ও master data mirror: মহড়ার যন্ত্র।
- `P5-02` দিয়েছে cutover মহড়ার পদ্ধতি (ধাপ ১ থেকে ৭, বারবার চালানো, সময় মাপা, trial balance তুলনা); এই card সেটাই enterprise volume ও Phase 16 import দিয়ে বাড়ায়।
- `P2-14` (Opening journal-এর Post) ও `PostingService` (`P2-12`): copy ধাপে opening journal এই পথে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P17-08` (parallel run): migration-এ মেলানো opening balance ছাড়া parallel run শুরু হয় না।
- `P17-09` (scale ও performance): source এই card-কে নির্ভরতায় রেখেছে (volume ও পরিবেশ)।
- `P17-15`, `P17-16` (অন্য tab-এ): production cutover-এর প্রস্তুতি।

**Related tables / schema**
নতুন table নেই। মহড়ার পরিবেশে Phase 16 `ImportService` (opening journal ও master data mirror লেখে, `PostingService` দিয়ে)। RC1 থেকে RC18-এর প্রযোজ্যগুলো, legacy trial balance-এর সাথে মিল।

**Business rules**
- R17.1, R17.3, R17.14 (production-এ migration rollback নেই, শুধু মহড়ায় restore)।
- মহড়া তিনবার: dry-run, copy ও মিল, সময়-মাপা ও rollback।
- Dry-run দুইবার একই ফল (idempotency); copy দুইবার নতুন journal বানায় না।
- Rollback মহড়া = snapshot restore। Restore-এর পর RC সবুজ।
- সময়, মেমরি, lock অপেক্ষা মাপা।
- ব্যর্থ হলে snapshot restore ও সমস্যার বিবরণ সহ আবার।
- Legacy trial balance = নতুন opening trial balance (control total শূন্য পার্থক্য)।
- **Open point:** MIG-1 থেকে MIG-6 তালিকা Blueprint ২৭.৯চ-এ, এই document-এ নেই। OD-105b (volume) ও OD-119 লিখিত নেই; "cutover জানালা" (বরাদ্দ সময়) ও "ত্রুটির হারের লিখিত সীমা"র সংখ্যা source-এ নেই।
- **Open point:** `P17-05` এই card-কে "অবরুদ্ধ করে" বলেছে, কিন্তু এই card-এর নির্ভরতায় `P17-05` নেই।
- **Open point:** Phase 5 `P5-02` Opening Balance গ্রিড হাতে ঢোকানোর পদ্ধতি; এখানে Phase 16 import দিয়ে — দুই পদ্ধতির সম্পর্ক (কোনটা production পথ) source-এ বলা নেই।

**DB flow**
মহড়ার পরিবেশে:
1. Snapshot।
2. Phase 16 `ImportService` দিয়ে dry-run (কিছু লেখে না, শুধু ত্রুটি দেখায়)।
3. Copy (opening journal ও master data mirror লেখে, `PostingService` দিয়ে)।
4. তারপর RC1 থেকে RC18-এর প্রযোজ্যগুলো ও legacy trial balance-এর সাথে মিল।
5. Rollback মহড়া = snapshot restore।
6. সময়, মেমরি, lock অপেক্ষা মাপা।

ব্যর্থ হলে snapshot restore, আর সমস্যার বিবরণ সহ আবার।

**Task scope**
1. Volume নিশ্চিত (OD-105b)।
2. মহড়া 1: dry-run।
3. মহড়া 2: copy ও মিল।
4. মহড়া 3: সময়-মাপা ও rollback।
5. Opening balance মিল।
6. Cutover ঘড়ি ও runbook।

**Out of scope**
আসল production cutover। Parallel run (`P17-08`)। Import কোডের পরিবর্তন (Phase 16)।

**Contract (API)**
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

**Definition of done**
- সাধারণ DoD পূরণ।
- AC17.8 সবুজ: তৃতীয় মহড়ার সময় জানালার ভেতরে; control total শূন্য পার্থক্য; ত্রুটির হার সীমার ভেতরে।
- Dry-run ও copy idempotent প্রমাণিত।
- প্রতি company-র মহড়া প্রতিবেদন ও cutover runbook জমা।

**Architecture constraints**
- Production-এ migration rollback নেই; rollback শুধু মহড়ায় snapshot restore।
- Copy `PostingService` ছাড়া ledger-এ লেখে না।
- Import dry-run কিছু লেখে না।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

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

Project
ERPFlow
Module
Accounting Module
Type
Operations + API (`ops` + `api`)
Title
Parallel run ও তুলনা
Branch Name
`feature/accounting-uat-parallel-run`
View Order
101
Description
**Summary**
Parallel run: নির্ধারিত period জুড়ে পুরোনো ও নতুন পদ্ধতি একসাথে; প্রতিদিন ও period-end-এ trial balance, aging, bank, tax, depreciation মিল। অব্যাখ্যাত discrepancy শূন্য (accepted exception-এ finance owner-এর সই) হলেই পুরোনো পদ্ধতি বন্ধ।

**Task serial**
`ACC-P17-08 — Parallel run ও তুলনা` · Ops + API · Phase 17, অষ্টম card

**Depends on**
- `ACC-P17-05` (cross-module E2E)
- `ACC-P17-07` (migration মহড়া: opening balance মিলে গেছে)

**Relation: আগের task**
- `P17-05` প্রমাণ করেছে চার পথে নতুন system ভেতরে সঙ্গতিপূর্ণ।
- `P17-07` দিয়েছে মিলে যাওয়া opening balance ও Phase 16 import mapping (legacy account code → নতুন account); parallel run-এর legacy ফাইল সেই mapping ব্যবহার করে।
- `P3-04` (Trial Balance), `P3-02` (`acc_period_balances`), `P3-10` (`acc_report_exports`), `P5-06` (`acc_enablement_log`): তুলনার ইনপুট ও আউটপুট table।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P17-15` (অন্য tab-এ, এই document-এ নেই): parallel run exit সই পরের release-control gate-এর প্রমাণ।
- Parallel run-এর exit শর্ত (`REL_PARALLEL_RUN_OPEN` না থাকা) পুরোনো পদ্ধতি বন্ধের সিদ্ধান্তের শর্ত।

**Related tables / schema**
নতুন table নেই।
- পড়া: `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-এ কোনো লেখা নেই।

**Business rules**
- R17.10, R17.11 (বিষয়বস্তু source-এ লেখা নেই)।
- তুলনা idempotent (period key + দিন): ব্যর্থ হলে ওই দিনের তুলনা আবার চালানো যায়।
- **Discrepancy শ্রেণি:** timing, rounding, mapping, rule পার্থক্য, legacy ভুল, নতুন system defect।
- **অবস্থা:** `open`, `explained`, `corrected`, `accepted` (অনুমোদন সহ)।
- Period-end তুলনায় trial balance, aging, bank, tax, depreciation।
- Accepted exception-এ finance owner-এর সই।
- Tolerance সীমায় ও সীমার বাইরে দুই মান পরীক্ষিত।
- ledger-এ কোনো লেখা নেই।
- **Open point:** OD-105 (parallel run-এর period সংখ্যা), OD-105a (script বা API), OD-108b লিখিত নেই; tolerance (rounding/timing সহনীয় পার্থক্য)-এর সংখ্যা source-এ নেই।
- **Open point:** `acc_enablement_log`-এর `flag` মান (P5-06: `accounting.enabled`, `event_intake.*`, `gate:*`) তে parallel run এন্ট্রির জন্য নতুন মান লাগবে (যেমন `gate:parallel_run`); source নির্দিষ্ট করেনি এবং `action` মান (`on`, `off`, `gate_passed`, `gate_revoked`) এর কোনটা ব্যবহৃত হবে তাও।
- **Open point:** `acc_report_exports.format` `xlsx`/`pdf` (P3-10); তুলনা report কোন `report` নাম ও format-এ সংরক্ষিত হবে source-এ নেই।
- **Open point:** Type `api` (OD-105a: script বা API) — endpoint-টি Contract-এ দেওয়া; কিন্তু "script" বেছে নিলে endpoint বাদ যাবে কি না OD-105a-এর ওপর।

**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 + দিন)।

**Task scope**
1. Legacy ফাইল ছাঁচ ও mapping।
2. দৈনিক তুলনা।
3. Period-end তুলনা (trial balance, aging, bank, tax, depreciation)।
4. Discrepancy workflow (শ্রেণি, অবস্থা, approval)।
5. Exit criteria পরীক্ষা।

**Out of scope**
Migration মহড়া (`P17-07`)। পুরোনো পদ্ধতি বন্ধের সিদ্ধান্ত (`P17-15`-ধরনের release-control card, অন্য tab)। Ledger সংশোধন।

**Contract (API)**
- `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 পরীক্ষা

**Definition of done**
- সাধারণ DoD পূরণ।
- AC17.9 সবুজ: অব্যাখ্যাত discrepancy শূন্য, control total শূন্য পার্থক্য, accepted exception-এ সই।
- বানানো ভুল সঠিক শ্রেণিতে ধরা পড়ে প্রমাণিত; তুলনা idempotent।
- Exit সই জমা।

**Architecture constraints**
- Parallel run ledger-এ কিছু লেখে না।
- তুলনা idempotent: একই period key + দিনে আবার চালালে একই ফল।
- `accepted` অবস্থা শুধু `accounting.release.approve`-ধারীর, সই সহ।

Owner
Dev C
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P17-09 — Scale ও performance test

Project
ERPFlow
Module
Accounting Module
Type
Test + Operations (`test` + `ops`)
Title
Scale ও performance test
Branch Name
`feature/accounting-uat-scale-performance`
View Order
102
Description
**Summary**
Scale/performance (PF-1–PF-8): নির্ধারিত volume-এ p95 লক্ষ্য, deadlock, lock ক্রম, bottleneck — benchmark প্রতিবেদন ও capacity plan। production load আগে জানা যাবে; fix সংশ্লিষ্ট Phase-এর card-এ, expand-only index দিয়ে।

**Task serial**
`ACC-P17-09 — Scale ও performance test` · Test + Ops · Phase 17, নবম card

**Depends on**
- `ACC-P17-05`
- `ACC-P17-07`
- সব P16 partitioning/archive card (Phase 16, অন্য tab-এ)

**Relation: আগের task**
- `P17-05` প্রমাণ করেছে কার্যকারিতা; `P17-07` দিয়েছে বাস্তব volume-এর data ও আমদানির যন্ত্র।
- Phase 16 partitioning/archive: বড় volume সামলানোর কাঠামো।
- Phase 6-এর perf লক্ষ্য: `P6-13` (10M line × 4 dimension, TB by dimension p95 3 সেকেন্ড, GL প্রথম পৃষ্ঠা 1 সেকেন্ড), `P6-10` (500 line × 4 dimension `validateLines` 150ms), `P6-09` (backfill 20,000 line/সেকেন্ড), `P6-07` (৫,০০০ সারি যাচাই ৬০ সেকেন্ড)। এগুলো PF-2/PF-3-এর ভিত্তিসংখ্যা হিসেবে কাজে লাগে (source-এ সরাসরি যুক্ত বলা নেই)।
- `P3-14` (Phase 3 performance), `P5-05` (শেষ security ও performance যাচাই): আগের পরিমাপ।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P17-10` (failover ও DR drill): source `P17-10` কে এই card-এর ওপর নির্ভরশীল রেখেছে (লোড চলাকালীন failover)।
- `P17-15` (অন্য tab-এ)।

**Related tables / schema**
নতুন table নেই। আলাদা scale পরিবেশে সিনথেটিক data। পড়া ও মাপা: slow query log, `EXPLAIN`, lock wait, deadlock, queue দৈর্ঘ্য।

**Business rules**
- R17.1, R17.16 (sev1 বা sev2 performance defect gate আটকায়)।
- Lock ক্রম বজায় থাকে কিনা দেখা (journal, approval, account, period, voucher counter)।
- ব্যর্থ হলে সংশোধন সম্পর্কিত phase-এর card-এ (expand-only migration দিয়ে index), এই card-এ নয়।
- **পরীক্ষা তালিকা:** 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)।
- **Open point:** p95 লক্ষ্যের সংখ্যা (PF-1 থেকে PF-8) source-এ নেই (Blueprint-এ); OD-105b (volume), OD-119 লিখিত নেই।
- **Open point:** এই card-এ বর্ণিত lock ক্রম ("journal, approval, account, period, voucher counter") Phase 6-এর চূড়ান্ত ক্রম (journal, approval request, account, period, number series `FOR SHARE`, counter `FOR UPDATE`, শেষে dimension value `FOR SHARE`)-এর চেয়ে ছোট। Phase 6 বাস্তবায়িত হলে "voucher counter" মানে number series counter ও dimension value lock-ও পরীক্ষায় ধরতে হবে।
- **Open point:** PF-7 "সব RC সবুজ" মানে RC1 থেকে RC18, `RC1d/m/n` সহ কি না নিশ্চিত নয়।
- **Open point:** "capacity পরিকল্পনা (কত বাড়লে কী বাড়াতে হবে)" এর ইনপুট (cpu, memory, DB node আকার) source-এ নেই।

**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-এ নয়।

**Task scope**
1. Volume বানানোর tool।
2. PF-1 (সমান্তরাল posting), PF-2 (report)।
3. PF-3 (bulk import), PF-4 (depreciation ও revaluation), PF-5 (consolidation)।
4. PF-6 (event intake burst), PF-8 (period close চলাকালীন posting)।
5. PF-7 (8 ঘণ্টা soak, মিশ্র load)।
6. Bottleneck ও পুনরায় মাপা।

**Out of scope**
Index বা কোড সংশোধন (সংশ্লিষ্ট Phase-এর card)। DR drill (`P17-10`)। Production capacity ক্রয়।

**Contract (API)**
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 ও পুনরায় মাপা

**Definition of done**
- সাধারণ DoD পূরণ।
- AC17.10 সবুজ; প্রতিটা PF-এর ফল সংখ্যায় লিখিত।
- Deadlock শূন্য বা স্বয়ংক্রিয় retry-তে সমাধান; PF-7-এ সব RC সবুজ।
- Benchmark প্রতিবেদন ও capacity পরিকল্পনা জমা; কোনো sev1/sev2 performance defect খোলা নেই।

**Architecture constraints**
- সংশোধন কখনো এই card-এ নয়; সংশ্লিষ্ট Phase-এর card-এ, expand-only migration দিয়ে।
- scale পরিবেশ আলাদা; production-এ কোনো লোড নয়।
- Lock ক্রম ভাঙা কোনো কোড গ্রহণযোগ্য নয়।

Owner
Dev A
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

---

## ACC-P17-10 — Failover ও DR drill

Project
ERPFlow
Module
Accounting Module
Type
Operations + Test (`ops` + `test`)
Title
Failover ও DR drill
Branch Name
`feature/accounting-uat-failover-dr-drill`
View Order
103
Description
**Summary**
Failover/DR drill (DR-1–DR-6): RPO ও RTO মাপ, লোড চলাকালীন primary বন্ধ, restore/failover-র পর journal গণনা ও posted hash, inbox identity — কোনো event হারায় না, duplicate journal হয় না; RC1–RC18 ও audit chain সবুজ।

**Task serial**
`ACC-P17-10 — Failover ও DR drill` · Ops + Test · Phase 17, দশম (এই document-এর শেষ) card

**Depends on**
- `ACC-P17-09`
- সব P16 DR card (Phase 16: backup/restore/DR, অন্য tab-এ)
- `ACC-P5-04` (Rollback ও জরুরি বন্ধ পরিকল্পনা)

**Relation: আগের task**
- `P17-09` দিয়েছে লোড-সক্ষম পরিবেশ ও মিশ্র load (PF-7 soak): DR drill লোড চলাকালীন primary বন্ধ করে।
- Phase 16 DR card: backup, restore, failover কাঠামো ও runbook (অন্য tab-এ)।
- `P5-04` দিয়েছে rollback স্তর (flag বন্ধ, code আগের version-এ, module বন্ধ) ও "করা যাবে না" তালিকা (production-এ trigger-এর `down()` নিষেধ ইত্যাদি) এবং migration সামঞ্জস্য তালিকা; drill এই runbook-এর সংশোধিত রূপ বানায়।
- `P4-02` (Finance Event Inbox, identity-ভিত্তিক idempotency) ও `P4-03` (consumer job): DR-4-এ event হারানো ও দুর্বার journal না হওয়ার ভিত্তি।
- `P3-13` (RC) ও Phase 16 audit chain: drill-এর পরে যাচাই।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P17-15`, `P17-19` (অন্য tab-এ, এই document-এ নেই): source এদের এই card-এর ওপর নির্ভরশীল বলেছে; DR drill সবুজ না হলে release-control gate (go/no-go) পার হয় না।
- সংশোধিত runbook পরের hypercare/rollback card-এর ইনপুট (অন্য tab)।

**Related tables / schema**
নতুন table নেই। পড়া: সব `acc_journals` (গণনা ও posted hash), `acc_finance_event_inbox` ও outbox, `acc_reconciliation_runs` (RC1 থেকে RC18), Phase 16 audit chain।

**Business rules**
- R17.18 (বিষয়বস্তু source-এ লেখা নেই)।
- 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)।
- মানুষ runbook অনুসরণ করে (স্বয়ংক্রিয় script নয়)।
- ব্যর্থ হলে কারণ লিখে আবার drill।
- **Drill তালিকা:** 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।
- RPO ও RTO লক্ষ্য OD-114 নিশ্চিত না হলে প্রস্তাব 15 মিনিট ও 4 ঘণ্টা।
- **Open point:** OD-114 (RPO/RTO) ও OD-115 (hosting এলাকা) লিখিত সিদ্ধান্ত নেই; প্রস্তাবিত 15 মিনিট/4 ঘণ্টা আপাতত ধরা, client সম্মতি লাগবে।
- **Open point:** "কোনো event হারায় নি, কোনো দুর্বার journal নেই" — মূল ঘটনার বিন্যাস "outbox" বলতে কোন outbox (Asset-এর `asset_finance_events` outbox, যা Accounting শুধু পড়ে) তা source-এ সুনির্দিষ্ট নয়।
- **Open point:** "দুর্বার" বানান সম্ভবত "দুবার" (duplicate) অর্থে; ধরা হয়েছে নকল journal।
- **Open point:** `P17-10` কে `P17-09` পাস করতেই হবে (নির্ভরতা), কিন্তু DR drill-এ লোডের মাত্রা ("লোড চলাকালীন") কত হবে source-এ নেই।

**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।

**Task scope**
1. Drill পরিকল্পনা ও অনুমতি।
2. DR-1 (DB primary বন্ধ), DR-2 (application node বন্ধ)।
3. DR-3 (point-in-time restore)।
4. DR-4 (queue ও inbox পুনরুদ্ধার)।
5. DR-5 (hosting এলাকা হারানো, যদি প্রযোজ্য)।
6. DR-6 ও প্রতিবেদন।

**Out of scope**
Backup/DR ব্যবস্থার নির্মাণ (Phase 16)। স্বয়ংক্রিয় failover script (drill মানুষ করে)। Go/no-go সিদ্ধান্ত (পরের release-control card, অন্য tab)।

**Contract (API)**
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 ও প্রতিবেদন

**Definition of done**
- সাধারণ DoD পূরণ।
- AC17.11 সবুজ: RPO ও RTO লক্ষ্যের ভেতরে, কোনো event হারায়নি, নকল journal নেই, RC1 থেকে RC18 ও audit chain সবুজ।
- Drill মানুষ runbook ধরে করেছে (সময় ঘড়ি ধরে), সংশোধিত runbook ও পরের drill-এর তারিখ জমা।

**Architecture constraints**
- Drill স্বয়ংক্রিয় script নয়; মানুষ runbook অনুসরণ করবে।
- Posted journal হারানো বা নকল তৈরি গ্রহণযোগ্য নয় (inbox identity দিয়ে ঠেকানো)।
- Drill production-এ নয়; আলাদা পরিবেশে।

Owner
Dev B
Estimate Hours
3
Status
Todo
QA Status
-
QA Comment

<!-- ===== END: ACC-Phase-17-Tasks.md ===== -->

