# Accounting Module — Phase 8 Tasks (Tax Management — কর ব্যবস্থাপনা)

> এখানকার প্রতিটা task আলাদা করে একজন কাজ করতে পারবে।
> এই document-এর P8-01 থেকে P8-05 শেষ হলে থাকবে: কর-কর্তৃপক্ষ, registration, return box, tax code ও তারিখ-ভিত্তিক rate, group, account map, আর একটাই গণনা engine (`TaxService`)। `tax.enabled` বন্ধ থাকলে আচরণ Phase 7 শেষের মতোই — tax table খালি, কেউ পড়ে না।
> কাজের ক্রম: jurisdiction ও box → tax code ও rate → group → account map → গণনা engine।
> Estimate, Owner, Sprint ইচ্ছা করে ফাঁকা রাখা হয়েছে।
> নোট: Phase 8-এ মোট P8-01 থেকে P8-18 আছে; এই document-এ শুধু P8-01–P8-05। P8-06–P8-18, Phase 8-এর Registry যোগ ও Build ক্রমের সারি আলাদা উপ-tab-এ; Phase 7 (Multi-Currency ও FX) অন্য tab-এ — তাই সেগুলো এখানে নেই।

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

- Database: MySQL, InnoDB, `utf8mb4`। সব নতুন table-এর prefix `acc_tax_`; ব্যতিক্রম shared table `acc_journal_line_taxes` (Phase 8-এর অন্য card-এ, এই document-এ নেই)। Phase 1-এর সাধারণ নিয়ম (PK ধরন `ACC-P0-02`-এর Convention Sheet অনুযায়ী, `company_id NOT NULL`, composite FK `(company_id, x_id)` ও `uq (company_id, id)`, `version INT` দিয়ে optimistic check, `ON DELETE RESTRICT`) Phase 8-এও খাটে।
- Capability flag `tax.enabled` (default বন্ধ)। বন্ধ থাকলে আচরণ Phase 7 শেষের মতোই; table খালি ও কেউ পড়ে না।
- **সব write API `Idempotency-Key` নেয়।** সব job `AccountingJob` base ধরে।
- **Journal তৈরি হয় শুধু `JournalService::createDraft`-এ; status বদলায় শুধু `PostingService`।** Tax গণনা কখনো journal বা ledger-এ লেখে না (`TaxService` pure)।
- **কোডে কোনো কর-account বা rate বাঁধা নেই।** সব configuration (code, rate, group, account map) তারিখ-ভিত্তিক; পুরোনো লেনদেনে কখনো পরিবর্তন হয় না (posted line-এ `account_id` সরাসরি লেখা, rate snapshot সংরক্ষিত)।
- **সংবেদনশীল বদল Maker-Checker:** rate সক্রিয় (`taxrate:` prefix), account map (`taxmap:` prefix) `P1-04`-এর change request ও `ChangeApplier` দিয়ে। প্রস্তাবকারী নিজে অনুমোদন করতে পারে না।
- **প্রতিটা configuration বদলের ইতিহাস** `acc_tax_config_history` (append-only, `P8-02`)-এ।
- **Permission:** `accounting.tax.view` (পড়া), `accounting.tax.manage` (তৈরি ও বদল), `accounting.tax.approve` (দ্বিতীয় অনুমোদন)।
- **Error আকার:** `{ "code", "message", "field", "details", "correlation_id" }` (`AccountingError`)। Error prefix `TAX_*`।
- **DoD-র সাধারণ অংশ:** Service–Repository architecture (interface সহ), Form Request, API Resource; `company_id` tenant scope + IDOR test; Feature test (happy path, validation, unauthorized/forbidden); audit event `AccountingAudit` দিয়ে; error `AccountingError` enum-এর code দিয়ে; `tax.enabled` বন্ধ রেখে merge নিরাপদ; বিদ্যমান ERPFlow ও Phase 1-7 regression সবুজ।

**Phase-স্তরের open point (একাধিক card প্রভাবিত):**

- OD-37, OD-39a, OD-39b, OD-40, OD-41, OD-42, OD-44b এবং AC8.x, G8.x, R8.x নম্বরের সংজ্ঞা এই document-এ নেই। প্রতিটা card শুধু নম্বর ও card-এ লেখা baseline ধরে চলে। একই `OD-41` `P8-01`-এ "return period ও frequency" আর `P8-02`-এ "tax point তারিখ" নামে এসেছে (দুই নামে একই নম্বর); কোনটা ঠিক, নিশ্চিত করতে হবে।
- `P8-02` ও `P8-03` ইত্যাদি card `acc_tax_determination_rules`, `acc_journal_line_taxes`, `tax_date`-এর ওপর নির্ভর করে, যেগুলোর table `P8-06` বা পরের card-এ তৈরি (এই document-এ নেই)।
- Settings key `tax.rounding_mode`, `tax.default_price_basis` card-এ উল্লেখ আছে, কিন্তু সংজ্ঞা (মান, default, lock) Phase 8-এর Registry-তে, যা এই document-এ নেই।

**Phase 8 নির্ভরতা-ছবি (এই document-এর P8-01 থেকে P8-05; বাইরের নির্ভরতা বন্ধনীতে):**

```
P1-04,P1-10,P2-03 ─► P8-01 ─┬─► P8-02 ─┬─► P8-03 ──┐
                            │          │           │
                            │          └─► P8-04 ──┼─► P8-05 ─► P8-06..P8-09, P8-17 (অন্য tab)
                            └──────────────► P8-04 ┘      ▲
       P1-04 + CoA (acc_accounts) ───────────────┘          └── P7 (FxService::convert)
P8-01 ─► P8-11, P8-16 (অন্য tab)   P8-02 ─► P8-06, P8-07, P8-11 (অন্য tab)   P8-04 ─► P8-13, P8-16 (অন্য tab)
```

---

## ACC-P8-01 — Jurisdiction, registration ও return box schema

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema)
Title
Jurisdiction, registration ও return box schema — Backend
Branch Name
`feature/accounting-tax-jurisdiction-schema`
View Order
89
Description
**Summary**
কর-কর্তৃপক্ষ, company registration আর return-এর box — এগুলো কাঠামো মতো DB-তে থাকতে হবে; পরের সব tax কাজ (code, rate, return) এই ভিত্তির ওপর দাঁড়াবে। কোনো দেশের নিয়ম আগে থেকে table-এ seed করা যাবে না।

**Task serial**
`ACC-P8-01 — Jurisdiction, registration ও return box schema` · Feature · Phase 8, প্রথম card

**Depends on**
- `ACC-P1-04` (change request / দ্বিতীয় অনুমোদন)
- `ACC-P1-10` (DB guard ও append-only helper)
- `ACC-P2-03` (Journal validator; source নির্ভরতায় রেখেছে, কারণ এই card-এ ব্যাখ্যা নেই)

**Relation: আগের task**
- `P1-04` দিয়েছে দ্বিতীয় অনুমোদনের কাঠামো (Deactivate ও box বদলে `accounting.tax.approve` ধরনের Maker-Checker)।
- `P1-10` দিয়েছে `Schema::appendOnly()` helper; এই card তা ব্যবহার করে না (নিচে ব্যাখ্যা), কিন্তু ইতিহাস `P8-02`-র append-only `acc_tax_config_history`-তে যায়।
- `P2-03` দিয়েছে validator; Phase 8-এর পরের card-এ tax line যাচাইয়ের জন্য লাগবে, এই card-এ সরাসরি ব্যবহার নেই।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P8-02` (tax code, rate): `acc_tax_codes.jurisdiction_id` FK `acc_tax_jurisdictions`-এ; `box_code_base`/`box_code_tax` `acc_tax_return_boxes`-এর `box_code`-এ।
- `P8-04` (account map): `acc_tax_account_map.jurisdiction_id` FK এখানে; map বদলে jurisdiction সারি `FOR UPDATE`।
- `P8-03` (group): source এই card-কে `P8-03`-এর নির্ভরতায় রাখেনি, কিন্তু `acc_tax_groups.jurisdiction_id` এই table-এ FK এবং member code একই jurisdiction হতে হয়।
- `P8-11`, `P8-16` (অন্য tab-এ, এই document-এ নেই): `P8-11` return box ব্যবহার করে বলে ধরা যায়; `P8-16`-এ উদাহরণ import file (Bangladesh VAT, TDS ইত্যাদি) এই table-এ import হয়।
- Phase 9 (party data): `TaxRegistrationValidator` hook ব্যবহার করবে। (Phase 9 অন্য tab-এ)

**Related tables / schema**

**`acc_tax_jurisdictions`**

| column | ধরন | null | নোট |
| --- | --- | --- | --- |
| id | BIGINT UNSIGNED PK | না |  |
| company_id | BIGINT UNSIGNED | না |  |
| code | VARCHAR(30) | না | company-তে unique |
| name | VARCHAR(150) | না |  |
| country_code | CHAR(2) | হ্যাঁ | তথ্য, logic-এ নয় |
| region | VARCHAR(100) | হ্যাঁ |  |
| authority_name | VARCHAR(150) | হ্যাঁ |  |
| filing_frequency | ENUM(monthly, quarterly, half_yearly, annual) | না | default monthly |
| filing_due_days | SMALLINT | না | period শেষের পর কত দিনে due, default 15 |
| tax_id_label | VARCHAR(60) | হ্যাঁ | যেমন "Registration No" |
| tax_id_pattern | VARCHAR(200) | হ্যাঁ | regex, শুধু format যাচাই |
| rounding_mode | ENUM(line, document) | হ্যাঁ | NULL হলে `tax.rounding_mode` |
| is_active | TINYINT(1) | না | default 1 |
| version, created_by, created_at, updated_at |  | না |  |

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

**`acc_tax_registrations`**

| column | ধরন | null | নোট |
| --- | --- | --- | --- |
| id, company_id |  | না |  |
| jurisdiction_id | BIGINT UNSIGNED | না |  |
| registration_no | VARCHAR(60) | না |  |
| registered_from | DATE | না |  |
| registered_to | DATE | হ্যাঁ |  |
| status | ENUM(active, suspended, cancelled) | না | default active |
| is_primary | TINYINT(1) | না | jurisdiction প্রতি একটাই primary (generated unique) |
| version, created_by, created_at, updated_at |  | না |  |

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

**`acc_tax_return_boxes`**

| column | ধরন | null | নোট |
| --- | --- | --- | --- |
| id, company_id |  | না |  |
| jurisdiction_id | BIGINT UNSIGNED | না |  |
| box_code | VARCHAR(30) | না | return-এর ঘরের কোড |
| label | VARCHAR(200) | না |  |
| line_kind | ENUM(output, input, withholding, adjustment, net, memo) | না |  |
| amount_basis | ENUM(base, tax, both) | না |  |
| sign | TINYINT | না | 1 বা -1 |
| sort_no | SMALLINT | না |  |
| is_active | TINYINT(1) | না |  |
| version | INT | না |  |

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

**Business rules**
- R8.1, R8.27, R8.38, R8.41।
- কিছুই seed হয় না। Bangladesh VAT বা TDS-এর মতো উদাহরণ import file হিসেবে থাকে (`P8-16`), table-এ আগে থেকে বসানো থাকে না।
- `country_code` শুধু তথ্য, কোনো logic-এ ব্যবহার নয়।
- `tax_id_pattern` শুধু format যাচাই (regex); registration-এর সত্যতা যাচাই নয়।
- `rounding_mode` NULL হলে `tax.rounding_mode` setting।
- jurisdiction প্রতি একটাই primary registration (generated unique)।
- ব্যবহৃত jurisdiction deactivate আটকায় (`TAX_JURISDICTION_IN_USE`)।
- Deactivate ও box বদলে দ্বিতীয় অনুমোদন (`accounting.tax.approve`)।
- ইতিহাস `acc_tax_config_history`-তে (`P8-02`), এই table-গুলো নিজে বদলায় (`Schema::appendOnly()` লাগে না)।
- **Open point:** DB flow-এ runtime write `acc_tax_config_history`-তে INSERT করে, কিন্তু সেই table `P8-02`-এ তৈরি (এবং `P8-02` এই card-এর ওপর নির্ভরশীল)। অর্থাৎ এই card-এ history লেখা সম্ভব নয় যতক্ষণ না `P8-02`-এর history migration আগে বা একই সাথে যায়। সমাধান (history table এই card-এ আনা, না history লেখা `P8-02`-এ বিলম্বিত) tech lead ঠিক করবে।
- **Open point:** Deactivate ও box বদলে "দ্বিতীয় অনুমোদন" কোন `ChangeApplier` prefix দিয়ে হবে (যেমন `taxrate:`/`taxmap:` আছে) source বলেনি। Registration বদলে (`PUT /tax/registrations/{id}`) অনুমোদন লাগে কি না তাও স্পষ্ট নয়।
- **Open point:** OD-37 (কোন jurisdiction আগে), OD-41 (return period ও frequency) লিখিত সিদ্ধান্ত নেই; `filing_frequency` বদল কী প্রভাব ফেলে (পুরোনো return) বলা নেই।
- **Open point:** `is_primary` "generated unique" ঠিক কীভাবে (nullable generated column যা non-primary সারিতে NULL) source-এ বলা নেই; Phase 6-এর `source_active` রীতি অনুসরণ করা যুক্তিসংগত।

**DB flow**
- Migration, এক ক্রমে: (1) তিনটা table তৈরি। (2) `Schema::appendOnly()` লাগে না (এগুলো বদলায়, ইতিহাস `acc_tax_config_history`-এ, `P8-02`)। (3) কোনো seed নেই, কোনো বিদ্যমান table বদলায় না।
- **Runtime write:** jurisdiction `FOR UPDATE` (শুধু নিজের table), `version` মিলিয়ে UPDATE, `acc_tax_config_history` INSERT, audit, commit। Ledger lock নেই। ব্যর্থ হলে পুরো transaction rollback, `TAX_CONFLICT`।

**Task scope**
1. Migration ও model: তিন table।
2. Service ও `version` check (`TaxJurisdictionService`)।
3. Registration ও pattern validator (`TaxRegistrationValidator`)।
4. Box কাঠামো service (পুরো তালিকা `version` সহ বদল)।
5. API ও permission।
6. Test।

**Out of scope**
Tax code, rate, group (`P8-02`, `P8-03`)। Account map (`P8-04`)। কোনো jurisdiction/box seed বা উদাহরণ import (`P8-16`, অন্য tab)। Return তৈরি (পরের card)।

**Contract (API)**
- Service: `TaxJurisdictionService::create/update/deactivate(company, ...)`। `TaxRegistrationValidator::validate(company, jurisdictionId, taxId): ValidationResult` (hook, Phase 9 party data দিয়ে ব্যবহার করবে)।
- API: `GET/POST /tax/jurisdictions`, `GET/PUT /tax/jurisdictions/{id}`, `POST /tax/jurisdictions/{id}/deactivate`, `GET/POST /tax/jurisdictions/{id}/registrations`, `PUT /tax/registrations/{id}`, `GET/PUT /tax/jurisdictions/{id}/boxes` (version সহ পুরো তালিকা বদল)।

**Permission**
`accounting.tax.view` পড়া। `accounting.tax.manage` তৈরি ও বদল। Deactivate ও box বদলে দ্বিতীয় অনুমোদন `accounting.tax.approve`।

**Error**
`TAX_JURISDICTION_NOT_FOUND`, `TAX_JURISDICTION_IN_USE`, `TAX_REGISTRATION_INVALID`, `TAX_CONFLICT`, `TAX_VALIDATION_FAILED`

**Audit**
`accounting.tax_jurisdiction.created`, `.updated`, `.deactivated`; `accounting.tax_registration.changed`; `accounting.tax_box.changed` (আগে ও পরের মান সহ)

**Acceptance**
AC8.1

**Test**
`TaxJurisdictionTest`:
- (ক) দুই jurisdiction একই company-তে।
- (খ) একই code দ্বিতীয়বার আটকায়।
- (গ) ব্যবহৃত jurisdiction deactivate আটকায়।
- (ঘ) `version` অমিলে `TAX_CONFLICT`।
- (ঙ) অন্য company-র id composite FK-তে আটকায়।

**Deliverable**
Migration, model, service, API, OpenAPI অংশ, `down()` মন্তব্য (table খালি হলে নিরাপদ)

**Sub-tasks**
(1) Migration ও model। (2) Service ও `version` check। (3) Registration ও pattern validator। (4) Box কাঠামো service। (5) API ও permission। (6) Test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC8.1 সবুজ।
- `tax.enabled` বন্ধ রেখে merge নিরাপদ; কোনো seed নেই প্রমাণিত।
- অন্য company-র id composite FK-তে সরাসরি SQL-এ আটকায় (test প্রমাণ)।

**Architecture constraints**
- কোনো দেশ বা কর-নিয়ম table-এ আগে থেকে seed করা যাবে না।
- Composite FK নিয়ম; `uq (company_id, id)` সব table-এ।
- Ledger lock নয়; jurisdiction `FOR UPDATE` শুধু নিজের table-এ।

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

---

## ACC-P8-02 — Tax code, rate ও config history

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service)
Title
Tax code, rate ও config history — Backend
Branch Name
`feature/accounting-tax-code-rate`
View Order
90
Description
**Summary**
সাত ধরনের tax code আর তারিখ-ভিত্তিক rate লাগবে; rate বদল এমনভাবে হবে যে পুরোনো লেনদেন কখনো বদলাবে না। প্রতিটি configuration বদল append-only ইতিহাসে (`acc_tax_config_history`) — অডিটে দেখা যাবে কোন তারিখে কোন rate প্রযোজ্য ছিল।

**Task serial**
`ACC-P8-02 — Tax code, rate ও config history` · Feature · Phase 8, দ্বিতীয় card

**Depends on**
- `ACC-P8-01` (jurisdiction ও return box)
- `ACC-P1-04` (change request, prefix `taxrate:`)
- `ACC-P1-10` (DB guard trigger ও `Schema::appendOnly()`)

**Relation: আগের task**
- `P8-01` দিয়েছে `acc_tax_jurisdictions` (`acc_tax_codes.jurisdiction_id` FK) ও `acc_tax_return_boxes` (code-এর `box_code_base`, `box_code_tax`)। 
- `P1-04` দিয়েছে change request ও `ChangeApplier` রেজিস্ট্রি; এই card `taxrate:` prefix নিবন্ধন করে।
- `P1-10` দিয়েছে trigger কাঠামো ও `Schema::appendOnly()`; এই card `acc_tax_rates`-এ বিশেষ trigger বসায় এবং `acc_tax_config_history`-কে append-only করে।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P8-03` (group): member হিসেবে tax code ও তার rate resolver ব্যবহার করে।
- `P8-04` (account map): `acc_tax_account_map.tax_code_id` FK `acc_tax_codes`-এ।
- `P8-05` (গণনা engine): `TaxRateResolver::resolve` ও `rate_epoch` cache ব্যবহার করে।
- `P8-06`, `P8-07`, `P8-11` (অন্য tab-এ, এই document-এ নেই): source এদের এই card-এর ওপর নির্ভরশীল বলেছে।
- সব পরের Phase 8 card: `acc_tax_config_history` লেখে (entity: jurisdiction, registration, box, code, rate, group, account_map, rule)।

**Related tables / schema**

**`acc_tax_codes`**

| column | ধরন | null | নোট |
| --- | --- | --- | --- |
| id, company_id |  | না |  |
| jurisdiction_id | BIGINT UNSIGNED | না |  |
| code | VARCHAR(30) | না | company-তে unique |
| name | VARCHAR(150) | না |  |
| tax_type | ENUM(input, output, withholding, reverse_charge, zero_rated, exempt, out_of_scope) | না | ব্যবহৃত হলে অপরিবর্তনীয় |
| calc_method | ENUM(percent, fixed_amount) | না | default percent |
| price_basis | ENUM(exclusive, inclusive) | না | default `tax.default_price_basis` |
| recoverable_mode | ENUM(full, none, partial) | না | default full (input-এ অর্থবহ) |
| recoverable_pct | DECIMAL(9,6) | হ্যাঁ | partial হলে বাধ্যতামূলক |
| withholding_side | ENUM(deducted, suffered) | হ্যাঁ | withholding হলে বাধ্যতামূলক |
| box_code_base, box_code_tax | VARCHAR(30) | হ্যাঁ | return box (P8-01) |
| rate_epoch | INT | না | default 1, cache key |
| is_active | TINYINT(1) | না |  |
| version, created_by, created_at, updated_at |  | না |  |

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

**`acc_tax_rates`**

| column | ধরন | null | নোট |
| --- | --- | --- | --- |
| id, company_id |  | না |  |
| tax_code_id | BIGINT UNSIGNED | না |  |
| rate | DECIMAL(20,10) | হ্যাঁ | শতাংশ (15 মানে 15%) |
| fixed_amount | DECIMAL(20,4) | হ্যাঁ | calc_method fixed হলে |
| effective_from | DATE | না |  |
| effective_to | DATE | হ্যাঁ | NULL মানে খোলা |
| status | ENUM(pending, active, superseded, rejected) | না |  |
| change_request_id | BIGINT UNSIGNED | হ্যাঁ | P1-04 |
| first_used_at | DATETIME | হ্যাঁ | প্রথম posted ব্যবহারে বসে |
| proposed_by, approved_by, approved_at |  | হ্যাঁ | approved_by ≠ proposed_by |
| created_at | DATETIME | না |  |

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

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

| column | ধরন | null | নোট |
| --- | --- | --- | --- |
| id, company_id |  | না |  |
| entity | ENUM(jurisdiction, registration, box, code, rate, group, account_map, rule) | না |  |
| entity_id | BIGINT UNSIGNED | না |  |
| action | ENUM(create, update, activate, deactivate, supersede, cancel) | না |  |
| old_value, new_value | JSON | হ্যাঁ |  |
| reason | VARCHAR(500) | হ্যাঁ |  |
| changed_by | BIGINT UNSIGNED | না |  |
| changed_at | DATETIME | না |  |

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

**Business rules**
- R8.2 থেকে R8.7, R8.11, R8.15, R8.16, R8.17, R8.41।
- Rate তারিখ-ভিত্তিক: একই code-এ `effective_from` নতুন সারি আগের সব সারির `effective_from`-এর পরে; overlap আটকায় (`TAX_RATE_OVERLAP`)।
- **Retroactive নিষেধ:** নতুন rate-এর `effective_from <= ওই code-এর সর্বশেষ posted `tax_date` হলে `TAX_RATE_RETROACTIVE`।
- ব্যবহৃত rate-এর `rate` ইত্যাদি অপরিবর্তনীয় (trigger, `TAX_RATE_IMMUTABLE`)। `tax_type` ব্যবহৃত হলে অপরিবর্তনীয়।
- প্রস্তাবকারী ≠ অনুমোদনকারী (`approved_by ≠ proposed_by`)।
- অনুমোদনে আগের open সারির `effective_to = new.effective_from - 1 দিন` (trigger-অনুমোদিত একমাত্র বদল)।
- `rate_epoch` প্রতি অনুমোদনে +1; resolver cache key।
- `partial` recoverable হলে `recoverable_pct` বাধ্যতামূলক; withholding হলে `withholding_side` বাধ্যতামূলক।
- `calc_method = fixed_amount` হলে rate-এর বদলে `fixed_amount` (দুটোর ঠিক একটা)।
- **Open point:** Retroactive পরীক্ষা `acc_journal_line_taxes` (shared table, Phase 8-এর পরের card, এই document-এ নেই) এ `tax_date` পড়ে। সেই table না থাকা পর্যন্ত পরীক্ষা কী করবে (খালি ধরে এগোনো, না আটকানো) source বলেনি; আপাতত "table না থাকলে পরীক্ষা সফল" ধরা যায়, নিশ্চিত নয়।
- **Open point:** OD-41 এখানে "tax point তারিখ" (`P8-01`-এ "return period ও frequency")। `acc_tax_rates`-এ কোন তারিখ (tax_date বনাম invoice/payment) ধরে rate নির্বাচন হবে, `TaxRateResolver::resolve(company, codeId, date)` শুধু `date` নেয়; সঠিক তারিখ নির্ধারণ OD-41।
- **Open point:** OD-44b (reverse charge) এ `reverse_charge` type-এর rate ও পরিচালনা কীভাবে হবে `P8-05` ধাপ (৭)-এ আছে; এই card-এ শুধু type ENUM।
- **Open point:** `rate BETWEEN 0 AND 100` CHECK হলেও `fixed_amount` এর সীমা নেই; ঋণাত্মক `fixed_amount` আটকানো source-এ বলা নেই।

**DB flow**
**Rate তৈরি (প্রস্তাব), এক transaction:**
1. `acc_tax_codes` সারি `FOR UPDATE` (শুধু নিজের table)।
2. `version` মেলানো।
3. নতুন `acc_tax_rates` সারি `status = pending`, `effective_from` > সব বিদ্যমান সারির `effective_from`।
4. Retroactive পরীক্ষা: `SELECT MAX(tax_date) FROM acc_journal_line_taxes` ওই code-এ, posted journal join, কোনো lock ছাড়া পড়া। `effective_from <= সর্বশেষ tax_date` হলে `TAX_RATE_RETROACTIVE`।
5. Change request `taxrate:<id>` (`P1-04`), history INSERT।

**অনুমোদনে (আলাদা transaction):** code `FOR UPDATE`; (4) আবার; আগের open সারির `effective_to = new.effective_from - 1 দিন` (trigger-অনুমোদিত একমাত্র বদল); নতুন সারি `active`; `acc_tax_codes.rate_epoch` +1; history; audit।

Ledger lock নেই। ব্যর্থ হলে সব rollback।

**Task scope**
1. Migration ও model: `acc_tax_codes`, `acc_tax_rates`, `acc_tax_config_history`।
2. Rate trigger (শুধু `effective_to`, `status`, `first_used_at` বদল; DELETE নিষিদ্ধ)।
3. `TaxCodeService`, `TaxRateService`, `taxrate:` ChangeApplier prefix।
4. `TaxRateResolver` ও cache (`rate_epoch`)।
5. Retroactive পরীক্ষা।
6. History writer।
7. API।
8. Test।

**Out of scope**
Jurisdiction (`P8-01`)। Group (`P8-03`)। Account map (`P8-04`)। গণনা (`P8-05`)। `first_used_at` বসানো (posting-সময়ের পরের card, এই document-এ নেই)।

**Contract (API)**
- Service: `TaxCodeService::create/update/deactivate`, `TaxRateService::propose(codeId, rate, effectiveFrom)`, `::activate(changeRequestId)`। `TaxRateResolver::resolve(company, codeId, date): TaxRate`।
- API: `GET/POST /tax/codes`, `GET/PUT /tax/codes/{id}`, `POST /tax/codes/{id}/deactivate`, `GET /tax/codes/{id}/rates`, `POST /tax/codes/{id}/rates` (প্রস্তাব), `POST /tax/rates/{id}/cancel` (pending হলে), `GET /tax/config-history?entity=&id=`।

**Permission**
`accounting.tax.view`; `accounting.tax.manage` প্রস্তাব ও code বদল; `accounting.tax.approve` rate সক্রিয় (প্রস্তাবকারী ≠ অনুমোদনকারী)।

**Error**
`TAX_CODE_NOT_FOUND`, `TAX_CODE_INACTIVE`, `TAX_CODE_IN_USE` (type বা jurisdiction বদল), `TAX_RATE_NOT_FOUND`, `TAX_RATE_OVERLAP`, `TAX_RATE_RETROACTIVE`, `TAX_RATE_IMMUTABLE`, `TAX_CONFLICT`

**Audit**
`accounting.tax_code.created`, `.updated`, `.deactivated`; `accounting.tax_rate.proposed`, `.activated`, `.cancelled` (আগের ও নতুন rate, effective তারিখসহ)

**Acceptance**
AC8.2, AC8.3

**Test**
`TaxRateEffectiveTest`:
- (ক) rate 15 থেকে 12, effective ১ জানুয়ারি। ৩১ ডিসেম্বরের তারিখে 15, ১ জানুয়ারি 12।
- (খ) overlap আটকায়।
- (গ) ব্যবহৃত rate-এর `rate` UPDATE trigger-এ আটকায়।
- (ঘ) retroactive আটকায়।
- (ঙ) প্রস্তাবকারী নিজে সক্রিয় করতে পারে না।
- (চ) পুরোনো posted tax সারি অপরিবর্তিত।

**Deliverable**
Migration, trigger, service, resolver, API, config history

**Sub-tasks**
(১) Migration ও model। (২) Rate trigger (শুধু `effective_to`, `status`, `first_used_at` বদল)। (৩) `TaxRateService` ও `taxrate:` ChangeApplier prefix। (৪) `TaxRateResolver` ও cache (`rate_epoch`)। (৫) Retroactive পরীক্ষা। (৬) History writer। (৭) API। (৮) Test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC8.2, AC8.3 সবুজ।
- Trigger ও append-only প্রমাণ সরাসরি SQL-এ (UPDATE/DELETE ব্যর্থ)।
- `tax.enabled` বন্ধ রেখে merge নিরাপদ।

**Architecture constraints**
- Rate কখনো in-place বদলায় না; সবসময় নতুন সারি + পুরোনোর `effective_to`।
- `acc_tax_config_history` কখনো UPDATE/DELETE নয়।
- Ledger lock নয়।
- Retroactive পরীক্ষা lock ছাড়া পড়া (পড়ার পরও অনুমোদনে আবার)।

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

---

## ACC-P8-03 — Tax group ও component

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service)
Title
Tax group ও component — Backend
Branch Name
`feature/accounting-tax-group`
View Order
91
Description
**Summary**
এক লেনদেনে একাধিক কর (base-এ একটা, compound-এ আগের করসহ আরেকটা) সামলাতে tax group — একাধিক component ও `apply_on` ক্রম। group নিজে rate রাখে না; member code-এর rate-ই ব্যবহার হবে।

**Task serial**
`ACC-P8-03 — Tax group ও component` · Feature · Phase 8, তৃতীয় card

**Depends on**
- `ACC-P8-02`

**Relation: আগের task**
- `P8-02` দিয়েছে `acc_tax_codes` (member হবে) ও active/rate সংজ্ঞা, `TaxRateResolver`, এবং `acc_tax_config_history` (entity `group` এখানে লেখা হয়)।
- `P8-01` (সরাসরি নির্ভরতায় নেই, কিন্তু `P8-02`-এর মাধ্যমে) দিয়েছে `acc_tax_jurisdictions`: group-এর `jurisdiction_id` FK এখানে; সব member একই jurisdiction-এর হতে হয়।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P8-05` (গণনা engine): `TaxInput`-এ `tax_group_id` নিলে group-এর member ক্রমে (`seq_no`, `apply_on`) component গণনা করে।
- `P8-06` (অন্য tab-এ, এই document-এ নেই): source এই card-এর ওপর নির্ভরশীল বলেছে।

**Related tables / schema**

**`acc_tax_groups`**

| column | ধরন | null | নোট |
| --- | --- | --- | --- |
| id, company_id |  | না |  |
| jurisdiction_id | BIGINT UNSIGNED | না |  |
| code, name | VARCHAR(30), VARCHAR(150) | না |  |
| rounding_mode | ENUM(line, document) | হ্যাঁ | NULL হলে jurisdiction/setting |
| is_active | TINYINT(1) | না |  |
| first_used_at | DATETIME | হ্যাঁ |  |
| version, created_by, created_at, updated_at |  | না |  |

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

**`acc_tax_group_members`**

| column | ধরন | null | নোট |
| --- | --- | --- | --- |
| id, company_id |  | না |  |
| group_id, tax_code_id | BIGINT UNSIGNED | না |  |
| seq_no | SMALLINT | না | 1 থেকে |
| apply_on | ENUM(base, base_plus_previous) | না | seq 1-এ অবশ্যই base |

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

**Business rules**
- R8.8, R8.9, R8.10।
- Group নিজে rate রাখে না; member code-এর rate ব্যবহার হয়।
- Member code একই jurisdiction-এর ও active হতে হবে (`TAX_GROUP_JURISDICTION_MIX`, `TAX_CODE_INACTIVE`)।
- একই code দুইবার একই group-এ নয় (`uq (company_id, group_id, tax_code_id)`)।
- `seq_no` 1 থেকে ক্রমিক; seq 1-এ `apply_on` অবশ্যই `base`; পরের সারিতে `base` বা `base_plus_previous` (compound)।
- ব্যবহৃত group-এর member বদলালে সেটা সম্ভব নয় (`TAX_GROUP_IN_USE`): নতুন group বানাতে হবে।
- `first_used_at` প্রথম posted ব্যবহারে বসে (পরের card-এ)।
- `rounding_mode` NULL হলে jurisdiction, তারপর `tax.rounding_mode` setting।
- **Open point:** `TAX_GROUP_INVALID` ঠিক কোন কোন শর্তে (সদস্যসংখ্যা শূন্য, `seq_no` ফাঁক, seq 1-এ `base_plus_previous`) তালিকা source-এ নেই; আনুমানিক এই তিনটা।
- **Open point:** OD-44b (reverse charge group) ও OD-39b (partial recovery) এই card-এর OD, কিন্তু card-এর নিয়মে এদের আলাদা প্রয়োগ নেই (শুধু code-স্তরে `reverse_charge`/`partial`)। Group-স্তরে reverse charge কীভাবে প্রতিফলিত হবে (যেমন দুই member: output ও input) `P8-05` ধাপ (৭) সম্পর্কিত, লিখিত নয়।
- **Open point:** DB flow-এ "group `version` +1" বলে কিন্তু "ব্যবহৃত group-এর member বদলালে নতুন group `version` সারি নয়" বাক্যটির অর্থ অস্পষ্ট (ধরা হয়েছে: ব্যবহৃত group-এ in-place member বদল নেই; নতুন group বানাও)।

**DB flow**
Group তৈরি বা বদল এক transaction:
1. Group `FOR UPDATE` (নিজের table)।
2. Member code-গুলো একই jurisdiction ও active কি পড়া (lock নেই)।
3. সব member সারি delete-insert (draft group, অর্থাৎ এখনো ব্যবহৃত নয়)।
4. History (`acc_tax_config_history`, entity `group`)।
5. `rate_epoch` কোনো member code-এ নয়, বরং group `version` +1।

ব্যবহৃত group-এর member বদলালে `TAX_GROUP_IN_USE`: নতুন group বানাও। ব্যর্থ হলে rollback।

**Task scope**
1. Migration: `acc_tax_groups`, `acc_tax_group_members`।
2. Service ও validation (`TaxGroupService`)।
3. API।
4. Test।

**Out of scope**
Tax code/rate (`P8-02`)। গণনা (`P8-05`)। Group নির্ধারণ rule (`P8-06`, অন্য tab)। `first_used_at` বসানো।

**Contract (API)**
- Service: `TaxGroupService::create/update/deactivate`।
- API: `GET/POST /tax/groups`, `GET/PUT /tax/groups/{id}`, `POST /tax/groups/{id}/deactivate`।

**Permission**
`accounting.tax.view`, `accounting.tax.manage`

**Error**
`TAX_GROUP_INVALID`, `TAX_GROUP_JURISDICTION_MIX`, `TAX_GROUP_IN_USE`, `TAX_CODE_INACTIVE`

**Audit**
`accounting.tax_group.created`, `.updated`, `.deactivated`

**Acceptance**
AC8.4

**Test**
`TaxGroupTest`:
- (ক) দুই component compound।
- (খ) ভিন্ন jurisdiction-এর member আটকায়।
- (গ) একই code দুইবার আটকায়।
- (ঘ) ব্যবহৃত group-এর member বদল আটকায়।

**Deliverable**
Migration, service, API

**Sub-tasks**
(১) Migration। (২) Service ও validation। (৩) API। (৪) Test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC8.4 সবুজ।
- অন্য company-র code member হিসেবে বসানো composite FK-তে আটকায়।
- `tax.enabled` বন্ধ রেখে merge নিরাপদ।

**Architecture constraints**
- Group কখনো নিজে rate রাখে না।
- ব্যবহৃত group immutable (member তালিকা)।
- Composite FK ও `uq (company_id, id)`।
- Ledger lock নয়।

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

---

## ACC-P8-04 — Tax account map

Project
ERPFlow
Module
Accounting Module
Type
Feature (schema + service)
Title
Tax account map — Backend
Branch Name
`feature/accounting-tax-account-map`
View Order
92
Description
**Summary**
কোন tax কোন ledger account-এ যাবে — সেটা তারিখ-ভিত্তিক map; কোডে কোনো account বা rate বাঁধা নেই। map বদললেও posted line-এ `account_id` আগের মতোই থাকবে (সরাসরি লেখা)।

**Task serial**
`ACC-P8-04 — Tax account map` · Feature · Phase 8, চতুর্থ card

**Depends on**
- `ACC-P8-01` (jurisdiction)
- `ACC-P8-02` (tax code)
- `ACC-P1-04` (change request, prefix `taxmap:`)
- বিদ্যমান CoA (`acc_accounts`, `ACC-P1-06`)

**Relation: আগের task**
- `P8-01` দিয়েছে `acc_tax_jurisdictions` (map-এর `jurisdiction_id` FK, বদলে `FOR UPDATE`)।
- `P8-02` দিয়েছে `acc_tax_codes` (`tax_code_id` FK) ও `acc_tax_config_history`।
- `P1-04` দিয়েছে change request ও `ChangeApplier`; এই card `taxmap:` prefix নিবন্ধন করে।
- `P1-06` দিয়েছে `acc_accounts` (active, `is_group`, type, `uq (company_id, id)`) ও `AccountService`, যাতে এই card CoA hook বসায়।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P8-05` (গণনা engine): `TaxAccountMapService::resolve(company, jurisdictionId, taxCodeId, role, date): accountId` ব্যবহার করে প্রতি component-এর account বের করে (`TAX_ACCOUNT_MAP_MISSING` সহ)।
- `P8-13`, `P8-16` (অন্য tab-এ, এই document-এ নেই): source এই card-এর ওপর নির্ভরশীল বলেছে।
- `P1-06`-এর `AccountService` (বিদ্যমান): এই card-এর CoA hook mapped account নিষ্ক্রিয় বা type বদল আটকায়।

**Related tables / schema**

**`acc_tax_account_map`**

| column | ধরন | null | নোট |
| --- | --- | --- | --- |
| id, company_id |  | না |  |
| jurisdiction_id | BIGINT UNSIGNED | না |  |
| tax_code_id | BIGINT UNSIGNED | হ্যাঁ | jurisdiction-স্তরের role-এ NULL |
| tax_code_key | BIGINT UNSIGNED | না | generated `COALESCE(tax_code_id, 0)`, unique-এর জন্য |
| role | ENUM(tax_payable, tax_receivable, non_recoverable_expense, withholding_payable, withholding_receivable, net_tax_payable, tax_refundable, rounding) | না |  |
| account_id | BIGINT UNSIGNED | না |  |
| effective_from | DATE | না |  |
| effective_to | DATE | হ্যাঁ |  |
| status | ENUM(pending, active, superseded, rejected) | না |  |
| change_request_id | BIGINT UNSIGNED | হ্যাঁ |  |
| version, created_by, created_at |  | না |  |

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

**Business rules**
- R8.18, R8.41।
- Account যাচাই (সাধারণ read, ledger lock নেই): active, group নয়, posting অনুমোদিত, type role-এর সাথে মেলে (`TAX_ACCOUNT_TYPE_MISMATCH`)।
- Map বদলালে পুরোনো লেনদেনে কিছু বদলায় না, কারণ posted line-এ `account_id` সরাসরি লেখা।
- Account পরে নিষ্ক্রিয় বা type বদল হলে CoA service map পড়ে আটকায় (`TAX_ACCOUNT_IN_USE`)।
- Role ফাঁকা থাকলে calculate `TAX_ACCOUNT_MAP_MISSING`।
- নিজের প্রস্তাব নিজে অনুমোদন আটকায় (Maker-Checker)।
- jurisdiction-স্তরের role (`net_tax_payable`, `tax_refundable`, `rounding`) `tax_code_id` NULL; বাকি role code-ভিত্তিক।
- **Open point:** "type role-এর সাথে মেলে" এর role → অনুমোদিত account type মানচিত্র (যেমন `tax_payable` → liability, `tax_receivable` → asset, `non_recoverable_expense` → expense) source-এ তালিকা নেই, শুধু test (ক) "payable role-এ asset account আটকায়" আছে। পূর্ণ মানচিত্র ঠিক করতে হবে।
- **Open point:** DB flow-এ "jurisdiction-এর config epoch +1 (cache বাতিল)" বলা হয়েছে, কিন্তু `acc_tax_jurisdictions` table-এ epoch column নেই (`P8-01`-এ শুধু `version`)। Epoch কোথায় থাকবে (jurisdiction `version` ব্যবহার, নতুন column, না cache key) সিদ্ধান্ত লাগবে।
- **Open point:** OD-39a (non-recoverable posting) সিদ্ধান্ত লিখিত নেই; role `non_recoverable_expense` কীভাবে ব্যবহার হবে তা `P8-05` ধাপ (৬)-এ আছে।
- **Open point:** CoA hook `P1-06`-এর `AccountService` পরিবর্তন করে (নতুন error `TAX_ACCOUNT_IN_USE` যোগ); এটি Phase 1 code-এ পরিবর্তন, তাই regression ঝুঁকি আছে।

**DB flow**
**বদলের অনুরোধ, এক transaction:**
1. Jurisdiction সারি `FOR UPDATE` (শুধু নিজের table)।
2. `version` মেলানো।
3. Account যাচাই (সাধারণ read, ledger lock নেই): active, group নয়, posting অনুমোদিত, type role-এর সাথে মেলে।
4. নতুন সারি `pending`, change request `taxmap:<id>` (`P1-04`), history।

**অনুমোদনে, এক transaction:** jurisdiction `FOR UPDATE`; আবার যাচাই; আগের open সারির `effective_to` বন্ধ; নতুন সারি `active`; jurisdiction-এর config epoch +1 (cache বাতিল); audit।

Account পরে নিষ্ক্রিয় বা type বদল হলে CoA service map পড়ে আটকায় (`TAX_ACCOUNT_IN_USE`)। ব্যর্থ হলে সব rollback।

**Task scope**
1. Migration (`acc_tax_account_map`) ও trigger।
2. Account যাচাই।
3. `taxmap:` ChangeApplier।
4. Resolver ও cache।
5. CoA service hook (`P1-06`-এর `AccountService`)।
6. API।
7. Test।

**Out of scope**
Jurisdiction ও code (`P8-01`, `P8-02`)। গণনা engine (`P8-05`)। Determination rule (`P8-06`, অন্য tab)। উদাহরণ import (`P8-16`, অন্য tab)।

**Contract (API)**
- Service: `TaxAccountMapService::propose/activate`, `::resolve(company, jurisdictionId, taxCodeId, role, date): accountId`।
- API `GET/PUT /tax/account-map` (পুরো তালিকা, `version` সহ)।

**Permission**
view, manage, approve (দ্বিতীয় ব্যক্তি): `accounting.tax.view`, `accounting.tax.manage`, `accounting.tax.approve`

**Error**
`TAX_ACCOUNT_MAP_MISSING`, `TAX_ACCOUNT_TYPE_MISMATCH`, `TAX_ACCOUNT_IN_USE`, `TAX_CONFLICT`

**Audit**
`accounting.tax_account_map.proposed`, `.activated` (আগের ও নতুন account, effective তারিখ)

**Acceptance**
AC8.5

**Test**
`TaxAccountMapTest`:
- (ক) payable role-এ asset account আটকায়।
- (খ) role ফাঁকা থাকলে calculate `TAX_ACCOUNT_MAP_MISSING`।
- (গ) map বদলের পরেও পুরোনো posted line একই account।
- (ঘ) mapped account নিষ্ক্রিয় করা আটকায়।
- (ঙ) নিজের প্রস্তাব নিজে অনুমোদন আটকায়।

**Deliverable**
Migration, service, resolver, CoA hook, API

**Sub-tasks**
(১) Migration। (২) Account যাচাই। (৩) `taxmap:` ChangeApplier। (৪) Resolver ও cache। (৫) CoA service hook। (৬) API। (৭) Test

**Definition of done**
- সাধারণ DoD পূরণ।
- AC8.5 সবুজ।
- `P1-06`-এর বিদ্যমান CoA test সবুজ (hook regression নেই)।
- Trigger প্রমাণ: `account_id`/`role`/`effective_from` `pending` ছাড়া বদলায় না।

**Architecture constraints**
- কোডে কোনো account বাঁধা নেই; সব map থেকে।
- Posted line-এর `account_id` কখনো map দেখে বদলায় না।
- Ledger lock নয়; jurisdiction `FOR UPDATE` শুধু নিজের table-এ।
- Composite FK ও `uq (company_id, id)`।

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

---

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

Project
ERPFlow
Module
Accounting Module
Type
Feature (service)
Title
TaxService, গণনা engine ও rounding — Backend
Branch Name
`feature/accounting-tax-calculation-engine`
View Order
93
Description
**Summary**
সব tax হিসাব এক জায়গায় — `TaxService`, pure function; একই input ও config epoch-এ একই ফল, কোনো lock/write নেই। preview, journal, পরের Phase (যেমন Phase 14) সবাই এটাই ডাকবে; কর-গণনায় অমিল হবে না।

**Task serial**
`ACC-P8-05 — TaxService, গণনা engine ও rounding` · Feature · Phase 8, পঞ্চম card

**Depends on**
- `ACC-P8-02` (tax code, rate, resolver)
- `ACC-P8-03` (group ও component)
- `ACC-P8-04` (account map)
- `P7` (`FxService::convert`, base amount আনতে) — Phase 7 অন্য tab-এ, এই document-এ নেই

**Relation: আগের task**
- `P8-02` দিয়েছে `TaxRateResolver::resolve(company, codeId, date)` (তারিখ-ভিত্তিক rate, `rate_epoch` cache) এবং code-এর `tax_type`, `calc_method`, `price_basis`, `recoverable_mode/pct`, `withholding_side`, `box_code_*`।
- `P8-03` দিয়েছে group ও member (`seq_no`, `apply_on`) যা compound গণনার ভিত্তি।
- `P8-04` দিয়েছে `TaxAccountMapService::resolve(...)` যা প্রতি component-এর `account_id` বের করে।
- `P7` দিয়েছে `FxService::convert` যা foreign currency amount base currency-তে আনে (OD-42)। `TaxInput.lines[].amount` base currency-তে; তাই caller বা এই service-কে আগে convert করতে হয় (সুনির্দিষ্ট দায়িত্ব নিচে open point)।
- `P8-01` (পরোক্ষ) দিয়েছে jurisdiction (`rounding_mode`)।

**Relation: পরের task (কে অপেক্ষা করছে)**
- `P8-06` থেকে `P8-09`, `P8-17` (অন্য tab-এ, এই document-এ নেই): source এই card-কে এদের ভিত্তি বলেছে (determination rule `P8-06`; journal-এ কর বসানো ও posting এর পরের card)।
- Journal লেখার card (Phase 8-এর পরের), Phase 14 (posting rules engine): `TaxService::calculate` ডাকে, যাতে একই ফল পায়। (Phase 14 অন্য tab-এ)

**Related tables / schema**
নতুন table নেই। Cache: process-এর ভেতর, key `(company, code_id, rate_epoch, date)`।

পড়ে (কোনো write নেই): `acc_tax_codes`, `acc_tax_rates`, `acc_tax_groups`, `acc_tax_group_members`, `acc_tax_account_map`, `acc_tax_determination_rules` (শেষটা `P8-06`-এ তৈরি, এই document-এ নেই)।

**Business rules**
- R8.6, R8.9 থেকে R8.17, R8.40।
- **Pure function:** একই input ও config epoch হলে একই ফল; কোনো lock, কোনো write নয়।
- Transaction লাগে না (repeatable-read snapshot caller-এর transaction থেকে)। ব্যর্থ হলে exception; কিছু লেখা হয়নি।
- **গণনা ধাপ:**
  1. প্রতি line-এর code বা group ঠিক (সরাসরি, নইলে `P8-06` determination rule)।
  2. প্রতি component-এর rate `TaxRateResolver` থেকে তারিখ ধরে।
  3. **Exclusive:** component ক্রমে `tax = (base + আগের কর, apply_on অনুযায়ী) × rate ÷ 100`।
  4. **Inclusive:** `base = gross ÷ f`, যেখানে `f` = base 1 ধরে exclusive গণনার `1 + Σ tax`। এতে additive ও compound দুটোই সামলায়।
  5. **Rounding:** `line` মোডে প্রতি component পূর্ণসংখ্যায় (precision, method)। `document` মোডে code ধরে মোট একবার round; ফারাক line-এ largest remainder পদ্ধতিতে, tie-এ কম `line_no`। Inclusive-এ `base = gross − Σ tax` যাতে মোট ঠিক gross হয়।
  6. **Recoverable ভাগ:** full সব, none শূন্য, partial `round(tax × pct ÷ 100)`, বাকিটা non-recoverable।
  7. **Reverse charge:** সমান অঙ্কের output ও input জোড়া।
  8. **Zero-rated, exempt, out of scope:** `tax_amount = 0` সারি, base সহ।
  9. **Withholding:** `withholding_side` অনুযায়ী role।
  10. **Account map থেকে account।**
  11. **Invariant:** `tax_amount >= 0`, `Σ components + base = gross` (inclusive), hash (`calculation_hash`)।
- **Open point:** `TaxInput.lines[].amount` "base currency" বলা, কিন্তু base amount আনার দায়িত্ব (caller নাকি `TaxService`-এর ভেতরে `FxService::convert`) source-এ দ্ব্যর্থক; নির্ভরতায় `P7 (FxService::convert, base amount আনতে)` থাকলেও `TaxInput`-এ currency বা rate field নেই। OD-42 (foreign currency)-এ সিদ্ধান্ত লাগবে। Phase 7 এই document-এ না থাকায় `FxService` স্বাক্ষর অজানা।
- **Open point:** `P8-06` (determination rule, `acc_tax_determination_rules`) না থাকলে ধাপ (১)-এ শুধু সরাসরি `tax_code_id`/`tax_group_id` চলে; `determination{...}` পথ, `TAX_RULE_NO_MATCH`, `TAX_RULE_AMBIGUOUS` এই document-এ পরীক্ষা করা যাবে না (table-এর অনুপস্থিতিতে stub দরকার)।
- **Open point:** OD-40 (rounding), OD-39a, OD-39b, OD-44b লিখিত নেই; Golden G8.1 থেকে G8.5, G8.8 (G8.6, G8.7 নয়) এর সংখ্যা এই document-এ দেওয়া নেই (Blueprint-এ), তাই golden fixture ফাইল লিখতে Blueprint দরকার।
- **Open point:** `TAX_BASIS_INVALID`, `TAX_ROUNDING_CONFLICT` কখন ছোড়া হয় source-এ ব্যাখ্যা নেই (সম্ভবত `price_basis` মান অজানা/line-এর মধ্যে মিশ্র basis; rounding `line` বনাম `document` বিরোধ)।
- **Open point:** "precision, method" বলে rounding method (half_up বনাম half_even) test-এ উল্লেখ আছে; সংজ্ঞা `tax.rounding_*` setting-এ সম্ভবত, যা Registry-তে (এই document-এ নেই)।

**DB flow**
শুধু read: `acc_tax_codes`, `acc_tax_rates`, `acc_tax_groups`, `acc_tax_group_members`, `acc_tax_account_map`, `acc_tax_determination_rules`। কোনো lock নেই, কোনো write নেই। Transaction লাগে না (repeatable-read snapshot caller-এর transaction থেকে)। ব্যর্থ হলে exception, কিছু লেখা হয়নি।

**Task scope**
1. `TaxInput`/`TaxResult` DTO।
2. Component engine (exclusive, inclusive factor)।
3. `TaxRounding` (line, document, largest remainder)।
4. Recoverable ভাগ।
5. Reverse charge ও withholding।
6. Account resolution।
7. Hash (`calculation_hash`)।
8. API preview (`POST /tax/calculate`)।
9. Golden ও property test।

**Out of scope**
কোনো journal, line বা ledger-এ লেখা। Determination rule table/engine (`P8-06`)। `acc_journal_line_taxes` পূরণ। Return তৈরি। FX conversion-এর বাস্তবায়ন (Phase 7)।

**Contract (API)**
- `TaxService::calculate(company, TaxInput): TaxResult`।
- `TaxInput = {tax_date, side (sale, purchase, expense, payment_out, receipt_in), lines[{ref, amount (base currency), price_basis?, tax_code_id? বা tax_group_id? বা determination{txn_type, party_tax_status, item_category, service_category, account_id}, quantity?}]}`।
- `TaxResult = {lines[{ref, components[{tax_code_id, tax_rate_id, rate_snapshot, component_seq, base_amount, tax_amount, direction, is_recoverable, posting_mode, account_id, box_code_tax, box_code_base}]}], base_total, tax_total, gross_total, rounding_notes[], warnings[], calculation_hash}`।
- API `POST /tax/calculate` (শুধু preview)।

**Permission**
Preview-এর জন্য `accounting.tax.view`

**Error**
`TAX_RATE_NOT_FOUND`, `TAX_CODE_INACTIVE`, `TAX_BASIS_INVALID`, `TAX_ROUNDING_CONFLICT`, `TAX_ACCOUNT_MAP_MISSING`, `TAX_RULE_NO_MATCH`, `TAX_RULE_AMBIGUOUS`

**Audit**
নেই (pure)। Preview-র ব্যবহার সাধারণ request log-এ।

**Acceptance**
AC8.6, AC8.7, AC8.8

**Test**
`TaxCalculationTest`:
- Golden G8.1 থেকে G8.5, G8.8।
- Property test: inclusive-এর পর `base + tax = gross` সবসময়।
- Document মোডে line-এর যোগফল = document-এর মোট।
- একই input দুইবার একই hash।
- Half_up বনাম half_even ফারাক।

**Deliverable**
Service, `TaxRounding`, `TaxRateResolver` cache, API, golden fixture ফাইল

**Sub-tasks**
(১) `TaxInput`/`TaxResult` DTO। (২) Component engine (exclusive, inclusive factor)। (৩) `TaxRounding` (line, document, largest remainder)। (৪) Recoverable ভাগ। (৫) Reverse charge ও withholding। (৬) Account resolution। (৭) Hash। (৮) API preview। (৯) Golden ও property test

**Definition of done**
- সাধারণ DoD পূরণ (audit ব্যতীত, কারণ pure)।
- AC8.6, AC8.7, AC8.8 সবুজ; golden ও property test সবুজ।
- কোনো table-এ লেখা হয় না প্রমাণিত (preview-র আগে ও পরে row count অপরিবর্তিত)।
- `tax.enabled` বন্ধ রেখে merge নিরাপদ।

**Architecture constraints**
- `TaxService` pure: কোনো lock, write, side effect নয়; একই input ও epoch → একই `calculation_hash`।
- সব টাকার গণনা BCMath/DECIMAL, float নয়।
- সব tax হিসাবের একমাত্র জায়গা: journal, preview, Phase 14 কেউ নিজে কর হিসাব করবে না।
- কোডে rate বা account বাঁধা নয়।

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