# 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
