# Attendance & Payroll — Dev Task Distribution

**বিল্ড অর্ডার (source of truth):** [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md)
**কার্ডের বিস্তারিত:** [ATTENDANCE_PAYROLL_TASK_CARDS.md](./ATTENDANCE_PAYROLL_TASK_CARDS.md)
**ডোমেইন রুলস:** [ATTENDANCE_PAYROLL_MODULE_SPEC.md](./ATTENDANCE_PAYROLL_MODULE_SPEC.md)

**তৈরি:** 10 Aug 2026 · **আপডেট:** 24 Aug 2026 — ✅ **`8.2-FE` Payslips UI done** (run list/generate/poll, detail settlement+frozen allocations, My Payslips)। Wave 6 **36/59**, বাকি **4 card / 23 pts**, পরের spine **`8.3a-BE`**। · 24 Aug 2026 — ✅ **`8.2b-BE` Advance Settlement & Payment Allocation done** (paid advance নেট, `PaymentAllocator` freeze, allocations GET)। Wave 6 **28/59**, বাকি **5 card / 31 pts**, পরের spine **`8.3a-BE`**। · 24 Aug 2026 — ✅ **`8.2a-BE` Payslip Generation done** (`payslips`, queued generate, Finaliser pass-through, **19** test)। Wave 6 **23/59**, বাকি **6 card / 36 pts**, পরের spine **`8.2b-BE`**। `8.2-FE` unlock। ⚠ PR **#228** এই card নয়। বিস্তারিত [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md)। · 20 Aug 2026 — 🎨 **UI design pass** (card নয়, points অপরিবর্তিত, কারো lane এগোয়নি): breadcrumb ও `PageHeader`-এর subtitle বাদ, হেডলাইন বার ~82px→~60px, header বাটন `md`→`sm`, `DataTable`-এর row **40px→28px**, Employee list-এ select bar আর টেবিল ঠেলে না, company switch-এ overlay + toast। 62 ফাইল · +226/−278, shared chrome-এ — তাই **সবার screen-ই দেখতে বদলাবে**, যদিও কারো card-এর কাজ বদলায়নি। `tsc -b` ১৫ error baseline-এর সাথে এক, `vite build` সবুজ; `check-style-boundary.sh` আগে থেকেই লাল (`JobStatusPanel.tsx`, `9.4-FE`-র ঋণ) — **`npm run build` ওখানেই আটকে আছে**। বিস্তারিত [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md)-এর শেষে। · 18 Aug 2026 (দ্বিতীয় পাস) — ⚠ **`6.1-FE` Monthly Approval UI merged (PR #198, Bablu)** — screen live, কিন্তু **দুটো gate ভেঙে**: `tsc -b` **১৫ error** (build লাল) আর `php artisan test` **1 failed / 452 passed** (bulk-approve-এর contract queued batch-এ বদলেছে, `6.1-BE`-র test আপডেট হয়নি)। **Bablu-র পরের কাজ ওটাই**, তারপর ছয়টা DoD-ঋণ — সবচেয়ে জরুরি: **detail page-এর Approve সবসময় `override: true` পাঠায়**, অর্থাৎ unresolved guard UI থেকে বন্ধ। **বাকি কাজ: 12 cards · 61 pts।** · 18 Aug 2026 — ✅ **`9.4-FE` Attendance Jobs UI merged (PR #197, Munna)** — Attendance › Jobs screen live, তাই **Munna-র Lane B-তে jobs track শেষ**; পরের card `8.2-FE` এখনো `8.2a-BE`-র অপেক্ষায়, মাঝখানে **`9.4-FE`-র চারটে ঋণ** শোধের কাজ আছে (নিচে)। **তিনটে blocker-ই শেষ:** `php artisan test` **453 passed / 1438 assertions**, `Modules/Attendance/tests` **79 passed**, `tsc -b --force` ০ error, `vite build` সবুজ। **বাকি কাজ: 13 cards · 66 pts।** · 17 Aug 2026 — 🐛 **Platform-এ দুটো infinite render loop সারানো** (`UserRolesDrawer`, `RolePermissionsDrawer` — "Maximum update depth exceeded"); card নয়, points অপরিবর্তিত, বিস্তারিত নিচে। · 17 Aug 2026 — ✅ **`5.3-FE`-র build-ঋণ শোধ:** `npm run build` আবার সবুজ (`tsc -b` ০ error), "Refresh Balance" button render হয়, আর day breakdown-টা runtime-এও সত্যিই আসে (normalizer `days` ফেলে দিচ্ছিল — আগের নোটে উল্টো লেখা ছিল)। **Munna এখন `9.2-BE` ধরতে পারে।** · 17 Aug 2026 — **`5.3-FE`** (PR #188) merged ⚠ — **Wave 4-এ এখন শুধু `5.5-FE` বাকি**, কিন্তু ঐ PR **`npm run build` ভেঙে দিয়েছে** (`tsc -b` ১৬ error) আর একটা card rule চুপচাপ কাজ করছে না; Munna-র পরের কাজ ওটাই। 🔴 **এবং test suite এখন শেষই হতে পারে না** — তিনটে approval executor-এ `reject()` নেই, PHP fatal দেয়; তাই "২৫টা লাল" সংখ্যাটাও আর যাচাই করা যাচ্ছে না। **বাকি কাজ গুনে ঠিক করা হয়েছে: 18 cards · 88 pts** (আগে ভুল করে 23 cards · ~104 pts লেখা ছিল) · 16 Aug 2026 — 🔴 **attendance suite-এ ২৫টা test লাল** (পুরনো ফাটল, কোনো এক PR-এর দোষ নয় — নিচের নোট); **`9.1-BE`** (PR #187) merged ⚠ তিনটে DoD-ঋণ সহ — Ashraful-এর পরের card **`6.1-BE`**; **`5.5-BE`** (PR #185) merged ⚠ — Bablu-র পরের card **`5.5-FE`**; **`5.3b`-র তিনটে DoD-ঋণ শোধ** (PR #186) — Munna-র পরের card **`5.3-FE`** · 13 Aug 2026 (দ্বিতীয় পাস) — **`5.3a-BE`** (PR #180) ও **`5.4-FE`** (PR #181) merged · 13 Aug 2026 — **`2.2-FE`** (PR #182) merged → **Story 2.2 ও Wave 3 সম্পূর্ণ** (43/43 pts) · 11 Aug 2026 — **`3.2-FE`** (PR #165) ও **`5.4-BE`** (PR #163) merged; **Story 5.2 সম্পূর্ণ** (`5.2-FE` Siam; `5.2-BE` Munna-র draft Siam ঠিক করেছেন) · seeder hotfix · **10.2-BE** · **7.4-FE** done; **7.5-BE** (PR #160) ও **7.5-FE** (PR #161) merged; **10.3-BE** Siam ফেরত নিয়ে ডেলিভার করেছেন (Lane C-তে যাওয়ার পর Bablu `5.4-BE`-তে ব্যস্ত ছিল) — **`8.3b-BE`-র শেষ ঋণটাও শোধ**; **3.2-FE + 2.2-FE** Lane C → Lane A · **বাকি কাজ:** 23 cards · ~104 pts · **টিম:** ৪ জন
**তৈরি:** 10 Aug 2026 · **আপডেট:** 24 Aug 2026 — ✅ **`8.2-FE` Payslips UI done** (run list/generate/poll, detail settlement+frozen allocations, My Payslips)। Wave 6 **36/59**, বাকি **4 card / 23 pts**, পরের spine **`8.3a-BE`**। · 24 Aug 2026 — ✅ **`8.2b-BE` Advance Settlement & Payment Allocation done** (paid advance নেট, `PaymentAllocator` freeze, allocations GET)। Wave 6 **28/59**, বাকি **5 card / 31 pts**, পরের spine **`8.3a-BE`**। · 24 Aug 2026 — ✅ **`8.2a-BE` Payslip Generation done** (`payslips`, queued generate, Finaliser pass-through, **19** test)। Wave 6 **23/59**, বাকি **6 card / 36 pts**, পরের spine **`8.2b-BE`**। `8.2-FE` unlock। ⚠ PR **#228** এই card নয়। বিস্তারিত [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md)। · 20 Aug 2026 — 🎨 **UI design pass** (card নয়, points অপরিবর্তিত, কারো lane এগোয়নি): breadcrumb ও `PageHeader`-এর subtitle বাদ, হেডলাইন বার ~82px→~60px, header বাটন `md`→`sm`, `DataTable`-এর row **40px→28px**, Employee list-এ select bar আর টেবিল ঠেলে না, company switch-এ overlay + toast। 62 ফাইল · +226/−278, shared chrome-এ — তাই **সবার screen-ই দেখতে বদলাবে**, যদিও কারো card-এর কাজ বদলায়নি। `tsc -b` ১৫ error baseline-এর সাথে এক, `vite build` সবুজ; `check-style-boundary.sh` আগে থেকেই লাল (`JobStatusPanel.tsx`, `9.4-FE`-র ঋণ) — **`npm run build` ওখানেই আটকে আছে**। বিস্তারিত [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md)-এর শেষে। · 18 Aug 2026 (দ্বিতীয় পাস) — ⚠ **`6.1-FE` Monthly Approval UI merged (PR #198, Bablu)** — screen live, কিন্তু **দুটো gate ভেঙে**: `tsc -b` **১৫ error** (build লাল) আর `php artisan test` **1 failed / 452 passed** (bulk-approve-এর contract queued batch-এ বদলেছে, `6.1-BE`-র test আপডেট হয়নি)। **Bablu-র পরের কাজ ওটাই**, তারপর ছয়টা DoD-ঋণ — সবচেয়ে জরুরি: **detail page-এর Approve সবসময় `override: true` পাঠায়**, অর্থাৎ unresolved guard UI থেকে বন্ধ। **বাকি কাজ: 12 cards · 61 pts।** · 18 Aug 2026 — ✅ **`9.4-FE` Attendance Jobs UI merged (PR #197, Munna)** — Attendance › Jobs screen live, তাই **Munna-র Lane B-তে jobs track শেষ**; পরের card `8.2-FE` এখনো `8.2a-BE`-র অপেক্ষায়, মাঝখানে **`9.4-FE`-র চারটে ঋণ** শোধের কাজ আছে (নিচে)। **তিনটে blocker-ই শেষ:** `php artisan test` **453 passed / 1438 assertions**, `Modules/Attendance/tests` **79 passed**, `tsc -b --force` ০ error, `vite build` সবুজ। **বাকি কাজ: 13 cards · 66 pts।** · 17 Aug 2026 — 🐛 **Platform-এ দুটো infinite render loop সারানো** (`UserRolesDrawer`, `RolePermissionsDrawer` — "Maximum update depth exceeded"); card নয়, points অপরিবর্তিত, বিস্তারিত নিচে। · 17 Aug 2026 — ✅ **`5.3-FE`-র build-ঋণ শোধ:** `npm run build` আবার সবুজ (`tsc -b` ০ error), "Refresh Balance" button render হয়, আর day breakdown-টা runtime-এও সত্যিই আসে (normalizer `days` ফেলে দিচ্ছিল — আগের নোটে উল্টো লেখা ছিল)। **Munna এখন `9.2-BE` ধরতে পারে।** · 17 Aug 2026 — **`5.3-FE`** (PR #188) merged ⚠ — **Wave 4-এ এখন শুধু `5.5-FE` বাকি**, কিন্তু ঐ PR **`npm run build` ভেঙে দিয়েছে** (`tsc -b` ১৬ error) আর একটা card rule চুপচাপ কাজ করছে না; Munna-র পরের কাজ ওটাই। 🔴 **এবং test suite এখন শেষই হতে পারে না** — তিনটে approval executor-এ `reject()` নেই, PHP fatal দেয়; তাই "২৫টা লাল" সংখ্যাটাও আর যাচাই করা যাচ্ছে না। **বাকি কাজ গুনে ঠিক করা হয়েছে: 18 cards · 88 pts** (আগে ভুল করে 23 cards · ~104 pts লেখা ছিল) · 16 Aug 2026 — 🔴 **attendance suite-এ ২৫টা test লাল** (পুরনো ফাটল, কোনো এক PR-এর দোষ নয় — নিচের নোট); **`9.1-BE`** (PR #187) merged ⚠ তিনটে DoD-ঋণ সহ — Ashraful-এর পরের card **`6.1-BE`**; **`5.5-BE`** (PR #185) merged ⚠ — Bablu-র পরের card **`5.5-FE`**; **`5.3b`-র তিনটে DoD-ঋণ শোধ** (PR #186) — Munna-র পরের card **`5.3-FE`** · 13 Aug 2026 (দ্বিতীয় পাস) — **`5.3a-BE`** (PR #180) ও **`5.4-FE`** (PR #181) merged · 13 Aug 2026 — **`2.2-FE`** (PR #182) merged → **Story 2.2 ও Wave 3 সম্পূর্ণ** (43/43 pts) · 11 Aug 2026 — **`3.2-FE`** (PR #165) ও **`5.4-BE`** (PR #163) merged; **Story 5.2 সম্পূর্ণ** (`5.2-FE` Siam; `5.2-BE` Munna-র draft Siam ঠিক করেছেন) · seeder hotfix · **10.2-BE** · **7.4-FE** done; **7.5-BE** (PR #160) ও **7.5-FE** (PR #161) merged; **10.3-BE** Siam ফেরত নিয়ে ডেলিভার করেছেন (Lane C-তে যাওয়ার পর Bablu `5.4-BE`-তে ব্যস্ত ছিল) — **`8.3b-BE`-র শেষ ঋণটাও শোধ**; **3.2-FE + 2.2-FE** Lane C → Lane A · **বাকি কাজ:** 23 cards · ~104 pts · **টিম:** ৪ জন
**তৈরি:** 10 Aug 2026 · **আপডেট:** 13 Sep 2026 — ✅ **CORS local+server (card নয়, points অপরিবর্তিত)।** loopback + public host FE origins from `VITE_API_URL`. · 13 Sep 2026 — ✅ **Production CORS fix (card নয়, points অপরিবর্তিত)।** `CorsOriginResolver` + prod php-fpm healthcheck + deploy CORS preflight; `.env` regenerate হয় না। · 10 Sep 2026 — ✅ **CI/CD deploy branch → `development` (card নয়, points অপরিবর্তিত)।** `main` বাদ; `development` = deploy branch; fail → previous-SHA rollback. · 6 Sep 2026 — ✅ **`AttendanceCorrectionExecutor` missing_in/out supersede (card নয়, points অপরিবর্তিত)।** existing same-type punch → `superseded_by_id`; **20/20** green. · 6 Sep 2026 — ✅ **CI/CD (GitHub Actions + SHA deploy, card নয়, points অপরিবর্তিত)।** `.github/workflows/ci.yml` · `scripts/deploy-production.sh` · [ops/CICD.md](../ops/CICD.md)। · 6 Sep 2026 — ✅ **PHPUnit 31-failure cleanup (CI baseline, card নয়, points অপরিবর্তিত)।** `make test` **767 passed / 2585 assertions**। বিস্তারিত [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md)। · 25 Aug 2026 — ✅ **`8.3b-FE` Disbursement UI done** (`/payroll/disbursement-batches`, channel summary + readiness, bank/mobile vs cash/cheque detail, A4 register print, nav Payroll › Disbursement)। **Wave 6 বন্ধ 59/59**। · 25 Aug 2026 — ✅ **`8.3b-BE` Multi-Channel Disbursement done** (batches/items, allocation-driven, channel-aware readiness, send/confirm/retry/ack, export, **18** test)। Wave 6 **54/59**, বাকি **1 card / 5 pts**, পরের spine **`8.3b-FE`**। · 25 Aug 2026 — ✅ **`8.3a-FE` Run Approval UI done** (`/payroll/payroll-runs/{id}/approval`, platform approve/reject, approval-summary)। Wave 6 **46/59**, বাকি **2 card / 13 pts**, পরের spine **`8.3b-BE`**। · 25 Aug 2026 — ✅ **`8.3a-BE` Run Approval & Advance Settlement done** (`PayrollRunExecutor`, paid→settled, carry-forward, `approval-summary`, **9** test)। Wave 6 **41/59**, বাকি **3 card / 18 pts**, পরের spine **`8.3b-BE`**। · 24 Aug 2026 — ✅ **`8.2-FE` Payslips UI done** (run list/generate/poll, detail settlement+frozen allocations, My Payslips)। Wave 6 **36/59**, বাকি **4 card / 23 pts**, পরের spine **`8.3a-BE`**। · 24 Aug 2026 — ✅ **`8.2b-BE` Advance Settlement & Payment Allocation done** (paid advance নেট, `PaymentAllocator` freeze, allocations GET)। Wave 6 **28/59**, বাকি **5 card / 31 pts**, পরের spine **`8.3a-BE`**। · 24 Aug 2026 — ✅ **`8.2a-BE` Payslip Generation done** (`payslips`, queued generate, Finaliser pass-through, **19** test)। Wave 6 **23/59**, বাকি **6 card / 36 pts**, পরের spine **`8.2b-BE`**। `8.2-FE` unlock। ⚠ PR **#228** এই card নয়। বিস্তারিত [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md)। · 20 Aug 2026 — 🎨 **UI design pass** (card নয়, points অপরিবর্তিত, কারো lane এগোয়নি): breadcrumb ও `PageHeader`-এর subtitle বাদ, হেডলাইন বার ~82px→~60px, header বাটন `md`→`sm`, `DataTable`-এর row **40px→28px**, Employee list-এ select bar আর টেবিল ঠেলে না, company switch-এ overlay + toast। 62 ফাইল · +226/−278, shared chrome-এ — তাই **সবার screen-ই দেখতে বদলাবে**, যদিও কারো card-এর কাজ বদলায়নি। `tsc -b` ১৫ error baseline-এর সাথে এক, `vite build` সবুজ; `check-style-boundary.sh` আগে থেকেই লাল (`JobStatusPanel.tsx`, `9.4-FE`-র ঋণ) — **`npm run build` ওখানেই আটকে আছে**। বিস্তারিত [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md)-এর শেষে। · 18 Aug 2026 (দ্বিতীয় পাস) — ⚠ **`6.1-FE` Monthly Approval UI merged (PR #198, Bablu)** — screen live, কিন্তু **দুটো gate ভেঙে**: `tsc -b` **১৫ error** (build লাল) আর `php artisan test` **1 failed / 452 passed** (bulk-approve-এর contract queued batch-এ বদলেছে, `6.1-BE`-র test আপডেট হয়নি)। **Bablu-র পরের কাজ ওটাই**, তারপর ছয়টা DoD-ঋণ — সবচেয়ে জরুরি: **detail page-এর Approve সবসময় `override: true` পাঠায়**, অর্থাৎ unresolved guard UI থেকে বন্ধ। **বাকি কাজ: 12 cards · 61 pts।** · 18 Aug 2026 — ✅ **`9.4-FE` Attendance Jobs UI merged (PR #197, Munna)** — Attendance › Jobs screen live, তাই **Munna-র Lane B-তে jobs track শেষ**; পরের card `8.2-FE` এখনো `8.2a-BE`-র অপেক্ষায়, মাঝখানে **`9.4-FE`-র চারটে ঋণ** শোধের কাজ আছে (নিচে)। **তিনটে blocker-ই শেষ:** `php artisan test` **453 passed / 1438 assertions**, `Modules/Attendance/tests` **79 passed**, `tsc -b --force` ০ error, `vite build` সবুজ। **বাকি কাজ: 13 cards · 66 pts।** · 17 Aug 2026 — 🐛 **Platform-এ দুটো infinite render loop সারানো** (`UserRolesDrawer`, `RolePermissionsDrawer` — "Maximum update depth exceeded"); card নয়, points অপরিবর্তিত, বিস্তারিত নিচে। · 17 Aug 2026 — ✅ **`5.3-FE`-র build-ঋণ শোধ:** `npm run build` আবার সবুজ (`tsc -b` ০ error), "Refresh Balance" button render হয়, আর day breakdown-টা runtime-এও সত্যিই আসে (normalizer `days` ফেলে দিচ্ছিল — আগের নোটে উল্টো লেখা ছিল)। **Munna এখন `9.2-BE` ধরতে পারে।** · 17 Aug 2026 — **`5.3-FE`** (PR #188) merged ⚠ — **Wave 4-এ এখন শুধু `5.5-FE` বাকি**, কিন্তু ঐ PR **`npm run build` ভেঙে দিয়েছে** (`tsc -b` ১৬ error) আর একটা card rule চুপচাপ কাজ করছে না; Munna-র পরের কাজ ওটাই। 🔴 **এবং test suite এখন শেষই হতে পারে না** — তিনটে approval executor-এ `reject()` নেই, PHP fatal দেয়; তাই "২৫টা লাল" সংখ্যাটাও আর যাচাই করা যাচ্ছে না। **বাকি কাজ গুনে ঠিক করা হয়েছে: 18 cards · 88 pts** (আগে ভুল করে 23 cards · ~104 pts লেখা ছিল) · 16 Aug 2026 — 🔴 **attendance suite-এ ২৫টা test লাল** (পুরনো ফাটল, কোনো এক PR-এর দোষ নয় — নিচের নোট); **`9.1-BE`** (PR #187) merged ⚠ তিনটে DoD-ঋণ সহ — Ashraful-এর পরের card **`6.1-BE`**; **`5.5-BE`** (PR #185) merged ⚠ — Bablu-র পরের card **`5.5-FE`**; **`5.3b`-র তিনটে DoD-ঋণ শোধ** (PR #186) — Munna-র পরের card **`5.3-FE`** · 13 Aug 2026 (দ্বিতীয় পাস) — **`5.3a-BE`** (PR #180) ও **`5.4-FE`** (PR #181) merged · 13 Aug 2026 — **`2.2-FE`** (PR #182) merged → **Story 2.2 ও Wave 3 সম্পূর্ণ** (43/43 pts) · 11 Aug 2026 — **`3.2-FE`** (PR #165) ও **`5.4-BE`** (PR #163) merged; **Story 5.2 সম্পূর্ণ** (`5.2-FE` Siam; `5.2-BE` Munna-র draft Siam ঠিক করেছেন) · seeder hotfix · **10.2-BE** · **7.4-FE** done; **7.5-BE** (PR #160) ও **7.5-FE** (PR #161) merged; **10.3-BE** Siam ফেরত নিয়ে ডেলিভার করেছেন (Lane C-তে যাওয়ার পর Bablu `5.4-BE`-তে ব্যস্ত ছিল) — **`8.3b-BE`-র শেষ ঋণটাও শোধ**; **3.2-FE + 2.2-FE** Lane C → Lane A · **বাকি কাজ:** 23 cards · ~104 pts · **টিম:** ৪ জন

এই ফাইল sequence ডকের **অর্ডার বদলায় না** — শুধু কে কোন lane নেবে সেটা ঠিক করে। কোনো দ্বন্দ্ব হলে sequence ডকই চূড়ান্ত।

---

## টিম ও Lane

| নাম | ভূমিকা | Lane | বাকি pts |
| --- | --- | --- | --- |
| **Siam** | PM + Tech Lead + Dev | **Lane D** — Payroll Config & Money Inputs *(ইচ্ছাকৃতভাবে critical path-এর বাইরে)* | **0 — lane শেষ** (+ `9.3-BE` তুলে নিয়েছেন) |
| **Ashraful** | Dev | **Lane A** — Record screen + Close → Payroll Run **BE spine** *(critical path)* | ~18 |
| **Munna** | Dev | **Lane B** — Leave, Jobs & Payslip UI | ~0 (ঋণ শোধ) |
| **Bablu** | Dev | **Lane C** — Corrections, Records & Run UI | ~10 |
| **Ashraful** | Dev | **Lane A** — Record screen + Close → Payroll Run **BE spine** *(critical path)* | ~0 (spine done) |
| **Munna** | Dev | **Lane B** — Leave, Jobs & Payslip UI | ~0 (ঋণ শোধ) |
| **Bablu** | Dev | **Lane C** — Corrections, Records & Run UI | ~0 |

---

## কেন এই ভাগ

১. সবচেয়ে লম্বা serial chain — **6.1-BE → 6.2-BE → 8.1-BE → 8.0-BE → 8.2a-BE → 8.2b-BE → 8.3a-BE → 8.3b-BE = ৪৭ pts** — কোনোভাবেই parallel করা যায় না। এটাই ship date ঠিক করে, তাই পুরো BE spine **একজনের** (Ashraful) হাতে — hand-off-এ সময় নষ্ট হবে না।
২. **Siam-এর কাজ ইচ্ছাকৃতভাবে critical path-এর বাইরে রাখা হয়েছে।** PM + review-এর interruption যেন কারো কাজ আটকে না দেয়। তাঁর card payroll-এর, কিন্তু spine-এর পাশে — `7.5` পুরো chain-এ **stub-able** ছিল, আর spine ওখানে পৌঁছানোর আগেই শেষ হয়ে গেছে।
৩. তবু money-math-এর সবচেয়ে ঝুঁকিপূর্ণ রুল — **advance = payment (R3)** — lead-এর হাতেই থাকছে (`7.5-BE`), কারণ ভুল হলে `8.2b` settlement পুরো ভুল হবে। ✅ ceiling arithmetic আলাদা pure class-এ, DB-মুক্ত unit test সহ।
৪. Leave chain (**5.2 → 5.3a → 5.3b**) সম্পূর্ণ স্বাধীন — আলাদা টেবিল, আলাদা স্ক্রিন। Munna একা পুরোটা নিতে পারে।
৫. **BE spine একজনের, FE অন্যদের** — Ashraful `8.1-BE` merge করলে Bablu `8.1-FE` ধরে, `8.2a-BE` merge করলে Munna `8.2-FE` ধরে। এতে spine কখনো FE-র জন্য থামে না।
৬. **একই টেবিল দুই lane-এ যাবে না** — এটাই merge conflict এড়ানোর মূল নিয়ম। একই কারণে **record screen (3.2-FE + 2.2-FE) Ashraful-এর হাতে** — 3.1/3.2-BE ওরই লেখা, আর 2.2-FE ওই একই detail স্ক্রিনের ভিতরে বসে।
৭. **10.3-BE Lane D → Lane C → ফের Lane D** (10 Aug): 10.2-BE done হওয়ায় card-টা খুলেছিল, তখন Siam `7.5-BE`-তে ছিলেন বলে Bablu-কে দেওয়া হয়। কিন্তু Bablu `5.4-BE` ধরে ফেলায় আর Siam-এর lane খালি হওয়ায় Siam-ই শেষ করেছেন — `8.3b-BE`-র গেট ঝুলিয়ে রাখা যেত না। ✅ done।

---

## Lane D — Siam · Payroll Config & Money Inputs

**মালিকানা:** employee payment methods, salary advances, payroll config UI
**কেন এই lane:** সব card critical path-এর বাইরে, তাই PM/review-এ ব্যস্ত থাকলেও কেউ block হবে না। শুরুতে ২২ pts; **10.2-BE** ও **7.4-FE** শেষ, **7.5-BE + 7.5-FE** দুটোই merged, আর **10.3-BE** Lane C থেকে ফেরত এনে শেষ করা হয়েছে — **lane-এর সব card শেষ এবং Ashraful-এর দিকে আর কোনো ঋণ নেই**; Siam এখন পুরোপুরি PM + review-এ।

| # | Card | Pts | Status | নোট |
| - | ---- | --- | ------ | --- |
| 0 | **Seeder hotfix** | ~0.5 দিন | ✅ done | `TestCase::upsertEmployeeProfile()` helper — সব Employee feature test setUp একই প্যাটার্নে; `SeederIdempotencyTest` যোগ। Employee suite **87 test green**। |
| 1 | **10.2-BE** Bank Account Validation | 1 → **3** | ✅ done | তিনটা bug-ই fix — নিচের ব্রিফ দেখুন |
| 2 | **7.5-BE** Salary Advances | 8 | ✅ done | **PR #160** — ৮ endpoint, frozen `basis_gross`, মাস-total ceiling, দুই approval path, executor body; ৪৪ test green। `8.2b-BE`-এর গেট খুলে গেছে — নিচের ব্রিফ দেখুন। |
| 3 | **7.5-FE** Salary Advances UI | 5 | ✅ done | **PR #161** — single screen, সব operation stacked Offcanvas-এ, page change নেই। Headroom form-এই cap করা, তাই doomed request কখনো server-এ যায় না। `advance_enabled = false` হলে nav item ও route দুটোই নেই। |
| 4 | **10.3-BE** Missing Payment Method Report | 3 | ✅ done | Lane C-তে গিয়েছিল, কিন্তু Bablu `5.4-BE`-তে ঢুকে গেছে আর Ashraful-এর `8.3b-BE` এটার উপর দাঁড়ায় — তাই Siam ফেরত নিয়েছেন। নিচের ব্রিফ দেখুন। |
| — | ~~**7.4-FE** Deductions & Loans UI~~ | 3 | ✅ done | **Bablu** শেষ করেছে (PR #157) — nav `deductions: ready: true`, list + detail page। এতে payroll Configuration section সম্পূর্ণ। |

---

## Lane A — Ashraful · Close → Payroll Run (BE spine, critical path)

**মালিকানা:** attendance record screen (3.1/3.2 যার লেখা), `attendance_records`-এর status/lock, monthly close, পুরো payroll run pipeline (BE)
**নিয়ম:** spine chain-এর কোনো card skip করা যাবে না, একটার পর একটা। **ব্যতিক্রম শুধু 3.2-FE + 2.2-FE** — 3.1/3.2-BE ওরই লেখা, তাই record screen-ও ওর হাতে; বাকি FE অন্য lane ধরবে।

| # | Card | Pts | Status | নোট |
| - | ---- | --- | ------ | --- |
| — | ~~**3.2-FE** Daily Summary / Records~~ | 5 | ✅ done | **PR #165** — record grid + detail view; nav `/attendance/attendance-records`। Bablu-র `5.4-FE`-র host screen এখন প্রস্তুত। |
| 0b | **2.2-FE** Applied Policy Panel | 2 | ✅ done (PR #182) | Panel-টা snapshot-এর পুরো ফিল্ড সেট দেখায় — grace · working hours · min-hours present/half-day · working days · timezone · `resolved_at`, প্রতিটাই snapshot না থাকলে live shift-এ fallback করে। **Recalculate split control** এসেছে `attendance.record-recalculate` permission gate + "CONFIRM" টাইপ করার confirmation সহ, আর locked record-এ control-টা render-ই হয় না। **Story 2.2 সম্পূর্ণ।** |
| — | ~~**9.1-BE** Nightly Attendance Close~~ | 5 | ✅ done ⚠ (PR #187) | `ScheduleCloseDayCommand` hourly চলে, প্রতিটা company-র নিজের timezone-এ `hour === 2` হলে batch ছাড়ে; ২০০-র chunk-এ `ProcessCloseDayChunkJob` → `calculateDaily`। **Idempotency query-তেই** (`whereDoesntHave` — ঐ তারিখে record থাকা employee বাদ)। Manual trigger + batch status endpoint আছে। **⚠ তিনটে DoD-ঋণ:** দুই-timezone scheduler test · idempotency test · **unassigned employee কেবল log-এ, HR-এর report-এ নয়** (card স্পষ্ট বলেছিল "not only in logs"; `9.4-FE` এটার উপর দাঁড়ায়)। |
| — | ~~**6.1-BE** Monthly Attendance Approval~~ | 8 | ✅ done (PR #192) | HR মাস বন্ধ করে — ৯টা endpoint। দুটো gate-ই এখন সবুজ: suite 452/452, আর `3.2-BE`-র টেস্ট-ঋণ শোধ। |
| — | ~~**6.2-BE** Monthly Freeze~~ | 3 | ✅ done (PR #195) | Finance lock — payroll run-এর hard gate |
| 4 | ~~**8.1-BE** Payroll Run Creation~~ | 5 | ✅ done ⚠ | `payroll_runs` + `PayrollRunService`; নিজের দুটো test ফাইল চলে না (`Company::factory` · PHPUnit 12 `@dataProvider`) |
| 5 | ~~**8.0-BE** Attendance Snapshot~~ | 5 | ✅ done | `attendance_snapshots` + build/list/show/divergence; `getPayrollAttendanceForRun()`; ১৭ test |
| 6 | ~~**8.2a-BE** Payslip Generation~~ | 8 | ✅ done | `payslips` + chunked queue + `PayslipFinaliser` pass-through; attendance শুধু snapshot। `PayslipGenerationTest` **17** + Finaliser **2** সবুজ। |
| 7 | ~~**8.2b-BE** Advance Settlement & Allocation~~ | 5 | ✅ done | Paid advance নেট; `PaymentAllocator` freeze; draft generation advance row ছোঁয় না। `PayslipSettlementTest` + allocator matrix সবুজ। |
| 8 | ~~**8.3a-BE** Run Approval~~ | 5 | ✅ done | `PayrollRunExecutor` · paid→settled · carry-forward · `approval-summary` · bypass/workflow · **9** test |
| 9 | ~~**8.3b-BE** Multi-Channel Disbursement~~ | 8 | ✅ done | Allocation-driven batches; channel-aware readiness; send/confirm/retry; cash/cheque acknowledge; export; **18** test |

---

## Lane B — Munna · Leave, Jobs & Payslip UI

**মালিকানা:** leave tables, balances, approval executor, scheduled jobs, payslip স্ক্রিন
**নিয়ম:** শুরুর ৬টা card সম্পূর্ণ স্বাধীন — কারো merge-এর অপেক্ষা করতে হবে না।

| # | Card | Pts | Status | নোট |
| - | ---- | --- | ------ | --- |
| — | ~~**5.2-BE** Leave Application~~ | 5 | ✅ done | Munna-র draft, **Siam** ঠিক করে শেষ করেছেন — working-day count ISO day-number-এ (আগে সবসময় 0 আসত), preview/store এক path, preview আর throw করে না, `show`/`index`-এর IDOR বন্ধ, resource + paginated envelope; নতুন `applicable-policies` endpoint (assign করা policy dropdown-এর জন্য — আগে balance row-এর উপর নির্ভর করত, তাই খালি আসত)। 32 test green। |
| — | ~~**5.3a-BE** Leave Approval~~ | 5 | ✅ done (PR #180) | Executor সম্পূর্ণ — request + balance দুটোই lock, execution-time-এ balance পুনঃযাচাই, `consumption` ledger, প্রতি working day-তে `leave_request_days` row + leave type stamp, `sum(day_value) != total_days` হলে 422। Overlap · locked month · already-finalized তিনটেই 409। Cancel path day row void করে `reversal` লেখে। |
| — | ~~**5.2-FE** Leave Application UI~~ | 5 | ✅ done | **Siam** শেষ করেছেন — `leaveRequestApi.ts` + debounced preview hook, তিনটা route, nav **Attendance › Leave › Requests**। Day count ও balance সবই server-owned; holiday/weekly-off `preview.excluded_dates` থেকে grey হয়। |
| — | ~~**5.3b-এর ঋণ শোধ**~~ | — | ✅ done (PR #186) | তিনটেই মিটেছে: (ক) `calculateDaily` এখন পুরোটা `DB::transaction`-এ; (খ) `LeaveDayVoided` কেবল resolver-এ একবার fire হয়; (গ) `PunchLeaveVoidTest` — **7 test green (32 assertion)**, DoD-র চারটে case-ই ঢাকা। Re-entrancy সামলাতে `voidLeaveDay()`-এ default-`true` তৃতীয় parameter, তাই কোনো caller-এর signature ভাঙেনি। |
| — | ~~**5.3-FE** Leave Approvals UI~~ | 3 | ✅ done ⚠ (PR #188) | HR leave queue + detail + reject modal। Card-এর কঠিন rule গুলো সত্যিই আছে — balance detail খুললে নতুন করে fetch হয়, loading/insufficient অবস্থায় Approve disabled, reject reason খালি হলে আটকায়, day breakdown-এ voided-by-punch + তারিখ। **✅ build-ঋণ শোধ (17 Aug 2026):** `tsc -b` ০ error, `Alert`-এ `tone`/`actions` করায় "Refresh Balance" button render হয়, আর `normalizeRequest`-এ `days` carry করায় day breakdown runtime-এও আসে। |
| — | ~~**5.3b-BE** Punch Voids Leave~~ | 3 | ✅ done | আচরণ এসেছিল 5.3a-র PR-এ (#180), **DoD সম্পূর্ণ হয়েছে PR #186-এ**। আসল `LeaveDayResolverService` bind, `calculateDaily`-তে R2 branch card-এর pseudocode মেনে বসেছে, আর তিনটে ঋণই শোধ (উপরের row)। |
| — | ~~**9.2-BE** Leave Accrual & Carry-Forward~~ | 5 | ✅ done (PR #190) | দুটো scheduled command + দুটো chunk job + manual trigger endpoint (`/jobs/accrue-leave` · `/jobs/carry-forward`) + batch status। `LeaveAccrualJobTest` green। **18 Aug-এ যোগ হয়েছে annual entitlement cap guard** — `entitled_days` আর কখনো `entitlement_days` ছাড়ায় না (PR #197-এ)। |
| — | ~~**9.3-BE** Default Data Seeders~~ | 3 | ✅ done | **Siam** শেষ করেছে — per-company seeding + `Company::created` provisioning hook + Sun–Thu default shift; ৬ test green। |
| — | ~~**9.4-FE** Attendance Jobs UI~~ | 3 | ✅ done ⚠ (PR #197) | `/attendance/jobs` — unassigned report (primary content) + তিন-tab manual trigger + live batch progress। Progress `sessionStorage` + `?batch=`-এ থাকে, তাই page ছেড়ে ফিরলেও ফেরে; permission না থাকলে run panel render-ই হয় না; re-run accrual "0 applied" success দেখায়। **⚠ চারটে ঋণ:** "Fix Assignment" link কোনো filter লাগায় না (assignment list `search` param পড়ে না) · timezone hardcoded `Asia/Dhaka` · status panel unfiltered activity log পড়ে (৩০টার মধ্যে job log না থাকলে "Never run" মিথ্যে দেখাবে) · `JobStatusPanel.tsx` dead code। বিস্তারিত sequence ফাইলে। |
| 9 | ~~**8.2-FE** Payslips UI~~ | 8 | ✅ done | Run payslips + My Payslips + detail; frozen `/allocations`; settlement ৩ লাইন; generate/poll/retry। |

---

## Lane C — Bablu · Corrections, Records & Run UI

**মালিকানা:** correction flow (BE+FE), monthly approval ও payroll run-এর স্ক্রিন
**সদ্য শেষ:** **7.4-FE** Deductions & Loans UI (PR #157) — Lane D-র card ছিল, Bablu ডেলিভার করেছে।

| # | Card | Pts | Status | নোট |
| - | ---- | --- | ------ | --- |
| — | ~~**5.4-BE** Correction Request~~ | 3 | ✅ done | **PR #163** — `correction_requests` + store/index/show/cancel; duplicate guard; policy + approval executor; 7 test green। |
| — | ~~**10.3-BE** Missing Payment Method Report~~ | 3 | ✅ done | **Siam ফেরত নিয়েছেন** — Bablu `5.4-BE`-তে, আর Ashraful-এর `8.3b-BE` এটার উপর দাঁড়ায়। endpoint + `EmployeeMissingPaymentMethodReportServiceInterface`; 17 test green। |
| — | ~~**5.5-BE** Correction Approval~~ | 5 | ✅ done ⚠ (PR #185) | `AttendanceCorrectionExecutor` পাঁচ type-ই সামলায়; punch কেবল `superseded_by_id`-তে বদলায়, মোছে না; locked-month override 403/পাশ; reject-এ reason বাধ্যতামূলক; `CorrectionDecided` emit। `index`/`show` এখন approver-এর কাছেও খোলে (আগে HR নিজের queue দেখত না)। **15 test green**। **⚠ cross-lane gate ভেঙে merged** — `9.1-BE`-র আগে, নিচের নোট দেখুন। |
| — | ~~**5.4-FE** Correction Request UI~~ | 3 | ✅ done | **PR #181** — `pages/corrections/` list · detail · form; `CorrectionRequestForm` + `CorrectionDatePicker`; `correctionRequestApi.ts` + types; nav **Attendance › Corrections**; record detail থেকেও entry। BE-তে `CorrectionRequestService` সামান্য বদল + নতুন `CorrectionRequestApiTest`। |
| — | ~~**5.5-FE** Correction Approval UI~~ | 3 | ✅ done (PR #189) | HR correction queue + detail + day comparison + reject modal + locked-month banner। PR-এর `tsc -b` ঋণ পরে শোধ হয়েছে। |
| — | ~~**6.1-FE** Monthly Approval UI~~ | 5 | ✅ done ⚠ (PR #198) | Grid + month/year/status filter + employee search + build drawer + bulk approve (queued batch, live progress, succeeded/failed result table) + detail page; nav ও home card-এর dead `#` link সরানো। BE-তেও বড় কাজ — build ও bulk-approve এখন **202 queued** (দুটো নতুন job + cache batch status + `GET /batch/{batchId}`)। **⚠ `tsc -b` ১৫ error + `MonthlyAttendanceApprovalServiceTest` লাল** (contract বদলেছে, test নয়)। **ছয়টা DoD-ঋণ:** Approve সবসময় `override: true` · unlock reason hardcoded · unresolved popover-এ blocker list/deep link নেই (API-ও দেয় না) · day-by-day breakdown placeholder · reject action নেই · permission gating নেই ও frozen-এ Unlock লুকোয় না। বিস্তারিত sequence ফাইলে। |
| 2 | ~~**6.2-FE** Freeze UI~~ | 2 | ✅ done | Payroll › Monthly Freeze; query `month`/`year`/`employee_id` |
| 7 | ~~**8.1-FE** Payroll Run UI~~ | 3 | ✅ done ⚠ (PR #209) | list/create/detail; tsc ঋণ |
| 8 | ~~**8.0-FE** Snapshot UI~~ | 2 | ✅ done | Run-এ snapshot list/detail, draft build, freeze pre-flight (`employee_id` query), diagnostic divergence + snapshot/live contrast |
| 9 | ~~**8.3a-FE** Run Approval UI~~ | 5 | ✅ done | Decision screen · platform approve/reject · approval-summary · Net Pay≠Net Payable |
| 10 | ~~**8.3b-FE** Disbursement UI~~ | 5 | ✅ done | Batch ও register স্ক্রিন · `disbursementApi.ts` · A4 print |

---

## Phase 1 — এই সপ্তাহে যা হাতে যাচ্ছে

| জন | কাজ | Pts |
| --- | --- | --- |
| **Siam** | ~~Seeder hotfix~~ ✅ → ~~`10.2-BE`~~ ✅ → ~~`7.5-BE`~~ ✅ → ~~`7.5-FE`~~ ✅ → ~~`5.2-FE`~~ ✅ → ~~`5.2-BE` fix~~ ✅ *(Lane B থেকে এগিয়ে নেওয়া, Story 5.2 সম্পূর্ণ)* → PM + review | 10 |
| **Ashraful** | ~~`3.2-FE`~~ ✅ (PR #165) · ~~`2.2-FE`~~ ✅ (PR #182) · ~~`9.1-BE`~~ ✅ (PR #187, ঋণ শোধ 18 Aug) · ~~`6.1-BE`~~ ✅ (PR #192) · ~~`6.2-BE`~~ ✅ (PR #195) · ~~`8.1-BE`~~ ✅ ⚠ · ~~`8.0-BE`~~ ✅ · ~~`8.2a-BE`~~ ✅ · ~~`8.2b-BE`~~ ✅ → **`8.3a-BE`** *(spine)* | — |
| **Munna** | ~~`5.3a-BE`~~ ✅ (PR #180) → ~~`5.3b`-এর ঋণ শোধ~~ ✅ (PR #186) → ~~`5.3-FE`~~ ✅ (PR #188) → ~~build fix~~ ✅ → ~~`9.2-BE`~~ ✅ (PR #190) → ~~`9.4-FE`~~ ✅ (PR #197) → ~~`8.2-FE`~~ ✅ → **`9.4-FE`-র চারটে ঋণ** | — |
| **Bablu** | ~~`7.4-FE`~~ ✅ … → ~~`5.5-FE`~~ ✅ (PR #189) → ~~`6.1-FE`~~ ✅ ⚠ (PR #198) → ~~`6.2-FE`~~ ✅ · ~~`8.1-FE`~~ ✅ ⚠ · ~~`8.0-FE`~~ ✅ → 🔴 **`6.1-FE` gate/ঋণ**, তারপর `8.3a-FE` | — |
| **Ashraful** | ~~`3.2-FE`~~ ✅ (PR #165) · ~~`2.2-FE`~~ ✅ (PR #182) · ~~`9.1-BE`~~ ✅ (PR #187, ঋণ শোধ 18 Aug) · ~~`6.1-BE`~~ ✅ (PR #192) · ~~`6.2-BE`~~ ✅ (PR #195) · ~~`8.1-BE`~~ ✅ ⚠ · ~~`8.0-BE`~~ ✅ · ~~`8.2a-BE`~~ ✅ · ~~`8.2b-BE`~~ ✅ · ~~`8.3a-BE`~~ ✅ · ~~`8.3b-BE`~~ ✅ → spine **done** | — |
| **Munna** | ~~`5.3a-BE`~~ ✅ (PR #180) → ~~`5.3b`-এর ঋণ শোধ~~ ✅ (PR #186) → ~~`5.3-FE`~~ ✅ (PR #188) → ~~build fix~~ ✅ → ~~`9.2-BE`~~ ✅ (PR #190) → ~~`9.4-FE`~~ ✅ (PR #197) → ~~`8.2-FE`~~ ✅ → **`9.4-FE`-র চারটে ঋণ** | — |
| **Bablu** | ~~`7.4-FE`~~ ✅ … → ~~`5.5-FE`~~ ✅ (PR #189) → ~~`6.1-FE`~~ ✅ ⚠ (PR #198) → ~~`6.2-FE`~~ ✅ · ~~`8.1-FE`~~ ✅ ⚠ · ~~`8.0-FE`~~ ✅ → 🔴 **`6.1-FE` gate/ঋণ**, ~~`8.3a-FE`~~ ✅ · ~~`8.3b-FE`~~ ✅ | — |

চারটাই সম্পূর্ণ স্বাধীন — কেউ কারো PR-এর অপেক্ষা করবে না।

---

## ✅ Siam-এর কাজ — সব শেষ (10 Aug 2026)

চারটাই merged (PR #160 · #161); `Employee` suite **87 passed / 285 assertions**, Salary Advances **44 passed / 153 assertions**, frontend `vite build` clean। নিচের বিবরণ রেকর্ডের জন্য রাখা হলো।

### ০. Seeder hotfix — ✅ done

`database/seeders/AttendancePayrollRoleSeeder.php:87` এখন `employee@erpflow.local`-এর জন্য একটা `EmployeePersonalInfo` row বানায়। Employee module-এর প্রতিটা feature test setUp-এ ওই একই user দিয়ে আরেকটা row create করে → `unique(company_id, user_id)` ভাঙে।

**ফল:** `EmployeeBankAccountValidationTest`, `EmployeeBankAccountConcurrentUpdateTest`, `EmployeeSalaryFieldValidationTest` (10.1-BE, `done` মার্ক করা), `EmployeeDeductionTest`, `EmployeePersonalInfoUpdateTest`, `EmployeeEmploymentUpdateTest`, `EmployeeSalaryPaymentModesApiTest`, `EmployeeSalaryPaymentAccountsTest` — সব fail।

**যা করা হয়েছে:** `Tests\TestCase`-এ শেয়ার্ড `upsertEmployeeProfile()` helper (`company_id` + `user_id`-এর উপর `updateOrCreate`, দুটোই বাধ্যতামূলক) — উপরের সব setUp এখন এই একটাই path ব্যবহার করে। সাথে `SeederIdempotencyTest` — seeder দুবার চালালে row duplicate হয় না, demo employee-র profile ঠিক একটাই। পরে Ashraful/Bablu-র payroll BE টেস্টও এই helper-এর উপরেই দাঁড়াবে।

### ১. 10.2-BE — কার্ডে 1 pt, বাস্তবে ৩টা bug — ✅ done

যে তিনটা bug ধরা পড়েছিল ([EmployeeBankAccountValidationService.php](backend/Modules/Employee/app/Services/EmployeeBankAccountValidationService.php)):

1. **`set-primary` endpoint 500 দেয়।** [EmployeeBankAccountService.php:121](backend/Modules/Employee/app/Services/EmployeeBankAccountService.php#L121) শুধু `['is_primary' => true]` নিয়ে `update()` কল করে, আর `update()` [line 89](backend/Modules/Employee/app/Services/EmployeeBankAccountService.php#L89) সেই partial payload `validateAndClean()`-এ পাঠায় → type detect হয় `bank` → `"Bank name is required for bank accounts."` throw। **কার্ডের মূল AC পুরোপুরি ভাঙা।**
2. **Validation error 422 না, 500 আসে।** Service `\InvalidArgumentException` throw করে, কিন্তু [ApiResponseTrait.php:62-89](backend/app/Http/Traits/ApiResponseTrait.php#L62-L89) শুধু `ValidationException`/`DomainException` map করে।
3. **টেস্ট দুটো কখনো চালানো হয়নি — 11/11 fail।** ভুল route (`/employee/employees/{id}/bank-accounts`, আসলটা `/employee/bank-accounts`), `patchJson` যেখানে route `put`/`post`, কোনো টেস্টই `payment_method_type` পাঠায় না অথচ store-এ ওটা required, আর `test_row_with_both_payment_methods_succeeds` 201 আশা করে যেখানে `detectPaymentMethodType()` দুটো method একসাথে দিলে reject করে।

**সমাধান (তিনটাই):**

1. `update()` এখন partial payload-কে existing row-এর সাথে merge করে তারপর validate করে — `set-primary` আর 500 দেয় না; single-primary swap model-level + row lock দিয়ে atomic।
2. Validation service সব জায়গায় `ValidationException::withMessages()` throw করে → trait-এর মাধ্যমেই 422 + `errors.*` payload।
3. দুটো টেস্ট ফাইল আসল route (`/api/v1/employee/bank-accounts`), আসল HTTP verb ও OR-method semantics অনুযায়ী লেখা হয়েছে; সাথে bank column nullable migration।

**DoD ✅:** `EmployeeBankAccountValidationTest` · `EmployeeBankAccountConcurrentUpdateTest` · `EmployeeSalaryPaymentAccountsTest` · `SeederIdempotencyTest` — **28 passed**; পুরো `Employee` suite **87 passed**।

### ২. 7.5-BE Salary Advances — ✅ done (PR #160)

**R3 যেভাবে বসানো হলো:** `amount` সবসময় server-এ resolve (client পাঠালে `prohibited` → 422); `basis_gross` + `employee_salary_id` request-এর সময় freeze, তাই পরে salary 30k → 40k করলেও advance-এর মান বদলায় না; ceiling **মাসের total-এ**, single request-এ নয়, আর দুই cap (`advance_max_percentage` + `advance_max_amount`) একসাথে খাটে — error payload-এ বাকি headroom যায় যাতে caller ঠিক অঙ্ক দিয়ে retry করতে পারে।

অঙ্কটা [AdvanceCeilingCalculator.php](backend/Modules/Payroll/app/Support/AdvanceCeilingCalculator.php)-এ আলাদা, DB-মুক্ত pure class — `create` আর `summary` দুটোই এটাই ব্যবহার করে, তাই দুই জায়গায় drift করার সুযোগ নেই। ১১টা unit test: single under / single over / দুই request মিলে over / `max_amount = null` / কোন cap bind করে / negative headroom।

**Approval:** `SalaryAdvanceExecutor`-এর body ভরা ও registry-তে বসানো। তিনটা path-ই test করা — payroll policy bypass (`advance_requires_approval = false`, কোনো `approval_requests` row লেখে না), platform-level bypass, আর পুরো workflow (executor exactly একবার)। `approve()` idempotent, তাই re-delivered approval double-stamp করে না।

**দুটো সিদ্ধান্ত জেনে রাখুন:**

1. **`payroll_runs` এখনো নেই (8.1-BE)।** closed-period রুলটা [PayrollPeriodGuard.php](backend/Modules/Payroll/app/Support/PayrollPeriodGuard.php)-এ `Schema::hasTable` দিয়ে — টেবিল না থাকলে period খোলা ধরে। শুধু defer করে রাখা হয়নি: test-এ minimal টেবিল দাঁড় করিয়ে প্রমাণ করা আছে `approved` run হলে create/edit/cancel → **409 run id সহ**, `draft` run হলে খোলা। 8.1-BE merge হলে ওই একটা ফাইলেই আসল query বসবে, বাকি কোড অপরিবর্তিত।
2. **`submit` ও `record-payment` ইচ্ছাকৃতভাবে period-guard মুক্ত।** card-এর রুল "created, edited, cancelled" পর্যন্তই। approved-but-unpaid advance পরে pay করা না গেলে সেটা কখনোই পরের run-এ carry করতে পারত না — guard বসালে ওই রুলটাই ভাঙত।

**8.2b / 8.3a-র জন্য যা তৈরি:** `paid` status (শুধু এটাই netted off হবে), `settled_payslip_id` / `settled_at` কলাম। **`settled` এই story-র কোনো endpoint সেট করে না** — ওটা 8.3a-BE-র run executor-এর কাজ।

**খোলা gap:** workflow-এ reject হলে `rejected` status কেউ সেট করে না — platform-এর reject path executor বা event কিছুই fire করে না (card-এও reject flow নেই)। HR `pending_approval` advance cancel করতে পারে, তাই practical কেসটা ঢাকা। Platform-এ hook লাগলে আলাদা card।

**DoD ✅:** migration · executor + registry · ceiling unit test (৪টা কেস-ই) · frozen-basis test · দুই approval path · দুটো event listener ছাড়া · `api collection/Payroll/Salary Advances/*.bru` (repo convention `.bru` + `folder.yml`, card-এ `.yml` লেখা)। ৪৪ test green; full suite-এ baseline-এর সেই একই ৪টা pre-issue Attendance failure, নতুন regression নেই।

### ৩. 7.5-FE Salary Advances UI — ✅ done (PR #161)

**Headroom-ই স্ক্রিনের মূল মান।** Drawer-এ employee + period বাছলেই `summary` কল হয়; gross, মাসে এ পর্যন্ত নেওয়া, ceiling আর "still available" চারটাই দেখা যায়, value field `max_requestable`-এ cap করা, আর resolved টাকার অঙ্ক সবসময় বড় করে দেখানো — কারণ HR আর employee টাকায় কথা বলে যেখানে policy শতাংশে লেখা। ফলে ৫০% ceiling-এ ৬০ টাইপ করলে সেটা form-এই আটকায়, save-এর পরে 422 হয়ে ফেরে না।

**পুরোটা একটাই স্ক্রিন।** New advance, detail, record-payment, cancel-confirm — প্রতিটি shared `Offcanvas`-এ stack হয়ে খোলে, কোথাও page change হয় না। `/salary-advances/:id`-ও আলাদা page নয়: একই list render হয়, id শুধু detail panel খোলে — deep link কাজ করে, panel বন্ধ করলে URL list-এ ফেরে।

**Status-ই ঠিক করে কোন action দেখাবে:** draft → Edit + Submit · approved → Record payment · paid → "waiting for settlement" (Cancel বোতাম disabled + tooltip, কারণ টাকা চলে গেছে) · settled → payslip link। Record-payment panel নিজেই লিখে দেয় যে এটা **already-made handover-এর রেকর্ড, কোনো টাকা সরায় না**; cheque/bank-এ reference field required হয়ে যায়।

**Cancel confirm-এ shared `ConfirmDialog` ব্যবহার করা যায়নি** — ওটা Bootstrap modal (z-index 1055), আর দ্বিতীয় stacked offcanvas বসে 1065-এ, ফলে dialog যে panel-কে থামাতে চায় তারই পিছনে খুলত। ছোট একটা `ConfirmDrawer` (offcanvas) লেখা হয়েছে।

**দুটো গেট মেরামত করতে হয়েছে (দুটোই আগে থেকেই ভাঙা ছিল):**

1. Nav item-এর path ছিল `/payroll/advances` আর permission `payroll.menu-view` — card বলে `/payroll/salary-advances` ও `payroll.advance-manage`। ঠিক করা হয়েছে; পুরনো path redirect করে।
2. **`advance_enabled` কেবল Payroll Settings page ভিজিট করলে store-এ বসত** — অর্থাৎ Salary Advances nav item বাস্তবে কখনোই নিজে থেকে দেখাত না। এখন `useAdvanceEnabled()` `GET /payroll/settings` একবার পড়ে (settings page-এর সাথে একই query key, তাই বাড়তি request নেই), আর জানা না যাওয়া পর্যন্ত nav ও route অপেক্ষা করে — "জানি না"-কে "বন্ধ" ধরে নেয় না, নইলে legit deep link bounce করত।

**একটা backend স্পর্শ (card-এর API তালিকা থেকেই আসা):** `GET /payroll/settings` ছিল `payroll.settings-manage`-এ আটকানো, অথচ এই স্ক্রিনের permission `payroll.advance-manage` — ফলে HR-only role nav item-ই দেখত না। `EnsurePermission` এখন `permission:a|b` (any-of) বোঝে — backward compatible, single key আগের মতোই কাজ করে — আর GET settings দুটোর যেকোনো একটায় খোলে; **PUT আগের মতোই `settings-manage`-এ**। Test: advance-only HR settings পড়তে পারে, লিখতে পারে না।

**DoD ✅:** `modules/payroll/api/salaryAdvanceApi.ts` · nav দুই গেটে (`payroll.advance-manage` + `advance_enabled`) · money format সর্বত্র shared `formatMoney` (`payroll/utils/allocatePayment.ts`), নতুন local helper নেই · `ApprovalStatusBadge` reuse।

**জানা সীমা:** settled advance-এর payslip link `/payroll/payslips/{id}` এখন **8.2-FE** route-এ যায়। `settled` stamp এখনো **8.3a-BE** সেট করে।

---

---

## ✅ 3.2-BE — `calculateDaily`-র shift resolution ফিক্স (Siam, 10 Aug 2026)

`9.3-BE`-এর default shift বসাতে গিয়ে ধরা পড়েছে, এবং ঠিক করা হয়েছে। **Ashraful-এর card — merge-এর আগে ওর চোখ বুলিয়ে নেওয়া দরকার।**

**মূল কারণ:** [`AssignmentResolutionService`](backend/Modules/Attendance/app/Services/AssignmentResolutionService.php) `ResolvedContext::$shift`-এ **`Assignment` row** বসায় (`assignable_type='shift'`, `assignable_id=<shift id>`) — আসল `Shift` মডেলটা কখনো load করে না। কিন্তু [`AttendanceRecordService`](backend/Modules/Attendance/app/Services/AttendanceRecordService.php) ওটাকে shift ধরে নিয়ে পড়ে: `start_time` · `end_time` · `grace_minutes` · `working_hours` · `min_hours_present` · `min_hours_half_day` · `working_days`। `assignments` টেবিলে এর একটাও কলাম নেই।

**ফল:**

| যা হওয়ার কথা | যা হচ্ছে |
|---|---|
| শুক্র/শনি → `weekend` | `isWorkingDay()` দুই branch miss করে `return true` → **সব দিনই working day** |
| ছুটির দিন → `holiday` | `isHoliday()` Assignment-এ `holidays` attribute খোঁজে → **সবসময় false** |
| Shift-এর 8 / 6 / 3 ঘণ্টা | fallback default 8 / 8 / 4 → late ও half-day ভুল |
| grace 15 মিনিট | `?? 0` → grace-ই নেই |
| `shift_id` = shift-এর id | **assignment-এর id** লেখা হচ্ছে (ভুল FK) |

`isWorkingDay()`-এ আলাদা একটা bug-ও আছে (day নাম string বনাম ISO int array) — কিন্তু সেটা কখনো চলেই না, কারণ `working_days` ওখানে পৌঁছায়ই না।

**কেন জরুরি ছিল:** `9.1-BE` nightly close আর `6.1-BE` monthly approval দুটোই এই engine-এর উপরে। ভুল weekend/holiday → ভুল মাস বন্ধ → ভুল payroll। দুটোই এখন unblocked।

---

**ফিক্স:** `AttendanceRecordService` এখন assignment থেকে আসল `Shift` ও holiday `AttendancePolicy` load করে — ঠিক যেভাবে `PunchService` আগে থেকেই করত। **`ResolvedContext`-এর আকার বদলানো হয়নি**, তাই `PunchService` / resolution controller / recalculation service অক্ষত। `isWorkingDay()` ISO day number মেলায়; `isHoliday()` policy config পড়ে ও recurring holiday একই month/day-তে মেলায়।

**পাশাপাশি আরেকটা bug:** `AttendancePunchRepository` `where('attendance_date', $date)` করত, কিন্তু model তারিখটা time-সহ persist করে। MySQL DATE কলামে truncate হয় বলে prod-এ চলত, **SQLite-এ কখনো মিলত না** — এ কারণেই 3.2-BE-র calculation test কোনোদিন লেখা যায়নি। এখন `whereDate()`।

**টেস্ট:** `AttendanceDailyCalculationTest` — ৯টা কেস (weekend · working day · holiday · recurring · non-recurring · present-within-grace · late-past-grace · half-day threshold · `shift_id`)। এতে টেস্ট-ঋণের **status-ladder অংশ ঢাকা পড়েছে**; session-pairing ও visibility-scoping বাকি।

---

## Siam — Lead হিসেবে যা enforce করতে হবে

### ৫টা hard gate

| Gate | নিয়ম | কেন |
| ---- | ---- | --- |
| ~~🔴 **লাল suite**~~ ✅ | **শোধ (18 Aug)** — suite এখন **452 passed / 0 failed**। `late_minutes` 390 বনাম 30-এর কারণ timezone ছিল না, test helper punch-কে UTC-তে না লিখে local wall-clock লিখত। `phpunit.xml`-এ module suite যোগ হওয়ায় CI এখন সত্যিই ওগুলো চালায় | — |
| ~~⚠ **9.1-BE-র ঋণ**~~ ✅ | **শোধ (18 Aug)** — unassigned আসলে `attendance.day_closed` activity log-এ যায় (`?event=` দিয়ে query করা যায়), ডকের "কেবল Log::info" নোটটাই বাসি ছিল। সাথে দুটো test-ও লেখা, আর ধরা পড়ল command-টা **register-ই করা ছিল না** | **`9.4-FE` আর আটকে নেই** |
| ~~**টেস্ট-ঋণ**~~ ✅ | **শোধ (18 Aug)** — `7.4-BE`-র ৪টা (idempotency · negative net pay · আলাদা run · completion) আর `3.2-BE`-র ৮টা (session-pairing ৩ · visibility-scoping ৫)। **`8.2a-BE`-র gate খুলে গেছে** | — |
| **Merge order** | `8.2b-BE` অবশ্যই `8.3b-BE`-এর আগে merge | cards + spec-এর কঠোর নিয়ম |
| ⚠ ~~**Cross-lane**~~ | ~~`5.5-BE` merge হবে `9.1-BE`-এর পরে~~ — **উল্টো ক্রমে হয়েছে** (#185 তারপর #187), কিন্তু দুটোই এখন in, তাই ক্রমটা কার্যত মিলে গেছে। **তবে আশঙ্কাটা অপরীক্ষিতই রইল:** nightly close আর correction approve দুটোই `calculateDaily` ডাকে, একসাথে চললে কী হয় তার কোনো test নেই | দুটোই recalculation path ছোঁয় |
| ~~**নিজের ঋণ ১**~~ ✅ | ~~`7.5-BE` (Siam) merge হবে `8.2b-BE` (Ashraful)-এর আগে~~ — **PR #160-এ merged**, spine তখনো `3.2-FE`/`9.1-BE`-তে | নইলে Ashraful stub দিয়ে settlement লিখত, পরে rewrite |
| ~~**নিজের ঋণ ২**~~ ✅ | ~~`10.3-BE` merge হবে `8.3b-BE` (Ashraful)-এর আগে~~ — **Siam ফেরত নিয়ে শেষ করেছেন**; `8.3b-BE` তখনো Wave 6-এ | channel-aware readiness এটাই ব্যবহার করে |
| ~~🔴 **3.2-BE ফিক্স**~~ ✅ | `calculateDaily`-র shift resolution bug **ঠিক করা হয়েছে** (Siam, নিচের ব্রিফ) — `6.1-BE`/`9.1-BE` আর এতে আটকাবে না | weekend/holiday কখনো resolve হতো না → ভুল মাস বন্ধ → ভুল payroll |

> শেষ দুটো নিজের গেট — spine-এর বাইরে থাকলেও Siam-ই একমাত্র জন যিনি Ashraful-কে আটকাতে পারেন। `7.5-BE` Phase 1-এই শুরু করা তাই ইচ্ছাকৃত ছিল, **আর সেটাই কাজে দিয়েছে: spine এখনো `3.2-FE`/`9.1-BE`-তে, অথচ `7.5-BE` শেষ।** **`10.3-BE`-ও এখন শেষ — Ashraful-এর দিকে আর কোনো ঋণ বাকি নেই।**

### সাপ্তাহিক

- প্রতি card merge-এর পর [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md)-এর status + এক-পাতার চেকলিস্ট আপডেট।
- Card `done` মার্ক করার আগে DoD যাচাই — **10.2-BE ও 10.1-BE-র অভিজ্ঞতা বলছে "কোড আছে" ≠ "কাজ করে"**। টেস্ট চালিয়ে দেখুন, শুধু PR পড়ে নয়।
- **নতুন migration আছে এমন branch pull/merge করলেই `php artisan migrate:status`** — test suite `RefreshDatabase` দিয়ে শূন্য থেকে migrate করে, তাই **না-চালানো migration test-এ কোনোদিন ধরা পড়বে না**, শুধু আপনার dev DB-তে 500 হয়ে ফিরবে (16 Aug 2026-এর নোট দেখুন)।
- কেউ blocked হলে সেই lane-এর 🔓 ready card এগিয়ে আনুন (Munna **`8.2-FE` done**; `9.4-FE`-র ঋণ এখনো খোলা)।

### সিদ্ধান্তের পয়েন্ট

- **Lane A সবচেয়ে ভারী (54 pts)** — spine পুরোটাই serial, ভাগ করলে হাতবদলে সময় নষ্ট। Ashraful পিছিয়ে পড়লে `8.3a-BE` (5) Bablu-কে দিন। _(`3.2-FE` + `2.2-FE` দুটোই merged, তাই record screen নিয়ে হাতবদলের প্রশ্নটা আর নেই।)_
- **Siam-এর dev load এখন শূন্য** — Lane D-র সব card merged। পুরো সময় PM + review-এ; কোনো lane আটকে গেলে সেখানে হাত দেওয়া যাবে।
- **Story 2.2 complete** — `2.2-FE` merged (PR #182)। Wave 3-ও এতে বন্ধ (43/43 pts)।

---

## এক পাতার চেকলিস্ট

```
SIAM — Payroll Config & Money Inputs (lane শেষ)                0
  seeder fix ✅ done
  10.2-BE    ✅ done (৩টা bug-ই fix)
  7.4-FE     ✅ done (Bablu, PR #157)
  7.5-BE     ✅ done (PR #160) ← ৪৪ test green; 8.2b-র গেট খুলেছে
  7.5-FE     ✅ done (PR #161) ← Story 7.5 সম্পূর্ণ
  10.3-BE    ✅ done ← Lane C থেকে ফেরত; 8.3b-BE-র readiness গেট খোলা

ASHRAFUL — Record screen + BE spine (critical path)           ~26
  3.2-FE     ✅ done (PR #165) ← Bablu-র 5.4-FE এর host প্রস্তুত
  2.2-FE     ✅ done (PR #182) ← snapshot ফিল্ড সেট + recalculate control; Wave 3 বন্ধ
  9.1-BE     ✅ done (PR #187) ← ৩টে DoD-ঋণ শোধ (18 Aug), 4 test green
  6.1-BE     ✅ done (PR #192) ← দুটো gate-ই সবুজ
  6.2-BE     ✅ done (PR #195) ← freeze/unfreeze + MonthFrozen
  8.1-BE     ✅ done ⚠ ← নিজের দুটো test ফাইল চলে না
  8.0-BE     ✅ done ← attendance_snapshots + ১৭ test
  8.2a-BE    done      ← payslips + queued generate + Finaliser
  8.2b-BE    ⛔  ← 7.5-BE merged; stub লাগবে না
  8.3a-BE    ⛔  (ভারী পড়লে Bablu-কে)
  8.3b-BE    ⛔  ← 10.3-BE ✅ merged; readiness contract তৈরি

MUNNA — Leave, Jobs & Payslip UI                              ~8
  5.2-BE     ✅ done (Munna draft + Siam fix)
  5.3a-BE    ✅ done (PR #180)
  5.2-FE     ✅ done (Siam)
  5.3b-BE    ✅ done (5.3a-র PR-এ, DoD #186-এ)
  5.3b ঋণ    ✅ done (PR #186) ← transaction · duplicate event · 7 test green
  5.3-FE     ✅ done    (PR #188) ← ✅ build-ঋণ শোধ: tsc -b সবুজ + Refresh button render হয়
  build fix  ✅ done ← tsc -b সবুজ
  9.2-BE     ✅ done (PR #190) ← command + chunk job + endpoint
  9.3-BE     ✅ done (Siam)
  9.4-FE     ✅ done ⚠ (PR #197) ← jobs screen live, ৪ ঋণ বাকি
  9.4 ঋণ     ▶ এখন  ← Fix-link · timezone · log filter · dead file
  8.2-FE     ✅

BABLU — Corrections, Records & Run UI                         ~0
  7.4-FE     ✅ done (PR #157)
  5.4-BE     ✅ done (PR #163)
  10.3-BE    ✅ done (Siam ফেরত নিয়েছেন)
  5.4-FE     ✅ done (PR #181)
  5.5-BE     ✅ done ⚠ (PR #185) ← gate ভেঙে merged, 9.1-BE-র আগে
  5.5-FE     ✅ done (PR #189)
  6.1-FE     ✅ done ⚠ (PR #198) ← build লাল + ১টা test লাল, ৬ ঋণ
  #198-এর ঋণ ▶ এখন  ← tsc ১৫ error · MonthlyAttendanceApprovalServiceTest
  6.2-FE     ✅ done
  8.1-FE     ✅ done ⚠
  8.0-FE     ✅ done
  8.3a-FE    ✅
  8.3b-FE    ✅
```

**চিহ্ন:** ▶ চলছে · 🔓 deps clear, শুরু করা যায় · ⛔ blocked · 🔴 জরুরি

---

## ফ্রন্টএন্ড শেয়ার্ড ইনফ্রা নোট (11 Aug 2026)

**Lane বা কার্ড বণ্টন এতে বদলায় না।** `new-design` branch-এ Qbits design migration হয়েছে — login · layout shell · dashboard · employee Tailwind-এ; **attendance · payroll · configuration · platform · onboarding-এর একটি ফাইলও ছোঁয়া হয়নি**।

FE lane-এ যে কেউ কাজ করার আগে যা জানা দরকার:

- **Bootstrap-side কার্ডে কোনো পরিবর্তন লাগবে না** — আগের মতোই `shared/components/common/*` ও Bootstrap class ব্যবহার করুন।
- **নতুন কোনো স্ক্রিন Tailwind-এ লিখলে** class-এ `tw:` prefix লাগবে এবং `shared/components/ui/*` ব্যবহার করতে হবে; নাহলে `npm run build`-এর boundary gate আটকাবে।
- **একই ফাইলে দুই system মেশানো যাবে না** — ফাইলটা হয় পুরো Bootstrap, নয় পুরো Tailwind।
- **Font অ্যাপজুড়ে Inter** — সব স্ক্রিনের টাইপ বদলেছে, markup নয়।

বিস্তারিত: [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md) — "ফ্রন্টএন্ড শেয়ার্ড ইনফ্রা" অংশ।

---

## ফ্রন্টএন্ড শেয়ার্ড ইনফ্রা নোট (12 Aug 2026) — overlay stacking

**Lane বা কার্ড বণ্টন এতেও বদলায় না।** কিন্তু `shared/components/ui`-র **Modal ও Drawer** দুটোরই আচরণ বদলেছে, কারণ employee profile এখন drawer-এ খোলে আর তার ভেতরের tab-গুলো নিজেরাই drawer/modal খোলে।

Tailwind-side কার্ডে কাজ করার আগে যা জানা দরকার:

- **Nested overlay এখন নিজে থেকেই ঠিক কাজ করে** — শেষে যেটা খুলেছে সেটাই Escape ও backdrop click নেয়, নিচেরটা নয়। আলাদা কোনো prop লাগে না।
- **`ui/Drawer`-এ `size="xl"` আর `headerActions`** যোগ হয়েছে; বাকি prop আগের মতোই, বিদ্যমান call site বদলাতে হয়নি।
- **Bootstrap-side কার্ডে কোনো পরিবর্তন নেই** — `common/Offcanvas` ও তার নিজের stack অপরিবর্তিত।

বিস্তারিত: [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md) — "ফ্রন্টএন্ড শেয়ার্ড ইনফ্রা — overlay stacking" অংশ।

**`main` merge (12 Aug 2026):** PR #170–#174-এর employee tab fix-গুলো `new-design`-এ আনা হয়েছে — markup Tailwind-এর, behaviour `main`-এর, কোনো fix বাদ যায়নি। কোন ফাইলে কী হয়েছে: [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md) — "`main` merge into `new-design`" অংশ।

**Attendance new-design শুরু (12 Aug 2026):** `attendence-new-design` branch-এ Records slice (৬ ফাইল) শেষ। নতুন `ui/Dropdown`, `ui/Button`-এ `ref`, আর attendance icon-এর `bi-*` → lucide mapping যোগ হয়েছে। **Toast আর drawer-এর পেছনে লুকায় না** — z-index ১০৯০ → ১৫০০ + body-তে portal; employee module-এর "add/edit/delete-এ কোনো alert আসে না" সমস্যা এটাই ছিল। বিস্তারিত: [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md) — "Attendance module → new design" অংশ।

**Attendance new-design সম্পূর্ণ (12 Aug 2026):** ৪৭/৪৭ ফাইল Tailwind-এ, `src/modules/attendance` এখন build-এর style-boundary gate-এর ভিতরে। Toast-ও Tailwind — সব module এখন একই notification দেখবে। FE lane-এ attendance card নিলে `shared/components/ui/*` ব্যবহার করতে হবে, `common/*` নয়; নাহলে `npm run build` আটকাবে। বিস্তারিত: [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md)।

---

## ডেভ এনভায়রনমেন্ট নোট (12 Aug 2026)

**Lane বা কার্ড বণ্টন এতে বদলায় না**, কিন্তু stack চালানোর নিয়ম বদলেছে। দুটোই একবার করে সময় খেয়েছে, তাই আগে জেনে নিন:

- **সবসময় `docker compose --env-file backend/.env up -d`** — compose নিজে থেকে শুধু project root-এর `.env` পড়ে, `backend/.env` নয়। flag ছাড়া চালালে frontend 3010, mysql 3310, redis 6380-এ ওঠে (compose-এর default), `.env`-এর 3000/3306/6379-এ নয়। **এতে একবার CORS ভেঙেছিল** — frontend যে port-এ উঠেছিল সেটা `CORS_ALLOWED_ORIGINS`-এ ছিল না।
- **Browser CORS error দিলেই backend-কে দোষ দেবেন না।** IDE-র port forward `127.0.0.1:8010`-এ bind করে, Docker করে `*:8010`-এ; OS বেশি specific-টাকে আগে দেয়, তাই `localhost:8010` তখন Docker-এ পৌঁছায়ই না। `lsof -nP -iTCP:8010 -sTCP:LISTEN`-এ একাধিক listener দেখলে IDE-র Ports panel থেকে forward সরান।
- **`REDIS_PORT` 6379 ছাড়া অন্য কিছু দেবেন না** — compose ওটা host publish-এ ব্যবহার করে, Laravel-ও ওটা দিয়েই connect করে, আর Redis-এর কোনো per-service override নেই।
- **`.env` আর `.env.example`-এর key-set এখন এক** — নতুন key যোগ করলে দুটোতেই দিন, secret শুধু `.env`-এ।

বিস্তারিত: [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md) — "ডেভ এনভায়রনমেন্ট — `backend/.env` ↔ `docker-compose.yml`" অংশ।

**Payroll new-design সম্পূর্ণ (13 Aug 2026):** ৩০/৩০ ফাইল Tailwind-এ, `src/modules/payroll` এখন build-এর style-boundary gate-এর ভিতরে (মোট ৮টা ডিরেক্টরি)। Payroll configuration-এর চারটা modal এখন drawer, আর `ConfirmDrawer` workaround মুছে গেছে। **Employee, Attendance, Payroll — তিনটাই migrated**; বাকি configuration · platform · onboarding। ঐ তিনটার কার্ডে `common/*` ব্যবহার আগের মতোই চলবে, কিন্তু migrated module-এ কাজ করলে `shared/components/ui/*` লাগবে, নাহলে `npm run build` আটকাবে।

**Configuration new-design সম্পূর্ণ (13 Aug 2026):** ২১/২১ ফাইল Tailwind-এ, `src/modules/configuration` এখন style-boundary gate-এর ভিতরে (মোট ৯টা ডিরেক্টরি)। System Configuration-এর **নয়টা ফর্মই এখন drawer** (employment type · designation · document type · division · department · team · branch · grade · ID card), আর কোম্পানি tab-এর inline ফর্মটাও drawer-এ গেছে; `ArchiveDialog` ইচ্ছাকৃতভাবে modal-ই থাকল কারণ ওটা drawer-এর ভিতর থেকে উঠে একটা প্রশ্ন করে বাধা দেয়। **Employee · Attendance · Payroll · Configuration — চারটাই migrated**; বাকি শুধু platform (১৯ ফাইল) আর onboarding (২)। এই দুটোর কার্ডে `common/*` আগের মতোই চলবে; migrated module ছুঁলে `shared/components/ui/*` লাগবে, নাহলে `npm run build` আটকাবে।

তিনটা পুরনো tsc error এই কাজে root cause-সহ ঠিক হয়েছে — তার মধ্যে একটা আসল bug ছিল: `EmployeeGradeForm` কখনো `grade_code` মানটা নিত না, যদিও API, list column আর submit handler তিনটাই ওটা support করত। বিস্তারিত: [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md) — "Configuration module → new design" অংশ।

**Platform › Module Registry new design + drawer (13 Aug 2026):** platform module-এর প্রথম screen Tailwind-এ। `ModuleForm.tsx` page মুছে drawer হয়েছে (`?mode=create` / `?mode=edit&module=<slug>`), পুরনো দুটো URL redirect হয়ে বেঁচে আছে, আর "Add Module"/"Edit" এখন `platform.create` / `platform.update` দিয়ে UI-তেই gate করা। **বাকি ~১৮টা platform page এখনো Bootstrap-side** — ঐ কার্ডগুলোতে `common/*` আগের মতোই চলবে। style-boundary gate এখন individual ফাইলও নিতে পারে, তাই আংশিক migrated module-এ কাজ করার সময় নিজের migrated ফাইলটা তালিকায় যোগ করে দিলে সেটা আর regress করবে না। বিস্তারিত: [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md) — "Platform › Module Registry" অংশ।

**Platform › Roles new design + drawer (13 Aug 2026):** roles list · role form · permission matrix — তিনটাই Tailwind-এ। `RoleForm.tsx` page মুছে drawer (`?mode=create` / `?mode=edit&role=<uuid>`), পুরনো দুটো URL redirect হয়ে বেঁচে আছে, "Add Role" → `platform.create` আর "Edit"/"Permissions" → `platform.update` দিয়ে UI-তে gate করা। **Permission matrix-ও drawer** (`?mode=permissions&role=<uuid>`) — `PermissionMatrixPage.tsx` মুছে গেছে, পুরনো URL redirect হয়; ভিতরের এক-সারির table সরিয়ে wrap করা checkbox grid হয়েছে, তাই আর পাশে scroll করে না। Row-এর action তিনটা menu-তে না রেখে সারিতেই আছে। **তিনটা drawer-এই একটা bug ধরা পড়ে ঠিক হয়েছে:** reset effect শুধু query data-র উপর নির্ভর করত, কিন্তু react-query reopen-এ একই object reference দেয় — তাই Cancel করা পরিবর্তন পরের বার খোলার সময় বসে থাকত; dependency-তে `show` যোগ করা হয়েছে। platform-এর ৭টা ফাইল এখন gate-এর ভিতরে, **বাকি ~১৫টা page এখনো Bootstrap-side**। বিস্তারিত: [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md) — "Platform › Roles" অংশ।

**Roles row-এর action (13 Aug 2026):** নরম গোল border-এর একটা pill-এর ভিতরে তিনটা রঙিন গোল icon, ~৩০% overlap করা; hover করলে ছড়িয়ে যায়। `shared/components/ui/buttonStyles.ts` **ছোঁয়া হয়নি** — রং আর overlap দুটোই `RoleList`-এর নিজের `ROW_ACTION_BASE`/`ROW_ACTION_TONE`-এ, `ghost` variant-এর উপরে। platform-এর action checkbox দুই জায়গায় এক চেহারায় এসেছে (`components/ActionCheckboxCard.tsx`) — label-এর নিচে monospace-এ permission key, টিক দিলে brand tint।

**Platform › Users new design + drawer (13 Aug 2026):** create user · roles · permission overrides — তিনটাই page থেকে drawer (`?mode=create` / `?mode=roles&user=<uuid>` / `?mode=overrides&user=<uuid>`), পুরনো route তিনটা redirect হয়ে বেঁচে আছে। **Override screen-টা লক্ষ করার মতো:** ওটা পুরো permission catalogue দেখায় না — **কেবল assigned role-গুলো যা দেয় তাই**, সবগুলো ticked। Tick তুলে নিলে `deny` override হয়, হাত না দিলে permission role-কে অনুসরণ করতেই থাকে। `allow` override এই screen থেকে বানানো যায় না (বাড়তি কিছু দেওয়া role-এর কাজ), তবে **থাকা `allow` row save-এ অক্ষত থাকে**। নতুন `ui/Tooltip` icon-only action-এর জন্য — বাঁ পাশে বসে, কারণ DataTable-এর অনুভূমিক scroll উপরে/নিচে বসানো bubble কেটে দিত। **Backend-এ একটা `GET /users/{uuid}/effective-permissions` দরকার** — এখন baseline-টা প্রতিটা role-এর permission আলাদা করে এনে জোড়া হয়। platform-এর ১১টা ফাইল gate-এর ভিতরে, **বাকি ~১১টা page (actions · approvals · activity logs · OTP) এখনো Bootstrap-side**। বিস্তারিত: [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md) — "Platform › Users" অংশ।

**Platform › Approval Workflows new design + drawer (13 Aug 2026):** list · form · step builder তিনটাই Tailwind-এ, form এখন drawer (`?mode=create` / `?mode=edit&workflow=<uuid>`), পুরনো দুটো route redirect হয়। Row action সেই overlap-করা icon cluster + tooltip; step-এর move/remove-ও icon button + tooltip। **Edit-এ module/action এখন read-only** — update payload-এ ওগুলো যায়ই না, তাই slug ধরে module খোঁজার কাজটা বাদ গেছে, আর তাতে `ApprovalWorkflowForm`-এর পুরনো lint warning-ও মূল থেকে সেরেছে (`oxlint src/modules/platform` এখন ০ warning)। `custom` resolver-এর `<select multiple>` সরিয়ে checkbox তালিকা। platform-এর ১৪টা ফাইল gate-এর ভিতরে; **বাকি Approval Settings · Approval Requests · Actions · Activity logs · OTP**। বিস্তারিত: [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md) — "Platform › Approval Workflows" অংশ।

**Platform › Approval Settings + Requests new design (13 Aug 2026):** Approvals menu-র তিনটাই শেষ। Request detail page মুছে drawer (`?request=<uuid>`), পুরনো route redirect হয়। **`ApprovalStatusBadge` এখন Tailwind** (`ui/StatusBadge`-এর মোড়ক) — ওটা `modules/core/components`-এ gate-এর বাইরে থেকে attendance · payroll · platform, অর্থাৎ **সব migrated module-এ Bootstrap badge মার্কআপ ঢোকাচ্ছিল**, আর call site-এ কেবল `<ApprovalStatusBadge>` লেখা বলে gate ধরতে পারত না; ওটা আর `PendingApprovalAction` এখন gate-এ তালিকাভুক্ত। platform-এর ১৭টা ফাইল gate-এর ভিতরে (৩০ path); **বাকি Actions · Activity logs · OTP**। `oxlint src/modules/platform` ০ warning; `src/modules/core`-এ ৪টা warning আছে, চারটাই আগে থেকেই ছিল (stash করে যাচাই করা)। বিস্তারিত: [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md) — "Platform › Approval Settings + Requests" অংশ।

**Activity Log + Onboarding Policies new design (13 Aug 2026):** Activity log-এর detail Modal → drawer (তবে **URL-driven নয়** — একটামাত্র log আনার endpoint নেই, তাই share করা লিংক কাজ করত না; ফাইলে কারণ লেখা), হাতে লেখা pagination সরিয়ে `DataTable`-এর `pagination` prop। Onboarding policy form page → drawer (`?mode=create` / `?mode=edit&policy=<id>`), পুরনো route redirect হয়। **দুটো আসল ফাঁক বন্ধ হয়েছে:** policy delete-এ কোনো confirmation ছিল না (একমাত্র list যেখানে ছিল না), আর step-এর চারটে flag (`is_required` · `is_blocking` · `allow_skip` · `is_active`) payload-এ যেত কিন্তু ফর্মে field ছিল না — তৈরির পর আর বদলানো যেত না। Saved step এখন মোছার বদলে নিষ্ক্রিয় করা যায়, কারণ API-তে step delete endpoint নেই। Gate ৩৪ path; **platform-এ বাকি কেবল Actions (list + form) আর OTP log** — ঐ তিনটে হলে পুরো module directory হিসেবে gate-এ বসবে। বিস্তারিত: [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md) — "Activity Log + Onboarding Policies" অংশ।

**Qbits migration সম্পূর্ণ (13 Aug 2026):** **পুরো frontend Tailwind-এ।** শেষ ধাপে platform-এর বাকি তিনটে (Actions list + create drawer, OTP log) আর onboarding-এর চারটে step page + দুটো route guard + warning banner। Onboarding-এর পাতাগুলো এখন login screen-এর একই lockup-এ (`OnboardingCard`), তাই sign-in থেকে onboarding-এ গেলে product বদলে গেছে মনে হয় না; `AppBrand.tsx` মুছে গেছে, `BrandMark` ওর কাজ করে। style-boundary gate এখন **দশটা গোটা ডিরেক্টরি** পাহারা দেয়, ফাইল ধরে ধরে নয় — মানে যেকোনো module-এ কাজ করলেই `shared/components/ui/*` লাগবে, নাহলে `npm run build` আটকাবে।

**`shared/components/common/*`-এর বারোটা ফাইল এখন সম্পূর্ণ অব্যবহৃত** (`rg "components/common/"` ফাঁকা) — পুরো Bootstrap-side component library মৃত কোড। **মোছা হয়নি**, কারণ cleanup আলাদা কাজ আর সেটা শুরু করার নির্দেশ আসেনি; শুধু রেকর্ড করা হলো যে এখন নিরাপদে মোছা যায়। বিস্তারিত: [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md) — "Qbits migration: COMPLETE" অংশ।

**style-boundary gate কোনোদিন চলেনি — ঠিক করা হয়েছে (13 Aug 2026):** `frontend/scripts/check-style-boundary.sh` `rg` (ripgrep) ডাকত, কিন্তু **frontend container-এ `rg` নেই** — যেখানে `npm run build` ওটা চালায়। প্রতিটা call ব্যর্থ হয়ে `2>/dev/null`-এ চাপা পড়ত, তাই চারটে rule-ই সবসময় pass করত আর স্ক্রিপ্ট `✓ style boundary clean` ছাপত। Path সংখ্যাটা আসল ছিল বলে (migration-এর সঙ্গে বাড়ত) ভাঙা অবস্থাটা বিশ্বাসযোগ্য দেখাত। **অর্থাৎ এই migration জুড়ে "gate clean" বলে যা রিপোর্ট করা হয়েছে, তার কোনোটাই প্রমাণ ছিল না।**

হাতে চারটে rule চালিয়ে দেখা গেছে **ক্ষতি প্রায় শূন্য** — Bootstrap class ০, hard-coded z-index ০, `bi bi-`-এর দুটো হিটই doc comment, আর একটামাত্র আসল bug: `last:tw:border-b-0` (Tailwind v4-এ prefix বাইরে বসে, নইলে CSS emit হয় না) দুটো attendance ফাইলে — সেটা fix করা হয়েছে, আর **নতুন gate ওটা তার প্রথম run-এই ধরেছে**।

স্ক্রিপ্টটা এখন POSIX `find` + `grep -nHE` ব্যবহার করে (`rg` নয়), tool না পেলে `require()` দিয়ে `exit 1`, ফাইল তালিকা ফাঁকা হলে clean বলতে অস্বীকার করে, আর summary-তে ফাইল সংখ্যাও ছাপে (`10 path(s), 303 file(s) checked`) যাতে শূন্য-scan চোখে পড়ে। Rule 2 এখন `className=` দিয়ে anchored, তাই comment-এর `bi bi-` আর false positive দেয় না।

**নতুন build check লেখার নিয়ম:** সবুজ বিশ্বাস করার আগে ইচ্ছে করে একটা violation ঢুকিয়ে লাল হতে দেখতে হবে। যা ব্যর্থ হলে সবুজ দেখায়, সেটা গেট নয়।

**Bootstrap removal-এর plan তৈরি, কাজ শুরু হয়নি।** পুরো frontend Tailwind-এ, `shared/components/common/*`-এর বারোটা ফাইল অব্যবহৃত, তাই Bootstrap এখন কেবল খরচ (~৪৪০ KB CSS, ৩১৪ KB icon font যার একটা glyph-ও render হয় না, ৮০ KB bootstrap JS যার bind করার মতো `data-bs-*` নেই)। **আসল blocker চারটে, সবই stylesheet/build স্তরে:** (১) Bootstrap Reboot এখন Tailwind-এর base reset — `tailwind.css` preflight import করে না; (২) `sneat/_bootstrap.scss` ৩১টা `bootstrap/scss/*` partial টানে আর `vite.config.ts`-এর `loadPaths` ওগুলো resolve করায়; (৩) `check-bootstrap.sh` build **আর dev container দুটোই** আটকাবে (`docker-entrypoint.dev.sh:8`-এ `|| true` নেই); (৪) `tw:` prefix · unlayered utilities · z-index scale তিনটেই coexistence-এর জন্য। DB-তে থাকা `bi-*` নামগুলো render-time-এ lucide-তে map হয়, **কোনো data migration লাগবে না**।

**Bootstrap + Sneat সরানো হয়েছে (13 Aug 2026):** frontend এখন **কেবল Tailwind**, branch `remove-bootstrap`। CSS bundle **444 KB → 51 KB**, JS −90 KB, icon font −314 KB। `bootstrap` · `bootstrap-icons` · `@popperjs/core` · `sass` dependency, `src/styles/sneat/` (৮৭ ফাইল), `shared/components/common/` (১২ ফাইল), SCSS pipeline আর `check-bootstrap.sh` — সব গেছে।

**যা সবার জানা দরকার:**
- **`tw:` prefix আর নেই** — ১৯০ ফাইলে ৭,০৬২ জায়গা থেকে সরানো। এখন `className="flex gap-2"`, `className="tw:flex tw:gap-2"` নয়। পুরনো branch merge করলে এখানেই conflict হবে।
- **`useLayoutHtmlClass` মুছে গেছে** — নতুন page-এ ওটা ডাকবেন না।
- **`shared/components/common/*` নেই** — সব import `shared/components/ui/*` থেকে।
- **z-index token ছোট হয়েছে** (1200–1600 → 100–600)। Hard-coded z-index এখনো build আটকাবে।
- **Reboot → preflight** — element default বদলেছে (margin · list bullet · `<hr>` · button)। **Manual pass বাকি**, frontend-এ test suite নেই।

`docs/FRONTEND_TEMPLATE.md` superseded মার্ক করা হয়েছে — ওটা Sneat integration guide, আর অনুসরণ করার মতো নয়।

---

## `POST /leave-requests` → 500, dev DB-তে migration চলেনি (16 Aug 2026)

**Lane বা কার্ড বণ্টন বদলায় না — `5.3a-BE`/`5.3b-BE` ঠিকই আছে, কোনো code bug নেই।** dev database-টা পিছিয়ে ছিল: `2026_08_12_150000_create_leave_request_days_table` (5.3a-র সাথে আসা migration) কখনো চালানো হয়নি, তাই `leave_request_days` টেবিলটাই ছিল না। Leave submit করলে প্রতিবার `{"success": false, "message": "Failed to submit leave request.", "errors": {}}` ফিরত। দুটো pending migration চালিয়ে দেওয়া হয়েছে, এখন কাজ করে।

**যে তিনটে জিনিস সবার জানা দরকার:**

- **`migrate:status` না দেখে debug শুরু করবেন না।** test suite প্রতিবার শূন্য থেকে migrate করে, তাই এই শ্রেণির bug **৩৬টা test সবুজ রেখেই** production-সদৃশ DB-তে ভাঙে। schema ছোঁয়া branch merge করার পর এটাই একমাত্র check যেটা ধরে।
- **ফাঁকা `errors` + 500 মানে অচেনা exception, business rule violation নয়।** rule ভাঙলে `ApiResponseTrait::handleException()` 422/409 দিত বার্তা সহ; `QueryException` ওর কোনো শাখায় পড়ে না, তাই fallback message + ফাঁকা `errors` + 500। ও দুটো দেখলে response নয়, **`storage/logs/laravel.log`-এ** যেতে হবে — এবারে log-টা খালি করে ফেলা হয়েছিল বলেই trace হারিয়ে গিয়েছিল (logging নিজে ঠিক আছে, probe করে দেখা হয়েছে)।
- **Approval off থাকলে executor submit-এর ভিতরেই চলে।** company-তে approval enabled না থাকলে `ApprovalGateway` bypass নেয় আর `LeaveRequestExecutor` **inline** চলে — তাই approval path-এর যেকোনো ভাঙা জিনিস submit-এ এসে পড়ে। approval on থাকলে এই বাগটা লুকিয়ে থাকত। নতুন approval-চালিত feature test করার সময় **দুটো mode-ই** দেখুন।

**data নষ্ট হয়নি** — `store()` পুরোটা `DB::transaction()`-এ, প্রতিবার সম্পূর্ণ rollback হয়েছে; `leave_requests` ফাঁকা, ledger ০ সারি, কোনো balance কাটা যায়নি।

বিস্তারিত: [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md) — "`POST /leave-requests` → 500" অংশ।

---

## `5.5-BE` merged — cross-lane gate ভেঙে (16 Aug 2026)

**PR #185** (`att-5-5-be-correction-approval`, Bablu) main-এ আছে; doc-এ card-টা `▶ এখন` লেখা ছিল, এখন `done` করা হয়েছে। **কাজটা সত্যিই শেষ** — কার্ডের তিনটে DoD item-ই ঢাকা, `php artisan test Modules/Attendance/tests/Feature/AttendanceCorrectionExecutorTest.php` → **15 passed (64 assertions)**।

**⚠ কিন্তু cross-lane নিয়মটা ভাঙা হয়েছে।** নিয়ম ছিল `5.5-BE` merge হবে `9.1-BE`-এর **পরে**, কারণ দুটোই recalculation path ছোঁয়। `9.1-BE` **এখনো লেখাই হয়নি** — Attendance module-এ কোনো console command নেই, `AttendanceServiceProvider::configureSchedules()` এখনো commented stub, `routes/console.php`-এ কেবল তিনটে approval job।

**ক্ষতি সীমিত, কিন্তু দায়টা হাতবদল হয়েছে:** `5.5-BE` `calculateDaily`-র ভিতরটা বদলায়নি, শুধু superseded punch বাদ দিয়ে ওটাকে **ডাকে**। তাই conflict নয়, **semantic overlap** — nightly close যখন গোটা দিনের সব employee-র উপর `calculateDaily` চালাবে, সদ্য-approved correction-এর সাথে সময়ের দৌড় নিয়ে ভাবতে হবে।

- **Ashraful:** `9.1-BE` শুরুর আগে `AttendanceCorrectionExecutor::apply()` একবার পড়ুন — punch supersede করে recalculate ডাকার contract-টা nightly job-কেও মানতে হবে।
- **Bablu:** পরের card **`5.5-FE`** Correction Approval UI (3 pts) — unblocked। BE-তে `index`/`show` এখন `correction-create|correction-approve` দুটোতেই খোলে, তাই HR queue আর 403 খাবে না (আগে খেত)।

**শিক্ষা:** gate তখনই কাজ করে যখন merge-এর সময় কেউ তাকায়। এটা doc-এ লেখা ছিল, তবু ভাঙা গেছে — কারণ PR-এ কোনো check ছিল না। ordering constraint হয় PR template-এর checklist-এ তুলুন, নয় স্বীকার করুন যে ওটা advisory।

---

## `5.3b`-র ঋণ শোধ (PR #186) — এবং 🔴 ২৫টা লাল test (16 Aug 2026)

**PR #186** (`att-5-3b-be-punch-voids-leave`, Munna) merged। **তিনটে DoD-ঋণই মিটেছে** — `calculateDaily` এখন পুরোটা `DB::transaction`-এ, `LeaveDayVoided` কেবল resolver-এ একবার fire হয়, আর `PunchLeaveVoidTest`-এ **7 test green (32 assertion)** DoD-র চারটে case ঢাকে। Munna-র পরের card **`5.3-FE`**।

সুন্দর একটা সিদ্ধান্ত ওখানে আছে: `calculateDaily` transactional হওয়ায় ওর ভিতর থেকে `voidLeaveDay()` ডাকলে recursion হতো; সমাধান এসেছে `voidLeaveDay()`-এ **default-`true` তৃতীয় parameter** দিয়ে, তাই বাইরের কোনো caller-এর signature ভাঙেনি।

### 🔴 কিন্তু গোটা suite চালিয়ে দেখা গেল ২৫টা test লাল

| Suite | ফল |
|---|---|
| `AttendanceCorrectionExecutorTest` · `PunchLeaveVoidTest` · তিনটে leave suite | ✅ |
| `PunchServiceTest` ❌12 · `PunchApiTest` ❌7 · `LeaveBalanceServiceTest` ❌3 · `AttendanceDailyCalculationTest` ❌2 · `AssignmentResolutionServiceTest` ❌1 | **মোট ২৫** |

**এই PR-এর দোষ নয় — যাচাই করা হয়েছে।** `AttendanceRecordService.php` সাময়িকভাবে #186-এর আগের version-এ ফিরিয়ে চালানো হয়েছে, **একই test তখনও লাল** (ফাইল সঙ্গে সঙ্গে restore করা হয়েছে)। #186 `PunchService`/`LeaveBalanceService`/`AssignmentResolutionService`-এর কোনোটাই ছোঁয়নি। **ফাটলটা কয়েকটা PR ধরে জমেছে, কেউ গোটা suite চালায়নি।**

**দুটো আলাদা কারণ:**

- **Timezone অমিল** (২টা) — test `punch_time` বসায় local string হিসেবে, `calculateDaily` shift-এর সময় UTC-তে নেয়। `late_minutes` আসে **390**, প্রত্যাশিত 30 — ব্যবধান হুবহু ৩৬০ মিনিট, Asia/Dhaka-র UTC+6। **এটা কেবল test-এর দোষ কিনা নিশ্চিত নয়** — punch UTC-তে জমা হওয়ার কথা, তাই production path-ও একই ভুল করতে পারে। **সিদ্ধান্ত দরকার।**
- **বাসি test কোড** (২৩টা) — `LeaveBalanceServiceTest` service-কে ১টা ctor argument দেয়, লাগে ২টা। Punch-এর ১৯টা `No active shift found` — assignment বসে `effective_date => now()`, তাই আগের দিনে পড়া punch-এর জন্য shift মেলে না (**তারিখ-নির্ভর ভঙ্গুর test**)।

**নতুন hard gate যোগ করা হয়েছে** (উপরের টেবিলে): `6.1-BE`/`9.1-BE` শুরুর আগে এগুলো সবুজ করতে হবে। দুটোই `calculateDaily`-র উপরে বসে — ভুল late → ভুল status → ভুল মাস বন্ধ → **ভুল payroll**।

**শিক্ষা:** "আমার test সবুজ" আর "suite সবুজ" এক জিনিস নয়। প্রতিটা PR সৎভাবেই নিজের ফাইলের সবুজ রিপোর্ট করেছে, অথচ পাশের suite ততক্ষণে লাল। Card `done` মার্ক করার আগে অন্তত **module-এর গোটা suite** একবার চালান।

---

## `9.1-BE` Nightly Attendance Close merged — PR #187 (16 Aug 2026)

`att-9-1-be-nightly-close` (Ashraful) main-এ। **Part J (Automation)-এর প্রথম card** — এতদিন Attendance module-এ একটাও console command বা scheduled job ছিল না। **Ashraful-এর পরের card `6.1-BE`।**

**নকশার দুটো ভালো সিদ্ধান্ত:**

- **এক cron, বহু timezone।** `ScheduleCloseDayCommand` hourly চলে আর প্রতিটা active company-র জন্য দেখে ওর নিজের timezone-এ `hour === 2` কিনা। প্রতি company-র আলাদা cron লাগে না।
- **Idempotency query-তেই।** `chunkActiveWithoutAttendance()` কেবল সেই employee তোলে যাদের ঐ তারিখে record **নেই** (`whereDoesntHave`)। দুবার চললে দ্বিতীয়বার কাউকে পায় না — কোনো flag বা lock লাগেনি।

**⚠ তিনটে DoD-ঋণ — তিনটেই বাকি:**

| DoD | অবস্থা |
|---|---|
| দুই ভিন্ন timezone-এর দুই tenant নিয়ে scheduler test | ❌ **PR-এ একটাও test ফাইল নেই** |
| Job দুবার চালিয়ে identical state assert | ❌ নকশা ঠিক মনে হয়, **অপ্রমাণিত** |
| Unassigned employee HR-এর পড়ার report-এ | ❌ কেবল `Log::info`-তে — card বলেছিল "not only in logs" |

তৃতীয়টা নিছক ঋণ নয় — **`9.4-FE`-র একটা গোটা screen ওটার উপর দাঁড়ায়**, আর পড়ার মতো উৎস এখনো নেই। নতুন gate হিসেবে উপরে যোগ করা হয়েছে।

**দুটো ঝুঁকি নজরে রাখুন:**

- **মিস করলে catch-up নেই।** Command শুধু দেখে "এখন কি 2টা"। ঐ ঘণ্টার run মিস হলে (queue বন্ধ, deploy, worker restart) ঐ দিনটা **নিঃশব্দে বন্ধ হবে না**, পরে আর ফিরে দেখে না। DST spring-forward-এ 2টা ঘণ্টাটাই থাকে না — একই ফল।
- **লাল ভিতের উপরে বসেছে।** 🔴 gate বলেছিল `9.1-BE`-র আগে ২৫টা লাল test সবুজ করতে; **মানা হয়নি**। Nightly close সরাসরি `calculateDaily` ডাকে — ঠিক যে function-এর timezone আচরণ নিয়ে সন্দেহ (`late_minutes` 390 বনাম 30)। Suite #187-এর পরেও ২৫ লালেই আছে।

---

## Employee contact tab-এ user-এর email/mobile default (Siam, 16 Aug 2026)

User create করার সময় যে `email` আর `mobile` দেওয়া হয়, সেগুলো Employee profile-এর **Contact info** tab-এ আর হাতে টাইপ করতে হবে না — contact record না থাকলে ঐ দুটো field আগে থেকেই ভরা আসে।

- Backend: `EmployeePersonalInfoResource`-এর `user` block-এ `mobile` যোগ — additive, পুরনো consumer ভাঙে না।
- Frontend: `ContactInfoTab`-এ contact না থাকলে `official_email`/`mobile_no` linked user থেকে seed হয়; **ব্যবহারকারী কিছু টাইপ করে ফেললে সেটা overwrite হয় না**, আর contact একবার save হলে সব সময় DB-র মানই দেখায়।

Prefill-ই শুধু, backend-এ force করা হয়নি — HR চাইলে official email আলাদা দিতে পারে, uniqueness check আগের মতোই।

---

## Emergency phone ≠ employee-এর নিজের নম্বর (Siam, 16 Aug 2026)

Emergency contact phone এখন **mobile_no এবং alternative_mobile_no** — দুটোর কোনোটার সাথেই মিলতে পারবে না। আগে শুধু `mobile_no`-র সাথে মিল ঠেকানো হতো।

যুক্তিটা সহজ: emergency contact-এর কাজই হলো employee-কে না পেলে অন্য কাউকে পাওয়া। ওর নম্বর যদি employee-রই আরেকটা নম্বর হয়, তাহলে জিনিসটার কোনো মানে থাকে না।

- নিয়মটা `EmployeeContactService`-এ একটাই জায়গায় (`assertEmergencyPhoneIsIndependent`) — create আর update দুটোই ওটাই ডাকে, তাই দুই পথে আচরণ আলাদা হওয়ার সুযোগ নেই।
- **যেকোনো দিক থেকে ধরা পড়ে** — emergency phone বদলে alternative-এর সমান করলেও 422, alternative বদলে emergency-র সমান করলেও 422। Update-এ payload-এ না-থাকা field-এর জন্য DB-র মান ধরা হয়।
- পুরনো `employee.contacts.emergency_phone_must_differ` config flag-ই দুটো check নিয়ন্ত্রণ করে; নতুন কোনো flag যোগ করা হয়নি।
- Frontend-এ একই নিয়ম mirror করা — save চাপার আগেই field-error দেখায়, server round-trip লাগে না।

**Test:** `tests/Feature/EmployeeContactValidationTest.php` (৫টা, নতুন ফাইল) — Employee module-এ contact-এর এটাই প্রথম coverage।

---

## Address tab-এ division/district/upazila/postal code cascade (Siam, 16 Aug 2026)

Address tab-এর Division আর District ছিল একটা করে hard-coded option ("Dhaka"), Upazila আর Postal Code ছিল খালি text box। এখন চারটাই cascading dropdown — **Division → District → Upazila → Post office**, আর post office বাছলেই postal code বসে যায়। যে upazila-তে একটাই post office, সেখানে code নিজে থেকেই বসে।

ডেটা এসেছে root-এর `DatabaseSeeder.php` (Laravel 4 আমলের ফাইল, এই app-এ চলত না) থেকে — `backend/database/data/bangladesh-geo.json`-এ রূপান্তর করে।

**উৎসে দুটো ভুল পাওয়া গেছে, দুটোই ঠিক করা হয়েছে (নথিভুক্ত):**

- **Meherpur, Narail, Satkhira** ছিল Khulna **এবং** Sylhet — দুই জায়গাতেই, ৩৯টা row হুবহু copy। তিনটেই আসলে Khulna-র, তাই Sylhet-এর কপিগুলো বাদ। এটা না ধরলে district dropdown-এ একই জেলা দুই বিভাগে দেখাত।
- ১৫টা নামের প্রথম অক্ষর ছোট হাতের (`kalaroa`, `jahidpur`) — শুধু প্রথম অক্ষর বড় করা হয়েছে, কারণ এগুলো সরাসরি dropdown label।

শেষ ফল: **৭ বিভাগ, ৬৪ জেলা, ৪৭৯ উপজেলা, ১৩৩০ post office** — প্রতিটা জেলা ঠিক একটা বিভাগে।

**⚠ নজরে রাখুন:** dataset ২০১৫-র আগের — **Mymensingh বিভাগ নেই**, ওর জেলাগুলো Dhaka-র নিচে। উৎসে যা ছিল তাই রাখা হয়েছে; নিজে থেকে বিভাগ বানিয়ে জেলা সরানো হয়নি। ব্যবসায়িক সিদ্ধান্ত লাগলে data ফাইলটাই আপডেট করতে হবে।

**টেবিলগুলোর নাম `geo_` prefix দিয়ে** — কারণ `divisions` টেবিলটা আগে থেকেই দখলে, ওটা branch-এর নিচের org unit। দুটো গুলিয়ে গেলে বড় বিপদ।

**Test:** `GeoLocationApiTest` (৮টা) + `EmployeeAddressGeoValidationTest` (৫টা), দুটোই নতুন।

---

## 🐛 নিজের contact info update করা যাচ্ছিল না — "already in use" (Siam, 16 Aug 2026)

**লক্ষণ:** Contact info tab-এ নিজের তথ্য save করতে গেলেই `This official email is already in use. (and 4 more errors)`। অথচ নিজের row-ই edit করা হচ্ছে।

**কারণ:** `UpdateEmployeeContactRequest`-এর পাঁচটা `Rule::unique(...)`-এর একটাতেও **`->ignore()` ছিল না**। ফলে validation গোটা টেবিলে খুঁজত — যে row-টা edit হচ্ছে সেটাসহ — আর অপরিবর্তিত মান নিজের সাথেই সংঘর্ষ করত।

মজার ব্যাপার: **Service layer সঠিকই ছিল** — `existsOfficialEmail($companyId, $email, $contact->id)` নিজের id বাদ দিয়েই খুঁজত। বাগটা তার আগে, FormRequest-এ আটকে যেত।

**Fix:** পাঁচটা rule-এই `->ignore($this->route('id'))`।

**Test:** ২টা নতুন — একটা অপরিবর্তিত resave 200 দেয় কিনা, আরেকটা **অন্য employee-র email/mobile এখনো 422 দেয় কিনা** (নইলে rule তুলে দিলেই "fix" হয়ে যেত)।

**অন্য Update request-এ এই বাগ নেই** — সবগুলো `Rule::unique` ব্যবহারকারী request দেখা হয়েছে, বাকি সব Update-এ `ignore()` ছিল, বাকিগুলো Store (ওখানে লাগে না)।

### ⚠ একই জায়গায় আরও দুটো সমস্যা — ঠিক করিনি, বলা হলো

1. **Unique rule গুলো company-scoped নয়।** DB constraint `unique(['company_id','official_email'])`, service-ও company ধরে খোঁজে — কিন্তু FormRequest গোটা টেবিলে খোঁজে। মানে **অন্য company-র employee-র email আপনার এখানে block করে**, আর error message দিয়ে অন্য tenant-এর data থাকার কথা ফাঁস হয়।
2. **`emergency_contact_phone` আর `alternative_mobile_no` globally unique** — দুই ভাই একই কোম্পানিতে কাজ করলে বাবার নম্বর দুজনের emergency contact হতে পারবে না। এটা বাস্তবসম্মত নয়।

---

## Salary tab-এ mandatory field-এ লাল `*` (Siam, 16 Aug 2026)

Employee → Salary tab-এর form-এ কোন কোন field আবশ্যক তা দেখেই বোঝা যেত না — save চাপার পর error দেখে জানতে হতো।

**নতুন কিছু বানাতে হয়নি** — shared `Field` component-এ `required` দিলেই লাল `*` render হয় (`ContactInfoTab`, `AddressTab` আগে থেকেই এভাবে করে)। Salary form-এ কেবল prop-টাই কেউ দেয়নি।

কোনগুলোতে বসেছে তা `StoreEmployeeSalaryRequest`-এর সাথে মিলিয়ে নেওয়া হয়েছে, অনুমানে নয়: **salary structure, salary type, gross salary, currency, payment frequency, effective date**।

**Basic salary conditional** — `required={requiresBasic}`, অর্থাৎ যে structure basic component দাবি করে কেবল সেখানেই `*` ওঠে। বাকি সময় label-এ আগের মতোই "(optional)" থাকে, দুটো একসাথে কখনো দেখায় না।

`*` ছাড়াও native HTML5 `required` চালু হয় (component টা দুটোই করে) — এটা ঐ দুই tab-এর আচরণের সাথেই সঙ্গতিপূর্ণ।

---

## 🐛 Dropdown একবার select করলে আর deselect করা যেত না (Siam, 16 Aug 2026)

**কারণ শেয়ার্ড `Select`-এ, employee module-এ নয়।** Placeholder option-টা হার্ডকোড `disabled` ছিল:

```tsx
<option value="" disabled>{placeholder}</option>
```

মানে একবার কিছু বাছলে "কিছুই বাছা হয়নি" অবস্থায় আর ফেরা যেত না — optional field-এও না।

**Fix:** `disabled={required}`। Required field-এ আগের মতোই বন্ধ (ওখানে খালি করা বৈধ অবস্থাই নয়), optional field-এ খোলা।

**সবচেয়ে খারাপ ভুক্তভোগী ছিল filter গুলো** — Timeline tab-এর "Event Type" filter-এ `placeholder="All Types"`, একবার filter করলে আর সব event-এ ফেরা যেত না।

Employee-তে যা ঠিক হলো: Address-এর Country/Division/District/Upazila/Postal Code, Personal Info-র Blood Group ও Marital Status, Employment-এর Type/Work Mode/Probation Unit, Education-এর Board, Timeline-এর filter।

**⚠ পরিবর্তনটা শেয়ার্ড component-এ, তাই attendance/configuration/payroll/platform-এও প্রযোজ্য** (৭৬টা ফাইল `<Select>` ব্যবহার করে)। নতুন কোনো ঝুঁকি নেই — `''` মান আগে থেকেই পৌঁছনো সম্ভব ছিল (form খোলার সময় সব select খালিই থাকে), তাই payload-এ নতুন কিছু যাচ্ছে না, শুধু ঐ অবস্থায় ফেরা যাচ্ছে।

---

## `5.3-FE` merged — কিন্তু build ভেঙে গেছে (PR #188, 17 Aug 2026)

`att-5-3-fe-leave-approval` (Munna) main-এ। **Wave 4-এ leave track শেষ**, বাকি শুধু Bablu-র `5.5-FE`।

**কাজটা ভালো হয়েছে।** এই card-এর DoD অন্যগুলোর চেয়ে বেশি নির্দিষ্ট ছিল, আর প্রতিটাই মানা হয়েছে — balance detail খুললে নতুন করে fetch হয় (list থেকে বয়ে আনা নয়), loading বা insufficient অবস্থায় Approve disabled থাকে, reject reason `trim()` করে খালি হলে আটকায়, approved request-এ per-day `voided-by-punch` + তারিখ দেখায়, queue-তে চারটে filter-ই আছে। `9.1-BE`-র মতো DoD ফাঁকা রেখে আসেনি।

### 🔴 তবু দুটো জিনিস আটকে দিচ্ছে

**১. `npm run build` চলবে না।** Script হলো `check-style-boundary.sh && tsc -b && vite build`, আর `tsc -b` এখন **১৬টা error** দেয় — প্রতিটাই এই PR-এর তিনটে নতুন ফাইলে। এর ৯টা নিছক unused import/variable, বাকিগুলো আসল type সমস্যা (`ApiMeta` বলে কোনো export নেই, `DataTable` generic মেলেনি, ইত্যাদি)।

**২. একটা card rule চুপচাপ কাজ করছে না।** Detail page লিখেছে `<Alert variant="danger" title="…" action={<Button>Refresh Balance</Button>}>`, কিন্তু শেয়ার্ড `Alert` নেয় `tone` আর `actions`। তিনটে prop-ই অচেনা, তাই **banner লাল হয় না, শিরোনাম আসে না, আর "Refresh Balance" button একেবারেই render হয় না** — অথচ card-এ স্পষ্ট লেখা ছিল stale-balance failure "offers a refresh"। এটা type-ঝামেলা নয়, **আচরণের ঘাটতি**; এক শব্দের সংশোধনে মিটে যায়।

*(~~`LeaveRequest`-এ `days` নেই বলেও tsc চেঁচায়, কিন্তু ওটা runtime-এ ঠিকই আছে~~ — **এই দাবিটা ভুল ছিল**, নিচে দেখুন।)*

### ✅ ঋণ শোধ হয়েছে (17 Aug 2026)

**`npm run build` সবুজ** — style check clean (321 file), `tsc -b` ০ error, `vite build` ✓। `oxlint`-এর ৬টা warning পুরনো ও অন্য ফাইলে।

**"Refresh Balance" button এখন render হয়** — `Alert`-এ `tone`/`actions`, শিরোনামটা children-এ। Banner-ও এখন সত্যিই লাল।

**আর একটা bug বেরিয়েছে যা উপরে উল্টো লেখা ছিল।** `days` runtime-এ **কাজ করছিল না**। Backend দিকটা ঠিকই (`whenLoaded('days')` + show-এ eager-load), কিন্তু detail page যে `leaveRequestApi.get()` ব্যবহার করে সেটা `normalizeRequest()`-এর ভেতর দিয়ে যায় — একটা explicit object literal যেখানে `days` field-ই ছিল না। তাই payload-এ `days` এলেও ফেলে দেওয়া হতো, `request.days` সবসময় `undefined` থাকত, আর **voided-by-punch panel কখনোই render হতো না**। শুধু type যোগ করলে type-টা মিথ্যে বলত; `normalizeDay()` লিখে normalizer-এ carry করানো হয়েছে।

`DataTable`-টাও sibling `LeaveRequestListPage`-এর প্যাটার্নে আনা হয়েছে — explicit generic, `keyField="id"`, `emptyMessage`, আর error আলাদা `<Alert tone="danger">`-এ (`error`/`emptyTitle`/`emptyDescription` বলে কোনো prop `DataTable`-এ নেই, আর `keyField` required হয়েও দেওয়া হয়নি — অর্থাৎ tsc-র রিপোর্ট করা ১৬টার বাইরেও ভুল ছিল)।

**Munna এখন `9.2-BE` ধরতে পারে**, আর `5.5-FE` পরিষ্কার build-এর উপরে বসবে।

### সংখ্যা মিলিয়ে নেওয়া হয়েছে

Sequence file-এর Wave progress টেবিল **সাতটা merged card পিছিয়ে ছিল** (114 pts / 29 cards লেখা ছিল)। Seq সারিগুলোর status থেকে গুনে ঠিক করা হয়েছে — **done 140 pts / 36 cards, বাকি 88 pts / 18 cards**।

---

## 🐛 দুটো drawer-এ infinite render loop (17 Aug 2026)

Console-এ `UserRolesDrawer.tsx:39` → **"Maximum update depth exceeded"**। কোনো card-এর কাজ নয়, কিন্তু দুই জায়গায় একই ভুল ছিল বলে একসাথে সারানো হলো।

**কারণ:** `const { data: userRoles = [] } = useQuery(...)` — `data` যতক্ষণ `undefined` (loading, বা `uuid` null বলে query disabled), ততক্ষণ ঐ `= []` default **প্রতি render-এ নতুন array** দেয়। Array-টা `useEffect`-এর dependency, আর effect-টা `setSelected(...)` করে → নতুন reference → re-render → আবার নতুন `[]` → অনন্ত লুপ। `if (!show) return;` guard-টা শুধু drawer বন্ধ থাকলে বাঁচায়, তাই **drawer খুললেই fetch চলাকালীন লুপ**।

react-query-র `data` নিজে reference-stable — `= []` default-ই stability নষ্ট করছিল।

**ফিক্স:** default বাদ, fallback effect-এর ভেতরে (`userRoles?.map(...) ?? []`)। দুটো ফাইলেই comment রাখা হয়েছে যেন কেউ default-টা "পরিষ্কার" করতে গিয়ে ফিরিয়ে না আনে।

- `platform/components/UserRolesDrawer.tsx`
- `platform/components/RolePermissionsDrawer.tsx` ← হুবহু একই defect, রিপোর্ট হয়নি কিন্তু role permissions drawer খুললেই একইভাবে ভাঙত

**Sweep:** script দিয়ে গোটা `src` খোঁজা হয়েছে — literal default সহ query result যা setState-করা effect-এর dependency। এমন default ৩৭টা (২৭ ফাইলে), কিন্তু effect-dependency হিসেবে **কেবল এই দুটোই**। তিনটে `useMemo`-তে একই প্যাটার্ন আছে, ওগুলো লুপ করায় না (শুধু অকারণে recompute) — ছোঁয়া হয়নি। Detector-টা পুরোনো ভাঙা ফাইলের উপরে চালিয়ে যাচাই করা হয়েছে।

**যাচাই:** `npm run build` সবুজ · `oxlint src/modules/platform` ০ warning ০ error।

---

## 🐛 Super admin অন্য company-র user-কে permission assign করতে পারত না (Siam, 17 Aug 2026)

Platform → Users → Roles/Overrides drawer-এ save করলে `User '<uuid>' not found in this company.`

**কারণ:** `CompanyContextResolver` `X-Company-Id` header-টা মেলাত `whereHas('users', caller)` দিয়ে — অর্থাৎ caller ঐ company-র member হতে হবে। Super admin (`user_type = developer`) কোনো company-র member নয়, তাই header **নিঃশব্দে ফেলে দিয়ে** caller-এর default company-তে fall back করত। ফলে navbar থেকে company switch করলেও tenant context একটাই company-তে আটকে থাকত, আর `RoleEngine::resolveUser()` target user-কে ভুল company-র ভেতরে খুঁজত।

সাথে অসঙ্গতি: `GET /users` super admin-কে সব company-র user দেখাত, কিন্তু assign endpoint tenant context-এ locked — list-এর বেশিরভাগ target-ই reject হতো।

**সিদ্ধান্ত (Siam):** সব company-wise থাকবে; super admin/developer শুধু **company switch করে** যেকোনো company-তে ঢুকবে। আর ভুল/stale company header এলে silent fallback নয় — **403**।

**ফিক্স:**

- `Core/Tenancy/CompanyContextResolver.php` — `hasPlatformScope()`; platform user-এর header যেকোনো active company-তে resolve হয় (membership ছাড়াই), বাকি সবার জন্য membership check অপরিবর্তিত; resolve না হলে `PermissionDeniedException`
- `Core/Auth/Services/AuthService.php` — `formatCompanies()`/`switchCompany()` resolver-এর মধ্য দিয়ে, তাই super admin navbar-এ সব active company পায় এবং switch করতে পারে (আগে `findForUserByUuid` membership চাইত → 404)
- `Core/Users/Http/Controllers/Api/UserController.php` — user list সবসময় current company-তে scoped
- `platform/components/UserRolesDrawer.tsx` — role list current company-তে filter করা, নইলে অন্য company-র role বেছে ফেলা যেত

**সাইড-ইফেক্ট যা bug ধরিয়ে দিল:** `EmployeeDeductionTest` header-এ uuid-র বদলে company **id** পাঠাচ্ছিল — silent fallback-এর কারণে এতদিন accidentally pass করত। uuid-এ ঠিক করা হয়েছে।

**যাচাই:** নতুন `CompanyContextScopingTest` ৬/৬ · `EmployeeDeductionTest` ৭/৭ · Feature suite-এর বাকি failure baseline-এর সাথে হুবহু এক (attendance grace/timezone + pre-existing `SalaryAdvanceExecutor::reject` fatal) · Pint clean · `tsc --noEmit` clean।

---

## Navbar-এ company tab strip (Siam, 17 Aug 2026)

**সিদ্ধান্ত (Siam):** SaaS multi-company system — super admin যে company-তে ঢুকবে ঐ company-র সবকিছু দেখবে। Navbar-এ dropdown নয়, **প্রতি company-র জন্য tab/button**, একসাথে দুটো select করা যাবে না।

**Strip শুধু super admin/developer দেখে** — বাকি সবাই একটাই company-র ভেতরে কাজ করে, বাছার কিছু নেই।

- `core/components/layout/CompanySwitcher.tsx` (নতুন) — tab strip, `role="radiogroup"`/`role="radio"` (tab গুলো panel বদলায় না, active company-র *মান* বাছে), switch চলাকালীন সব disabled + target-এ spinner, company অনেক হলে strip scroll করে (header উঁচু হয় না)
- `core/components/layout/Header.tsx` — `<select>` বাদ
- `auth/context/AuthContext.tsx` — `can_switch_company` ধরা হয়; `switchCompany()` ব্যর্থ হলে আগের company uuid restore করে, সফল হলে `queryClient.clear()`
- `Core/Auth/Services/AuthService.php` — auth payload-এ `can_switch_company` (`CompanyContextResolver::hasPlatformScope()` থেকে)

UI-তে `user_type === 'developer'` hardcode করা হয়নি — কোন type company boundary পার হয় সেটা `config('platform.permissions.bypass_user_types')`-এর নিয়ম, ওটার দুটো copy রাখলে একদিন দুটো আলাদা হয়ে যাবে।

**সাথে সারানো দুটো bug:** (১) switch API ব্যর্থ হলে localStorage-এ নতুন uuid বসেই থাকত — এখন resolver 403 দেয় বলে app একটা নিষিদ্ধ company চাইতেই থাকত। (২) switch-এর পরে react-query cache clear হতো না, তাই আগের company-র row নতুন company-র নামের নিচে দেখা যেত।

**যাচাই:** `npm run build` সবুজ · `oxlint` ০ error (৫টা warning পুরোনো, অন্য ফাইলের)।

---

## 🐛 Company switch-এ page reload দিতে হতো (Siam, 17 Aug 2026)

Switch করার পর যে page-এ ছিলেন সেখানে পুরোনো company-র data-ই থাকত।

**দুটো আলাদা কারণ:**

1. `queryClient.clear()` cache মোছে কিন্তু **refetch করায় না** — mounted observer গুলো মৃত query ধরে বসে থাকে। `resetQueries()` দুটোই করে (data মোছে + active observer refetch করে)।
2. `employmentTypes`/`employeeAssets` redux-এ থাকে, thunk শুধু **mount-এ** চলে — cache যাই হোক ঐ page refetch করত না।

**ফিক্স:**

- `auth/context/AuthContext.tsx` — `clear()` → `await resetQueries()`, সাথে `dispatch(companyChanged())`
- `app/store.ts` — `companyChanged` action, root reducer সব slice initial state-এ ফেরায় (slice ধরে ধরে নয়, যাতে পরে নতুন slice এমনিতেই ঢাকা পড়ে)
- `core/components/layout/AppLayout.tsx` — `<Outlet key={currentCompany?.uuid} />`, switch-এ page remount

Reset আগে তারপর state update — উল্টো করলে key বদলানোর সময় আগের tenant-এর cached row এক ঝলক দেখা যেত। `staleTime` ৩০s বলে remount দুবার fetch করায় না।

**যাচাই:** `npm run build` সবুজ · `oxlint` ০ error।

---

## 🐛 Stale company uuid থাকলে boot-এ লগআউট হয়ে যেত (Siam, 17 Aug 2026)

PR #191-এর রিভিউতে ধরা — নিজেরই regression। Company header এখন ভুল হলে 403 দেয়, কিন্তু `AuthContext.hydrate()`-এর catch যেকোনো ব্যর্থতাকে মৃত session ধরে token+company মুছে দিত। ফলে **একটা company deactivate করলেই** যারা শেষবার ওখানে ছিল সবাই app খুললেই লগআউট।

**ফিক্স:** `loadSession()` — `me()` 403 দিলে stored company uuid ফেলে একবার retry; server তখন নিজের default বেছে নেয়। দ্বিতীয়বার ব্যর্থ হলে সেটা সত্যিই session-এর সমস্যা।

- `shared/api/client.ts` — `isForbidden()` helper
- `auth/context/AuthContext.tsx` — `loadSession()`, `hydrate()` ওটা ডাকে
- `tests/Feature/CompanyContextScopingTest.php` — deactivated company refused + header ছাড়া session টেকে (8/8 সবুজ)

> টেস্টে `withHeader()` জমতে থাকে; "header ছাড়া" request পাঠাতে `flushHeaders()` লাগে।

---

## 🔒 Tenant scoping model-এ নামানো হলো (Siam, 17 Aug 2026)

এক company-র configuration অন্য company দেখতে পাচ্ছিল। Audit করে দেখা গেল বাগ নয় — **৫৪টা tenant-owned model-এর একটাতেও global scope ছিল না**, প্রতিটা query-তে হাতে `where('company_id')` লিখতে হতো। যেখানে ভুলে গেছে সেখানেই leak।

শুধু Configuration নয় — Attendance (`ShiftService`), Payroll (`TaxSlabService`), Employee (`EmployeeTaxProfileService`), Core (`OnboardingPolicyService`) সবখানে একই প্যাটার্ন। মোট **৩২টা unscoped `find*ById()`** (cross-company edit/delete করা যেত) আর EmploymentType-এর list পুরোটাই leak করত।

**ফিক্স:** `BelongsToCompany` trait + `CompanyScope` global scope, ৪৭টা model-এ প্রয়োগ। Constraint এখন query-তে নয়, model-এ — তাই ভুলে যাওয়া `where` আর leak করাতে পারে না।

- `app/Core/Tenancy/CompanyScope.php`, `app/Core/Tenancy/Concerns/BelongsToCompany.php` — নতুন
- `TenantContext::enforce()` + `SetCompanyContext` — user থাকলে enforcement চালু
- `EmploymentTypeRepository` + `StoreEmploymentTypeRequest` — সরাসরি leak দুটো

**Permission layer বাদ** (`Role`, `UserPermissionOverride`, `Approval*`, `ActivityLog`, `Setting`) — নিয়ম আলাদা, আর approval job ইচ্ছাকৃতভাবে সব company জুড়ে চলে।

**Enforcement request-এ বাঁধা, console-এ নয়** — scheduled command, queue job, seeder অক্ষত। `runningInConsole()` ব্যবহার করিনি: PHPUnit-ও console, তাহলে feature test-এ scope নিষ্ক্রিয় থাকত।

**যাচাই:** `CompanyDataScopingTest` ১১/১১ সবুজ; fix সরালে ৬টা fail করে (অর্থাৎ test সত্যিই leak ধরে)। Feature suite baseline-এর সাথে হুবহু এক, নতুন failure নেই।

> আলাদা করে ধরা দরকার: `PayrollRunExecutor` আর `SalaryAdvanceExecutor` `ApprovalExecutorInterface::reject` implement করেনি — fatal error দিয়ে test suite থামিয়ে দেয়। আগে থেকেই আছে, এই কাজের বাইরে।

---

## 🐛 Required dropdown deselect করা যেত না (Siam, 17 Aug 2026)

Reporting manager, education degree, timeline event type, document type, organization department — একবার বাছাই করলে আর "Select …"-এ ফেরত যাওয়া যেত না।

**কারণ:** `Select.tsx`-এ placeholder `disabled={required}` ছিল। আগের রাউন্ডে optional field ঠিক হয়েছিল, required গুলো বাদ পড়ে যায় — অর্ধেক ফিক্স।

**ফিক্স:** placeholder সবসময় selectable। খালি করা মানে ফাঁক গলে যাওয়া নয় — `required` থেকেই যায়, তাই field আবার invalid হয় আর submit আটকায়।

- `shared/components/ui/form/Select.tsx` — `disabled={required}` বাদ

**পরিধি:** ২৭টা dropdown, ১৮ ফাইল। placeholder ছাড়া required Select (Status/Orientation ইত্যাদি closed enum) অপরিবর্তিত।

**যাচাই:** `oxlint` ০ error। `noValidate` ফর্ম গুলো scan করে দেখা হয়েছে — প্রতিটার নিজস্ব validation আছে, তাই খালি submit হবে না।

> `npm run build` main-এ আগে থেকেই লাল (৭টা TS error, `CorrectionApprovalDetailPage.tsx` + `ActivityLogPropertiesViewer.tsx`) — এই কাজের বাইরে, আলাদা করে ধরা দরকার।

---

## 🐛 Organization tab: static dropdown → আসল master data (Siam, 17 Aug 2026)

Organization assignment save করতে গেলে *"Master data for this field is not available yet."* — কারণ ৬টা dropdown `organizationDemoData.ts`-এর **হাতে লেখা array** থেকে ভরত, আর ঐ id ডেটাবেসে ছিল না। Branch-টা আবার আসল API থেকেই আসত, তবু backend-এর reject list-এ থাকায় আটকাত।

**Phase 1 (হয়ে গেছে) — যাদের ডেটা ও API দুটোই প্রস্তুত ছিল:**

- Branch, Division, Team, Grade এখন সত্যিকারভাবে গ্রহণ ও সংরক্ষণ হয়
- প্রতিটা company-scoped ভাবে validate হয় — অন্য company-র id দিলে 422
- Frontend-এ Division/Team/Grade → `listDivisions()` / `listTeams(1,100)` / `listEmployeeGrades()`

| ফাইল | পরিবর্তন |
|---|---|
| `EmployeeOrganizationAssignmentService.php` | reject list ৭ → ৩; branch/division/team/grade persist + validate |
| `EmployeeOrganizationAssignmentRepository.php` | generic `activeMasterExists()` |
| `OrganizationTab.tsx` | demo constant বাদ, আসল API |

**যাচাই:** `EmployeeOrganizationMastersTest` ৮/৮ সবুজ। `tsc -b` baseline-এর সমান।

**Phase 2 (বাকি):** Section, Cost Center, Work Location — table/API কোনোটাই নেই, নতুন master data হিসেবে বানাতে হবে। Shape product সিদ্ধান্ত বলে আলাদা ধাপে।

**Phase 2 (হয়ে গেছে) — Section, Cost Center, Work Location:**

তিনটেই flat master (`company_id + name + code + status`) হিসেবে বানানো, সাথে admin CRUD tab।

- Backend: ৩০টা ফাইল (migration/model/repo/service/request/resource/controller ×৩) + ১৮ route + binding। নতুন permission লাগেনি, বিদ্যমান `configuration.*` group-এই বসেছে।
- Frontend: `flatMasterApi.ts` (একটা factory → তিনটে client), `FlatMasterList`/`FlatMasterForm` (এক component, prop দিয়ে চালিত), System Configuration-এ ৩টা tab।
- `organizationDemoData.ts`-এর ৬টা static তালিকা মুছে ফেলা হয়েছে।

**ন'টা dropdown-ই এখন backend থেকে, company-scoped, আর save হয়।** `rejectUnavailableMasters()` মুছে গেছে — ঐ error message-টার আর অস্তিত্ব নেই।

**যাচাই:** `ConfigurationFlatMastersTest` ১৫/১৫ · `EmployeeOrganizationMastersTest` ৯/৯ · Feature suite baseline-এর সমান · `tsc`/`oxlint` clean।

> আলাদা করে ধরা দরকার: `grades` table-এ `grade_code` **global** unique আর `unique:grades,grade_name` rule — company-wise নয়। নতুন তিনটেয় `unique(['company_id','code'])` দেওয়া হয়েছে, কিন্তু Grade-এরটা এখনো ঠিক করা বাকি।

---

## 🐛 Grade uniqueness company-wise করা হলো (Siam, 17 Aug 2026)

`grades.grade_code`-এ global unique index আর `unique:grades,grade_name` rule — company ছাড়াই। **প্রথম company যে নাম নিত, বাকি সবার জন্য দখল হয়ে যেত।**

একই জায়গায় আরও তিনটে বাগ ধরা পড়ে:

1. `GradeUpdateRequest`-এ `route('grade')` পড়া হতো, route হলো `{id}` → সবসময় `null` → নিজের নামেই duplicate বলত
2. `generateGradeCode()` সংঘর্ষ করত ("Senior Engineer"/"Senior Executive" → দুটোই `SE-001`); `gradeCodeExists()` লেখা ছিল কিন্তু কখনো ডাকা হয়নি
3. `orderByRaw("FIELD(...)")` MySQL-only → sqlite-এ ভাঙে, তাই grades list কখনো test করা যায়নি

**ফিক্স:** নতুন migration (`unique(['company_id','grade_code'])` + `(company_id, grade_name)`), দুটো request company-scoped + route param ঠিক, code generator free না পাওয়া পর্যন্ত এগোয়, ordering portable।

**যাচাই:** `GradeCompanyScopingTest` ৫/৫ সবুজ · Feature suite baseline-এর সমান · migration চালানোর আগে duplicate গুনে দেখা হয়েছে (০)।

> **বাকি:** `FIELD(...)` Division/Team/Branch/EmployeeIdCardSetting repository-তেও আছে (৪টা) — একই এক-লাইন ফিক্স, এই কাজের বাইরে।

---

## 🔍 Company-wise কাজের post-merge review (Siam, 18 Aug 2026)

PR #196-এর পর পুরো change set আবার পড়া হয়েছে।

**নিশ্চিত:** ৫১টা model-এ `CompanyScope`; খালি query-তেও `where company_id` বসে; company না থাকলে `where 1 = 0`; নতুন master ও `grades` — সবের unique index `(company_id, …)`; **৪৮টা test সবুজ**; Feature suite baseline-এর সমান; scratch ফাইল commit হয়নি; দেড়শো লাইনের dead demo helper মুছে গেছে।

**খোলা আছে (কোনোটাই tenant leak নয়):**

1. `OrganizationTab`-এর `showDemoHistory` — history খালি বা error হলে এখনো **বানানো sample row** দেখায় (banner সহ)। demo-data প্যাটার্নের শেষ অবশিষ্টাংশ; empty/error state হওয়া উচিত।
2. Section/CostCenter/WorkLocation (আর Grade) **hard-delete** করে, Branch/Division/Team soft-delete করে। Assignment table-এ FK নেই, তাই ব্যবহৃত master মুছলে history-তে নামের বদলে `ID 5` থাকে।
3. `FIELD(...)` এখনো Division/Team/Branch/EmployeeIdCardSetting repository-তে (৪টা) — sqlite-এ ভাঙে, test করা যায় না।
4. `GradeService::update()` `$data['grade_name']` সরাসরি পড়ে যদিও rule-এ `nullable`।
5. Structure FK গুলোয় DB constraint নেই — service যাচাই করে, কিন্তু seeder/console পথে নয়।

---

## 🧹 "Punches" menu সরানো হলো (Siam, 18 Aug 2026)

Attendance menu-তে **Punch** (কাজ করে) আর **Punches** (`path: "#"`, কিছুই হতো না) — দুটো পাশাপাশি ছিল। Task card-এ "Punches" বলে কিছু নেই, শুধু `Attendance › Punch` (3.1-FE); entry-টা পরিকল্পনার বাইরে বসানো ছিল।

Page বানানোর বদলে সরানো হয়েছে, কারণ punch ডেটা ইতিমধ্যে দুই জায়গায় আছে — `/attendance/punch` (নিজের) আর record detail-এর timeline।

- `core/config/navigation.ts` — `attendance-punches` entry বাদ
- Backend অপরিবর্তিত: `GET /attendance/punches` Punch page-ই ব্যবহার করে

**যাচাই:** id-টা repo-র আর কোথাও ছিল না · `tsc` baseline-এর সমান · `oxlint` ০ error · Punch entry অক্ষত।

**Nav-এ dead link এখন ৪টা:** Monthly Approval (backend ✅ প্রস্তুত), Payroll Runs, Payslips, Disbursements (backend নেই)।

> Corrections আর Leave-এর `#` ভুল নয় — ওদের child-এ সঠিক path আছে, ওরা group header।

**পরের সবচেয়ে মূল্যবান কাজ:** Monthly Approval UI — ৯টা endpoint অব্যবহৃত, আর payroll এটার উপর আটকে আছে।

---

## ✅ সব ঋণ শোধ (18 Aug 2026)

**ফল:** backend **452 passed / 0 failed** · `tsc -b` **০ error** · `npm run build` সবুজ · `oxlint` **০ error**।

গোটা বিস্তারিত `ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md`-এর শেষ সেকশনে। এখানে টিমের জন্য যেটুকু দরকার:

### সব gate খুলে গেছে

| আগের gate | এখন |
|---|---|
| 🔴 ২৫টা লাল test | ✅ suite সবুজ। **এবং এতদিন CI ওগুলো চালাতই না** — `phpunit.xml`-এ module testsuite declare করা ছিল না। এখন আছে |
| `3.2-BE`-র টেস্ট-ঋণ | ✅ session-pairing ৩ + visibility-scoping ৫ |
| `7.4-BE`-র টেস্ট-ঋণ | ✅ ৪টা — **`8.2a-BE` আর আটকে নেই** |
| `9.1-BE`-র তিনটে DoD | ✅ scheduler ও idempotency test লেখা; unassigned report **আগে থেকেই ছিল** activity log-এ — **`9.4-FE` আর আটকে নেই** |
| `tsc -b` ৭ error | ✅ ০ |

### 🔴 যেটা জানা দরকার — nightly close কোনোদিন চলেনি

`attendance:schedule-close-day` command **কোথাও register করা ছিল না**। `routes/console.php`-এ hourly schedule লেখা আছে ঠিকই, কিন্তু module-এর command Laravel auto-discover করে না আর provider-এ `$commands` comment-out ছিল। `php artisan list`-এ command-টাই আসত না।

**অর্থাৎ `9.1-BE` merge হওয়ার পর থেকে আজ পর্যন্ত একটা দিনও নীরবে বন্ধ হয়নি।** ঠিক যে DoD-test-টা বাকি রাখা হয়েছিল, সেটাই এটা ধরল — ঋণ ফেলে রাখার দাম এর চেয়ে পরিষ্কার উদাহরণ আর হয় না।

### বাকি যে bug গুলো পথে বেরোল

- `sequence_no` superseded punch গুনত না → correction-এর পর একই দিনে দুটো punch একই নম্বর পেত
- Correction submit-এর response `pending` বলত, DB-তে `approved` বসত
- `GradeService::update()` name-only update-এ **status null করে দিত**
- `config/actions.php`-এ `include_common => true` থাকায় attendance/payroll-এ ৩০টা permission তৈরি হতো যা কোনো route যাচাই করে না
- `SalaryAdvanceExecutor`-এ reject করলে advance `pending_approval`-এই আটকে থাকত আর মাসের ceiling খেয়ে বসে থাকত

### পরের কাজ

| # | Card | pts | কেন |
|---|---|---|---|
| 1 | 🔴 **PR #198-এর দুটো gate** | — | `tsc -b` ১৫ error + লাল test — build ও suite দুটোই লাল (Bablu) |
| 2 | **`6.1-FE`-র ছয়টা DoD-ঋণ** | — | সবার আগে override checkbox — এখন Approve সবসময় override করে (Bablu) |
| 3 | **`8.1-BE`** → spine-এর বাকিটা | 5 | `6.2-BE` merged, Wave 6 এখান থেকে শুরু (Ashraful) |
| 4 | **`6.2-FE`** Freeze UI | 2 | Wave 5-এর শেষ card |
| 5 | **`9.4-FE`-র চারটে ঋণ** | — | Munna, `8.2a-BE`-র অপেক্ষায় থাকা অবস্থায় |

### একটাই খোলা রাখা হয়েছে

`employee_organization_assignments`-এ **FK constraint** — চলমান table-এ FK বসাতে গেলে পুরনো ডেটায় একটাও অবৈধ id থাকলে production migration ফেল করবে। soft-delete যোগ করায় আসল ক্ষতিটা (নাম হারানো) বন্ধ, তাই এটা আলাদা card হিসেবে ডেটা যাচাই সহ নেওয়া উচিত।


---

## ✅ `9.4-FE` Attendance Jobs UI — merged (18 Aug 2026, PR #197 · Munna)

Branch `att-9-4-fe-attendance-jobs`, ১৯ ফাইল (+1726 / −13)। **Attendance › Jobs** এখন live — unassigned employee report, তিন job-এর manual trigger, আর live batch progress।

**যাচাই:** `php artisan test` **453 passed / 1438 assertions** · `Modules/Attendance/tests` **79 passed** · `tsc -b --force` ০ error · `vite build` সবুজ · FE-র পাঁচটা call-ই আসল route-এ মেলে।

**সাথে BE-তেও একটা কাজ হয়েছে:** monthly accrual-এ **annual entitlement cap** — `entitled_days` আর কখনো policy-র `entitlement_days` ছাড়াবে না, আর service এখন সত্যিই কিছু লেখা হলে তবেই `true` ফেরায়। নতুন test সহ।

**⚠ চারটে ঋণ (Munna-র পরের কাজ):**

| # | কী | কোথায় |
|---|---|---|
| ১ | **"Fix Assignment" button filter করে না** — assignment list `search` param পড়েই না, তাই HR গোটা list-এ নামে। Card-এর AC ছিল "one click-এ ঐ employee" | `UnassignedEmployeesReport.tsx:113` |
| ২ | **Timezone hardcoded `Asia/Dhaka`**, আর সময় render হয় browser timezone-এ — job গুলো per-company timezone-এ চলে বলে card স্পষ্ট company-local চেয়েছিল | `AttendanceJobsPage.tsx:103,118,133,203` |
| ৩ | **Status panel unfiltered activity log পড়ে** (`per_page: 30`, কোনো `event` filter নেই) — ব্যস্ত tenant-এ job log ৩০-এর বাইরে চলে গেলে "Never run" ও "All Clear" মিথ্যে দেখাবে | `AttendanceJobsPage.tsx:30-38` |
| ৪ | **`JobStatusPanel.tsx` dead code** (কেউ import করে না), আর chunk-প্রতি log লেখায় stat গুলো এক chunk-এর, পুরো run-এর নয় | `components/jobs/` |

বিস্তারিত [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md)-এর শেষ অংশে।


---

## ⚠ `6.1-FE` Monthly Approval UI — merged (18 Aug 2026, PR #198 · Bablu)

Branch `att-6-1-fe-monthly-approval`, ২২ ফাইল (+1447 / −59)। **Attendance › Monthly Approval** এখন আসল screen — grid, filter, search, build drawer, bulk approve (queued batch + live progress + succeeded/failed result table), detail page।

**🔴 দুটো gate ভেঙে merge হয়েছে:**

| Gate | অবস্থা |
|---|---|
| `npm run build` | 🔴 `tsc -b` **১৫ error** — সবই নতুন `monthly-approval` ফাইলে (`Drawer`-এ `description` prop নেই ×2, অব্যবহৃত import ×4, implicit `any` ×9) |
| `php artisan test` | 🔴 **1 failed / 452 passed** — `MonthlyAttendanceApprovalServiceTest:48` পুরনো `['success']` key খোঁজে, `bulkApprove()` এখন queued batch envelope ফেরায় |

দুটোই ছোট কাজ, কিন্তু **এই নিয়ে চতুর্থবার FE card লাল build নিয়ে merge হলো** (`5.3-FE` · `5.5-FE` · `9.4-FE`-র আগে সবুজ ছিল · এখন `6.1-FE`), আর test-টা বলছে merge-এর আগে suite চালানোই হয়নি।

**⚠ ছয়টা DoD-ঋণ (Bablu-র পরের কাজ, ক্রম অনুযায়ী):**

| # | কী | কোথায় |
|---|---|---|
| ১ | 🔴 **Approve সবসময় `override: true`** — কোনো checkbox নেই, তাই `6.1-BE`-র unresolved guard UI থেকে কার্যত বন্ধ। Card বলেছিল override off-by-default ও deliberate | `MonthlyApprovalDetailPage.tsx:33` |
| ২ | **Unlock reason hardcoded** `'Requested by admin'` — reason input-ই নেই, তাই প্রতিটা unlock-এর audit trail একই বাক্য | `MonthlyApprovalDetailPage.tsx:41` |
| ৩ | **Unresolved popover decorative** — blocker list নেই, deep link নেই (`summary` prop নেওয়া হয়েও ব্যবহার হয়নি)। মূল কারণ BE — কোনো resource `unresolved_issues` expose করে না | `UnresolvedIssuesPopover.tsx` + `MonthlyAttendanceApprovalResource` |
| ৪ | **Day-by-day breakdown placeholder** — "grid goes here" লেখা; `getBreakdown()` API-তে আছে, কেউ ডাকে না। AC ছিল "no number is a dead end" | `MonthlyApprovalDetailPage.tsx` |
| ৫ | **Reject action নেই** (card action #6) | — |
| ৬ | **Permission gating নেই** (`monthly-approve` / `monthly-unlock` চেক হয় না), আর **frozen হলে Unlock লুকোয় না** — শুধু `is_locked` দেখা হয়, `frozen_at` নয় | দুটো page |

**BE-তে আলাদা করে দেখার তিনটে:** `getIdsByCompany()`-তে `status = 'active'` filter নেই (পাশের `chunkActiveWithoutAttendance()`-এ আছে) — তাই bulk build terminated employee-দেরও ধরবে · batch progress **প্রতি employee-তে একবার** cache-এ লেখা হয় আর `CACHE_STORE=database`, অর্থাৎ ৫০০ employee = ৫০০ বাড়তি DB write · নতুন queued path-এর একটাও test নেই।

বিস্তারিত [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md)-এর শেষ অংশে।

---

## ✅ Policy Group — PHASE 1: Assignment Foundation (18 Aug 2026 · Siam)

Policy Group + eligibility-ভিত্তিক auto-assign-এর ভিত্তি। **Group এখনো বানানো হয়নি** — Phase 1-এ শুধু assignment layer-টা সেই কাজের উপযোগী করা হলো।

**মূল সমস্যা যা সারল:** `assertNoActiveOverlap()` overlap key-তে `assignable_id` ছিল না, তাই একজন employee-কে Casual Leave দেওয়ার পর Sick Leave দিলেই exception — Policy Group-এর ধারণাটাই এখানে আটকে ছিল।

| # | কী | ফাইল |
|---|---|---|
| ১ | **Cardinality map** — `leave` = multi, `shift`/`holiday` = exclusive। শুধু `assignable_id` key-তে জুড়লে shift ভেঙে যেত | `Models/Assignment.php` |
| ২ | **`assignOrGet()`** — identity (assignable + scope + effective_date) ধরে idempotent; `create()` এখন এরই alias, তাই manual/bulk/auto এক পথে | `Services/AssignmentService.php` |
| ৩ | **Company scope normalize** (`scope_id = company_id`) — আগে write NULL / read companyId ছিল, তাই **company-wide assignment কারো জন্যই resolve হতো না** | `AssignmentService` + migration |
| ৪ | **`section` scope যোগ** (table/model আগেই ছিল, allowed list-এ ছিল না); Division/Team অক্ষত | `Assignment`, `scopeModelMap()` |
| ৫ | **`SCOPE_HIERARCHY` এক constant** — `company < branch < division < department < section < team < employee` | `Models/Assignment.php` |
| ৬ | **`EmployeeScopeResolver` (নতুন)** — তিন service-এর duplicate scope query এক জায়গায়, আর date-aware (`findActiveOnDate`) | `Services/EmployeeScopeResolver.php` |
| ৭ | **Balance seeding** — employee-scope assignment নিজের `scope_id` ব্যবহার করে (নতুন joiner আর miss হয় না), year এখন `effective_date` থেকে, `firstOrCreate` দিয়ে race-safe | `CreateLeaveBalanceOnAssignment`, `LeaveBalanceService` |
| ৮ | **DB constraint** — unique index `assignments_identity_unique`; duplicate থাকলে migration নাম বলে fail করে, নিজে delete করে না | migration |

**Metadata:** `assignments.source` (`manual`/`auto`/`bulk`) যোগ হয়েছে, `AssignmentResource`-এ expose করা — Phase 4-এর badge/filter-এর জন্য। `policy_group_id` Phase 2-তে আসবে (table না থাকলে column অর্থহীন)।

**নতুন test (২৭টা):** `AssignmentIdempotencyTest` (১২) · `AssignmentScopeResolutionTest` (১০) · `LeaveBalanceSeedingTest` (৫)।

**Gate:** `php artisan test` — **480 passed / 1 failed**। ওই একটা failure `MonthlyAttendanceApprovalServiceTest` — উপরে PR #198-এর ঋণ #-তে যেটা আগেই লেখা আছে, এই phase ওই ফাইল ছোঁয়নি।

**সিদ্ধান্ত যা lock হয়েছে:** policy variant (`policy_key`) **out of scope** — একই `policy.id` একাধিক scope-এ বসতে পারে, specificity resolution ঠিক করে · group-generated assignment সবসময় **employee scope** (probation_end_date per-employee বলে অন্য কিছু সম্ভব না) · re-evaluation **additive only**, পুরনো assignment কখনো delete/end হবে না · gender canonical value lowercase `male`/`female`/`other` (DB + UI + import তিনটাই যাচাই করা)।

**পরের ধাপ:** Phase 2 — Policy Group migration, model, eligibility JSON validation, evaluator, resolver, API।

---

## 📋 Policy Group — PHASE 2 PLAN (backend, শুরু হয়নি)

Phase 1-এর উপরে বসবে। **এই phase-এ কোনো assignment তৈরি হবে না** — Group, eligibility আর resolver বানানো হবে মাত্র। Assignment লেখা শুরু Phase 3-এ।

### ২.১ Migration (৩টা)

**`attendance_policy_groups`** — `attendance_policies`-এর convention হুবহু:

| column | note |
|---|---|
| `company_id` | tenant |
| `name` (150), `code` (50) | `unique (company_id, code)` |
| `description` | text, nullable |
| `eligibility` | **json** — `attendance_policies.config`-এর মতো |
| `effective_date_strategy` (20) | `joining_date` (default) / `probation_end_date` |
| `status` (20) | `Active` / `Inactive` |
| `created_by`, `updated_by` | nullable |

index `(company_id, status)`

**`attendance_policy_group_policy`** (pivot) — `company_id` (pivot-এও tenant scope), `policy_group_id`, `attendance_policy_id`, timestamps, `unique (policy_group_id, attendance_policy_id)`। FK **cascadeOnDelete** — group বা policy মুছলে pivot row অর্থহীন (নতুন `sections` migration-এর style)।

**`assignments`-এ `policy_group_id`** — nullable, index, FK **nullOnDelete**। Group মুছে গেলেও assignment ও তার balance/ledger বেঁচে থাকবে। **এটা কেবল metadata — runtime-এ কেউ পড়বে না।**

### ২.২ Model

`AttendancePolicyGroup` — `BelongsToCompany`, `eligibility` → `'array'` cast, `policies(): BelongsToMany` (explicit pivot table + `withTimestamps`), scope `forCompany`/`active`, status ও strategy constant।

### ২.৩ Eligibility JSON contract

```json
{
  "match": "all",
  "rules": [
    { "attribute": "employment_type_id", "operator": "in",     "value": [1, 2] },
    { "attribute": "department_id",      "operator": "in",     "value": [5] },
    { "attribute": "gender",             "operator": "equals", "value": "female" },
    { "attribute": "probation_status",   "operator": "equals", "value": "confirmed" }
  ]
}
```

- `match`: `all` (default) / `any`
- operator: `in` · `not_in` · `equals` · `not_equals` — এইটুকুই, YAGNI
- attribute (Phase 2): `employment_type_id` · `grade_id` · `department_id` · `branch_id` · `division_id` · `section_id` · `team_id` · `gender` · `probation_status`
- ভবিষ্যতে `designation_id` · `work_location_id` · `cost_center_id` — শুধু registry-তে এক লাইন

`PolicyGroupEligibilityRule` validator — `LeavePolicyConfigRule`-এর মতো unknown key/attribute/operator reject করবে।

### ২.৪ Eligibility evaluation (৪টা class)

```
PolicyGroupResolver
  └─ PolicyGroupEligibilityEvaluator
       ├─ EligibilityContextBuilder  → EmployeeEligibilityContext (DTO)
       ├─ EligibilityAttributeRegistry  (attribute → context field)
       └─ GenderNormalizer
```

**প্রতি attribute-এ আলাদা class বানানো হবে না** — একটা generic comparator + registry map। শুধু `gender` (normalize) আর `probation_status` (derived) আলাদা handler পাবে। CLAUDE.md §9/§11: academic abstraction নয়।

`EligibilityContextBuilder`-এ **batch method বাধ্যতামূলক** (`buildForCompany`) — Phase 3-এর "Apply to Existing" হাজার employee ঘোরাবে, per-employee query হলে মরবে। Org data আসবে Phase 1-এর `findActiveOnDate()` থেকে, তাই date-aware।

`GenderNormalizer`: trim + lowercase, তারপর `m`→`male`, `f`→`female`, `o`→`other`। **Data migration নয়** — শুধু তুলনার সময়, দুই পাশে।

### ২.৫ Resolver — Phase 3-এর হাতে যা তুলে দেবে

`PolicyGroupResolver::plan(companyId, employeeId, date)` → `list<ResolvedPolicyAssignment{ policyId, policyGroupId, effectiveDate }>`

effectiveDate = group-এর strategy অনুযায়ী: `joining_date`, বা `probation_end_date` (null হলে `joining_date`-এ fallback)।

এতে Phase 3-এর `AutoAssignmentService` নিছক একটা loop → `assignOrGet()` হয়ে যাবে, কোনো eligibility logic ছাড়া।

### ২.৬ API

`attendance.policy-group-view` / `attendance.policy-group-manage` — `config/actions.php`-এ (sort_order 22, 23), সাথে `AttendancePayrollPermissionRegistryTest`-এ key যোগ।

| method | path | কাজ |
|---|---|---|
| GET | `/policy-groups` | list (filter: status · policy_id · search) |
| POST | `/policy-groups` | create |
| GET | `/policy-groups/{id}` | detail + policies |
| PUT | `/policy-groups/{id}` | update (pivot sync transaction-এ) |
| PATCH | `/policy-groups/{id}/status` | policy/shift/type-এর convention |
| POST | `/policy-groups/eligibility/preview` | **unsaved** eligibility-র জন্য matching employee count + sample → UI-র live counter |
| GET | `/policy-groups/attributes` | rule builder-এর জন্য attribute/operator metadata |

`/attributes` endpoint-টা রাখছি যাতে frontend attribute list hardcode না করে — নাহলে একই তালিকা দুই জায়গায় থাকবে।

**"Apply to Existing" এখানে নয় — সেটা Phase 3**, কারণ ওটা assignment লেখে।

### ২.৭ যা আরও লাগবে

Repository + contract · Service + contract (pivot sync transaction-এ) · ৫টা FormRequest · `PolicyGroupResource` + `EligibilityPreviewResource` · `AttendanceServiceProvider`-এ binding।

**Service-level validation:** `policy_ids` সব একই company-র ও Active হতে হবে · eligibility-র `department_id`/`grade_id` ইত্যাদি company-তে আসলেই আছে কিনা — নাহলে group নীরবে কাউকেই match করবে না।

### ২.৮ Test

| file | কী |
|---|---|
| `Unit/PolicyGroupEligibilityEvaluatorTest` | employment type · grade · gender (#13,14,15) · match all/any · `not_in` · খালি rules · probation derivation |
| `Unit/GenderNormalizerTest` | `male`/`Male`/`M`/`female`/`F`/`other`/ফাঁকা |
| `Unit/EligibilityContextBuilderTest` | date-aware org (#17) · batch-এ N+1 নেই · employment ছাড়া employee |
| `Feature/PolicyGroupApiTest` | CRUD · tenant isolation · permission gating · code uniqueness · invalid eligibility reject · preview count |
| `Feature/PolicyGroupResolverTest` | একাধিক group-এর union · দুই group-এ একই policy → dedupe · probation strategy (#16) · inactive group/policy বাদ |

### ২.৯ চারটে ছোট সিদ্ধান্ত (default ধরে এগোব, আটকাব না)

1. **খালি `rules`** = company-র সবাই match করে ("Company General" group)
2. **`probation_status`**: `confirmation_date` বা `probation_end_date` পেরিয়ে গেলে `confirmed`; **দুটোই null হলে `confirmed`** (probation প্রযোজ্য নয় ধরে) — HR confirm করা দরকার
3. **একই policy দুই group-এ** → **আগের (earliest) effective_date জেতে**, employee সুবিধাটা তাড়াতাড়ি পায়
4. **Group status** `Active`/`Inactive` only, `Archived` নেই (policy-তে আছে, group-এ দরকার নেই)

### ২.১০ Gate

`php artisan test` সবুজ (`MonthlyAttendanceApprovalServiceTest`-এর পুরনো ঋণটা বাদে)। Phase 1-এর ২৭টা test অবিকল সবুজ থাকতে হবে — বিশেষ করে `AssignmentScopeResolutionTest`।

---

## ✅ Policy Group — PHASE 2: Backend (19 Aug 2026 · Siam)

Group + eligibility + resolver। **কোনো assignment লেখা হয়নি** — সেটা Phase 3।

### নতুন ফাইল (২১টা)

| স্তর | ফাইল |
|---|---|
| Migration | `attendance_policy_groups` · `attendance_policy_group_policy` · `assignments.policy_group_id` |
| Model | `AttendancePolicyGroup` |
| DTO | `EmployeeEligibilityContext` · `ResolvedPolicyAssignment` |
| Support | `GenderNormalizer` · `EligibilityAttributeRegistry` |
| Rule | `PolicyGroupEligibilityRule` |
| Service | `EligibilityContextBuilder` · `PolicyGroupEligibilityEvaluator` · `PolicyGroupResolver` · `AttendancePolicyGroupService` (+ ৪টা contract) |
| Repository | `AttendancePolicyGroupRepository` (+ contract) |
| HTTP | `PolicyGroupController` · ৫টা FormRequest · `PolicyGroupEligibilityRuleValidator` · `PolicyGroupResource` · `EligibilityAttributeResource` |
| Test | `GenderNormalizerTest` · `PolicyGroupEligibilityEvaluatorTest` · `EligibilityContextBuilderTest` · `PolicyGroupResolverTest` · `PolicyGroupApiTest` |

বদলানো: `routes/api.php` · `config/actions.php` · `AttendanceServiceProvider` · `AttendancePayrollPermissionRegistryTest`

### API

```
GET    /policy-groups                         view
GET    /policy-groups/attributes              view   ← wildcard-এর আগে declare
GET    /policy-groups/{id}                    view
POST   /policy-groups                         manage
POST   /policy-groups/eligibility/preview     manage ← unsaved rule-এর live count
PUT    /policy-groups/{id}                    manage
PATCH  /policy-groups/{id}/status             manage
DELETE /policy-groups/{id}                    manage
```

### Eligibility

Attribute: `employment_type_id` · `grade_id` · `branch_id` · `division_id` · `department_id` · `section_id` · `team_id` · `gender` · `probation_status`
Operator: `in` · `not_in` · `equals` · `not_equals` · match `all`/`any`

**প্রতি attribute-এ আলাদা rule class নেই** — একটা registry table + generic comparator। নতুন attribute = এক লাইন।

**Save-time guard:** অন্য tenant-এর বা অস্তিত্বহীন department/grade id দিলে **422**, নাহলে group নীরবে save হয়ে কাউকেই match করত না।

### Test (৫৯টা নতুন)

`GenderNormalizerTest` ১৪ · `PolicyGroupEligibilityEvaluatorTest` ১৬ · `EligibilityContextBuilderTest` ৭ · `PolicyGroupResolverTest` ১৩ · `PolicyGroupApiTest` ১২

দুটো performance regression test আছে — context builder ৩ query-তে আটকানো, resolver-এর group read ২ query-তে (employee সংখ্যা নির্বিশেষে)।

### Review-এ যা সারানো হলো

১. `EligibilityAttributeRegistry` প্রতি lookup-এ closure সহ rebuild হচ্ছিল → memoized
২. `PolicyGroupResolver::planFor()` প্রতি employee-তে group query করত — Phase 3-এর backfill-এ ৫০০০ employee = ৫০০০ query → instance cache + `forgetCachedGroups()`
৩. `delete()`-এর comment যা বলছিল কোড তা করত না → provenance হারানো assignment এখন log হয়

### Gate

`php artisan test` — **542 passed / 1 failed**। ওই একটাই পুরনো `MonthlyAttendanceApprovalServiceTest` (PR #198-এর ঋণ)। Phase 1-এর সব সবুজ।

### পরের ধাপ — Phase 3

`EmploymentCreated` · `EmploymentUpdated` · `OrganizationAssignmentCreated` event → Attendance listener → `PolicyGroupAutoAssignmentService` → `resolver->plan()` → `assignOrGet()`। সাথে "Apply to Existing Employees" preview + bulk (existing `BulkAssignmentService` batch pattern reuse)।

⚠ Phase 3-এ মনে রাখতে হবে: resolver-এর group cache instance-scoped — queue worker-এ job-এর মাঝে `forgetCachedGroups()` ডাকতে হবে, নাহলে edit করা group পুরনো অবস্থায় ধরা পড়বে।

---

## ✅ Policy Group — PHASE 3: Events + Auto-Assignment (19 Aug 2026 · Siam)

```
Event → ReevaluatePolicyGroupsForEmployee → PolicyGroupAutoAssignmentService
      → resolver->planFor() → assignOrGet()
```

Phase 2-এর একটা class-ও বদলায়নি।

### নতুন ফাইল (১১টা)

| স্তর | ফাইল |
|---|---|
| Event (Employee) | `EmploymentCreated` · `EmploymentUpdated` · `OrganizationAssignmentCreated` |
| Listener | `ReevaluatePolicyGroupsForEmployee` |
| Service | `PolicyGroupAutoAssignmentService` · `PolicyGroupBackfillService` (+ ২ contract) |
| DTO | `AutoAssignmentOutcome` |
| Support | `BatchProgressStore` (BulkAssignmentService থেকে extract) |
| Job | `ProcessPolicyGroupBackfillChunkJob` |
| Request | `PolicyGroupBackfillRequest` |
| Test | `PolicyGroupAutoAssignmentTest` · `PolicyGroupEventTriggerTest` · `PolicyGroupBackfillTest` |

### বদলানো (৯টা)

`EmployeeEmploymentService` · `EmployeeOrganizationAssignmentService` (event fire) · `Attendance/EventServiceProvider` · `AttendanceServiceProvider` · `Assignment` (fillable) · `AssignmentService` (`policy_group_id`) · `CreateLeaveBalanceOnAssignment` (**after-commit fix**) · `BulkAssignmentService` (BatchProgressStore) · `PolicyGroupController` + routes · `PolicyGroupApiTest`

### 🔴 যেটা ধরা পড়ল

চারটে event-ই transaction-এর ভিতরে fire হয়, আর `config/queue.php`-তে সব connection-এ `after_commit => false`। Queued listener commit-এর আগেই চলতে পারত। দুটো listener-এ `$afterCommit = true` — নতুনটা, আর **`CreateLeaveBalanceOnAssignment`** যেটা Phase 1 থেকেই latent ছিল (test-এ `QUEUE_CONNECTION=sync` বলে ধরা পড়েনি)।

### API (৩টা নতুন route, `policy-group-manage`)

```
POST /policy-groups/{id}/apply/preview   → eligible/already_assigned/new/conflicts/skipped, কিছু লেখে না
POST /policy-groups/{id}/apply           → 202, chunked job dispatch
GET  /policy-groups/{id}/apply/{batchId} → progress
```

### Test (৩৪টা নতুন)

`PolicyGroupAutoAssignmentTest` ১২ · `PolicyGroupEventTriggerTest` ১১ · `PolicyGroupBackfillTest` ১১ (+ API-তে ৪টা)

Idempotency, duplicate prevention, tenant isolation, effective-date propagation, multi-group, retry, cache lifecycle — সব কভার করা।

### Gate

`php artisan test` — **580 passed / 1 failed**। ওই একটাই পুরনো `MonthlyAttendanceApprovalServiceTest` (PR #198-এর ঋণ)। Phase 1 ও 2-এর সব সবুজ।

### ⚠ Phase 4 (UI)-এর আগে জানা দরকার

**১. তারিখ পেরোলে কেউ reevaluate করে না** — `probation_status = confirmed` rule বা ভবিষ্যৎ-তারিখের transfer, তারিখ এলেও কিছু হয় না, পরের event পর্যন্ত অপেক্ষা। দুটোরই test আছে। Nightly sweep লাগবে, Phase 3-এর scope-এ ছিল না।
**২. `probation_status` — দুই date null = `confirmed`**, HR confirmation এখনো বাকি।
**৩.** Probation-এর জন্য HR-কে **rule নয়, `effective_date_strategy`** ব্যবহার করতে হবে — UI-তে এটা বোঝানো দরকার, নাহলে confirmed-only group কাউকেই ধরবে না।

---

## ✅ Policy Group — PHASE 4: UI (19 Aug 2026 · Siam)

### নতুন ফাইল (৯টা)

| স্তর | ফাইল |
|---|---|
| Types/API | `types/policyGroup.ts` · `api/policyGroupApi.ts` |
| Component | `EligibilityRuleBuilder` · `EligibilityRuleRow` · `EligibilityPreviewCard` · `PolicyPickerPanel` · `useEligibilityOptions` |
| Page | `PolicyGroupListPage` · `PolicyGroupFormPage` · `PolicyGroupApplyPage` |
| Employee | `profile-tabs/AssignedPoliciesTab.tsx` |

### বদলানো (৮টা)

`attendance/index.tsx` (routes) · `AttendanceConfigNav` (tab) · `ScopePicker` (**Section**) · `types/assignment.ts` (section scope + source + policy_group_id) · `AssignmentListPage` (source column + filter) · `utils/assignmentDisplay.ts` (section label) · `profile-tabs/registry.ts` · backend: `AssignmentResource` + `IndexAssignmentRequest`

### 🔴 Backend contract-এ দুটো ফাঁক ধরা পড়ল

UI লিখতে গিয়ে বেরোলো:
১. `AssignmentResource`-এ **`policy_group_id` ছিল না** — source badge-এর group link ওটার উপর দাঁড়ানো, browser-এ নীরবে ভাঙত
২. `IndexAssignmentRequest`-এ **`source` filter whitelist করা ছিল না** — repository support করত কিন্তু `validated()` ফেলে দিত

দুটোই সারানো + regression test (`the_assignment_api_exposes_the_provenance_the_ui_reads`)।

### UI-র সবচেয়ে দরকারি দুটো জিনিস

**১. Live match counter** — form-এ "১৪২ of ২০০ employees match", ০ হলে লাল সতর্কতা। কাউকে না-ধরা group নীরবে save হয়ে যেত, ধরার আর উপায় ছিল না।

**২. Probation warning** — form-এ স্থায়ী alert: confirmed-দের জন্য **Probation end date option** ব্যবহার করো, `probation_status = confirmed` **rule নয়**। Phase 3-এর ফাঁকটা ব্যবহারকারীর সামনে আনা।

### Gate

| Check | ফল |
|---|---|
| `tsc -b` | **আমার ০ error**। বাকি ১৩টা `monthly-approval`/`monthlyAttendanceApi`-তে — PR #198-এর ঋণ |
| `npm run build` | 🔴 style-boundary-তে আটকায় `JobStatusPanel.tsx` (dead code, `9.4-FE` ঋণ #৪), আমি ছুঁইনি |
| `php artisan test` | **581 passed / 1 failed** (পুরনো `MonthlyAttendanceApprovalServiceTest`) |

⚠ **merge-এর আগে:** repo-র build পুরনো কারণেই লাল। ওই ১৫টা tsc error + `JobStatusPanel` সারানো দরকার — নাহলে পঞ্চমবার লাল build merge হবে।

### Policy Group feature সম্পূর্ণ (Phase 1–4)

Assignment foundation → Policy Group backend → Events + auto-assignment → UI। বাকি দুটো জিনিস:
**১.** তারিখ পেরোলে reevaluate করার nightly sweep (probation শেষ / ভবিষ্যৎ-তারিখের transfer)
**২.** `probation_status` — দুই date null = `confirmed`, HR confirmation বাকি

---

## 🔧 REPO DEBT — Pre-existing gate failures (Policy Group-এর অংশ নয়)

**অবস্থা:** টিকিট খোলা, কাজ শুরু হয়নি। Policy Group Phase 1–4-এর সাথে **ইচ্ছাকৃতভাবে মেশানো হয়নি** — Phase 4-এর diff সবুজ দেখাতে অন্যের ঋণ সারানো হলে দুটো কাজই অস্পষ্ট হয়ে যেত।

এই তিনটে Policy Group-এর আগে থেকেই লাল ছিল, আর repo-র build gate এখন এগুলোর জন্যই আটকে আছে।

| # | কী | কোথায় | উৎস |
|---|---|---|---|
| ১ | **১৩টা TypeScript error** | `api/monthlyAttendanceApi.ts` (২) · `monthly-approval/BuildMonthlyAttendanceDrawer.tsx` (২) · `BulkApproveDrawer.tsx` (১) · `UnresolvedIssuesPopover.tsx` (১) · `MonthlyApprovalDetailPage.tsx` (২) · `MonthlyApprovalListPage.tsx` (৫) | PR #198 (`6.1-FE`) |
| ২ | **Style-boundary build failure** | `components/jobs/JobStatusPanel.tsx` — Bootstrap class, আর ফাইলটা **dead code** (কেউ import করে না) | `9.4-FE` ঋণ #৪ |
| ৩ | **`MonthlyAttendanceApprovalServiceTest::bulk_approve_partial_failure`** | test `['success']` পড়ে, service `['succeeded']` ফেরায় | PR #198 |

### করার সময় খেয়াল

- **আলাদা branch, আলাদা commit** — Policy Group-এর কোনো ফাইলের সাথে মিশবে না
- `#২`-এ প্রশ্ন করতে হবে: `JobStatusPanel.tsx` dead code, তাই **মুছে ফেলা** নাকি Tailwind token-এ রূপান্তর? মোছাই সম্ভবত ঠিক, কিন্তু সেটা সিদ্ধান্ত
- `#৩` সারানোর সময় ঠিক করতে হবে test না service কোনটা ভুল — `bulkApprove()` এখন queued batch envelope ফেরায়, তাই সম্ভবত test-ই পুরনো
- সারানোর পর **নিজে থেকে full gate চালাতে হবে**: `npm run build` + `php artisan test` দুটোই সবুজ হতে হবে

---

## ⏸ Policy Group — অনুমোদনের অপেক্ষায় থাকা দুটো follow-up

**কোনোটাই implement করা হয়নি।** স্পষ্ট অনুমোদন ছাড়া শুরু হবে না।

### F1 — তারিখ পেরোলে পুনর্মূল্যায়নের nightly sweep

Reevaluation শুধু event-এ চলে, তাই **তারিখ নিজে থেকে এসে গেলে কিছু ঘটে না**:
- `probation_status = confirmed` rule দেওয়া group — probation শেষ হলেও কেউ ফিরে দেখে না
- ভবিষ্যতের তারিখে করা transfer — সেই দিন এলেও নতুন department-এর group আসে না

দুটোরই behaviour এখন test দিয়ে pin করা (`a_transfer_dated_in_the_future_does_not_grant_the_new_group_yet`), তাই এটা লুকানো নয় — জানা সীমা।

**সাময়িক উপায়:** probation-এর জন্য rule নয়, `effective_date_strategy` ব্যবহার (UI-তে alert দিয়ে বলা আছে)।

### F2 — `probation_status`-এর semantics স্পষ্ট করা

এখনকার অনুমান: `confirmation_date` → নাহলে `probation_end_date` → **দুটোই null হলে `confirmed`**।

`employee_employments.status` (`probation`/`confirmed`/`active`/…) থেকে নেওয়া হয়নি, কারণ ওটা কারো মনে করে flip করার উপর নির্ভরশীল যেখানে date গুলো effective-dated সত্য।

**HR confirmation দরকার:** probation date না থাকা মানে কি "probation প্রযোজ্য নয়" (এখনকার ধারণা), নাকি "এখনো probation-এ"? উত্তর উল্টো হলে `EmployeeEligibilityContext::probationStatus()`-এ এক লাইন বদল + test আপডেট।

---

## ⚠ DEPLOY STEP — নতুন permission DB-তে sync করতে হবে

**লক্ষণ:** Attendance › Configuration › **Policy Groups** tab-এ ক্লিক করলে কিছুই আসে না — চুপচাপ home-এ ফিরিয়ে দেয়।

**কারণ কোডে নয়, environment-এ।** `config/actions.php`-এ permission যোগ করা মানে DB-তে row তৈরি হওয়া নয়। Permission না থাকলে `RequirePermissionRoute` `<Navigate to="/" replace />` করে — কোনো error দেখায় না, তাই "কিছু হচ্ছে না" মনে হয়।

**সমাধান — প্রতিটা environment-এ একবার:**

```bash
php artisan db:seed --class=ActionSeeder --force
```

`syncFromConfig()` চালায় — additive ও idempotent: নতুন action তৈরি করে, module-এ link করে, permission row বানায়, আর administrator role-এ যোগ করে। পুরনো কিছু মোছে না।

**তারপর permission cache bust করতে হবে**, নাহলে চালু session পুরনো তালিকা দেখতে থাকবে:

```php
app(App\Platform\Contracts\PermissionEngineContract::class)->bustCacheForCompany($companyId);
```

**যাচাই:**

```
attendance.policy-group-view    => id=122, role: administrator
attendance.policy-group-manage  => id=123, role: administrator
GET /api/v1/attendance/policy-groups             → 200
GET /api/v1/attendance/policy-groups/attributes  → 200
```

administrator ছাড়া অন্য role-কে (HR ইত্যাদি) দিতে হলে Roles screen থেকে হাতে assign করতে হবে — `syncSystemAdministratorRoles()` কেবল administrator-কেই দেয়।

> 💡 এটা Policy Group-এর নিজস্ব সমস্যা নয় — **যেকোনো নতুন permission**-এর ক্ষেত্রেই একই ধাপ লাগে। আগের কোনো card-এ লেখা ছিল না বলে এখানে লিখে রাখা হলো।

---

## ✅ F2 — Explicit `probation_status` (19 Aug 2026 · Siam)

Probation এখন date থেকে derive হয় না — **HR নিজে হাতে সেট করা field**।

### কেন lifecycle `status` reuse করা হয়নি

`employee_employments.status` আসলে lifecycle (`probation → active → resigned/terminated`), আর HR-এর dropdown-এ **`confirmed` option-ই ছিল না** — বাস্তবে confirmed মানে `active` বসানো হতো। তার উপর ওই column payment ও headcount report পড়ে। তাই আলাদা `probation_status` column, দুটোই আলাদা প্রশ্নের উত্তর দিতে পারে।

### বদল (backend ৮ + frontend ৩)

| স্তর | কী |
|---|---|
| Migration | `employee_employments.probation_status` varchar(20), default `probation`, index — **date-derived backfill সহ** |
| Model | `PROBATION_STATUS_PROBATION` / `_CONFIRMED` + `allowedProbationStatuses()` |
| Request | Store (`nullable`) · Update (`sometimes`) |
| Service | `create()`-এ explicit value লেখে · `update()`-এ **HR-only** (`status`-এর মতো, self-update-এ বাদ) |
| Resource | expose |
| **DTO** | derivation বাদ → stored readonly property; `on_probation` → **`probation`** |
| Builder | employment থেকে পড়ে; record না থাকলে `probation` |
| Registry | property read করে |
| Frontend | payload type · HR-এর নতুন **Probation status** dropdown · eligibility label |

### যা ইচ্ছাকৃতভাবে অপরিবর্তিত

`probation_end_date` ও `confirmation_date` context-এ আছেই, আর **`effectiveDateFor()` হুবহু আগের মতো** — probation strategy এখনো ওই তারিখেই assignment শুরু করে। Date-typed eligibility attribute (A(b)) **scope-এ নেই**।

### Backfill — default দিয়ে ভরে দেওয়া হয়নি

সবাইকে default `probation` দিলে ইতিমধ্যে confirmed কর্মীরা probation-এ ফিরে যেত আর confirmed-only group থেকে নীরবে বাদ পড়ত। তাই পুরনো date logic দিয়েই backfill:

```
confirmation_date <= আজ                        → confirmed
নাহলে probation_end_date <= আজ                 → confirmed
দুটোই null                                      → confirmed  (probation প্রযোজ্য নয়)
বাকি সব                                         → probation
```

**যাচাই:** আসল MySQL-এ পাঁচটা date-combination synthetic row দিয়ে migration-এর হুবহু predicate চালিয়ে **৫/৫ PASS**, transaction rollback করে (কোনো row থাকেনি)। Migration rollback + re-apply-ও যাচাই করা।

### Rule-data migration লাগেনি

Deploy-এর আগে persisted rule-এ `on_probation` আছে কিনা দেখতে হবে:

```php
DB::table('attendance_policy_groups')->get(['id','code','eligibility'])
  ->filter(fn ($g) => str_contains((string) $g->eligibility, 'on_probation'));
```

Dev-এ **০টা group, ০টা hit** — তাই কোনো data migration লাগেনি। Staging/production-এ hit পেলে `probation`-এ বদলাতে হবে।

### Test (587 pass / 1 fail)

**Rewrite (৩টা, semantics বদলেছে বলেই):** `probation_status_is_derived_from_the_evaluation_date` → `..._is_read_from_what_hr_recorded` · `a_confirmation_date_takes_precedence...` → `the_dates_no_longer_decide_probation_status` · `an_employee_with_no_probation_recorded_counts_as_confirmed` → `..._defaults_to_probation_until_hr_says_otherwise`

**অপরিবর্তিত (৪টা)** — effective-date strategy-র test, semantics বদলায়নি: `the_effective_date_follows_the_group_strategy` · `a_missing_probation_end_falls_back_to_the_joining_date` · `the_probation_strategy_starts_the_assignment_when_probation_ends` · `the_effective_date_comes_from_the_resolver_untouched`

**নতুন (৮টা):** HR confirm → group পায় (end-to-end) · probation-এ ফেরালে পুরনো assignment অক্ষত · নিজে নিজেকে confirm করতে পারে না · নতুন record `probation`-এ শুরু · stored field date-কে override করে · employment record না থাকলে `probation` · attributes endpoint-এর value মডেলের সাথে মেলে

একমাত্র failure সেই পুরনো `MonthlyAttendanceApprovalServiceTest`। `tsc -b` — **আমার ০ error**।

### F2-এর পরে F1-এর পরিসর ছোট হলো

Probation আর তারিখ পেরোলে নীরবে বদলায় না — HR flip করলেই event যায়। **nightly sweep-এ এখন কেবল future-effective transfer আর date-ভিত্তিক effective-date বদল বাকি।**

### F3 অপরিবর্তিত (আলাদা follow-up)

`EmployeeImportService` এখনো service bypass করে — কোনো event যায় না। তবে **নতুন field-টা নিরাপদ:** column-এ NOT NULL + default `probation`, তাই import-এ তৈরি record-ও নির্দিষ্ট মান পায়, inconsistent হয় না। Import-এর event/auto-assignment গ্যাপ F3-এই থাকল।

---

## ✅ F3 — Import → Policy Group event gap (19 Aug 2026 · Siam)

Import এতদিন domain service bypass করত, তাই **কোনো event fire হতো না** — bulk onboarding করা employee-রা policy group কিছুই পেত না।

### সমাধান — importer-ই write path থাকল

Service-এ route করিনি (তোমার constraint): `EmployeeEmploymentService::create()` employment থাকলে throw করে, `transfer()`-এর নিজস্ব effective-date নিয়ম আছে — ওগুলো ধরলে **যে row আগে import হতো সেটা fail করত**, Policy Group-এর কোনো লাভ ছাড়াই।

বদলে `ensure*` method গুলো এখন **কী করল সেটা return করে**, আর `processRow()` সব লেখা শেষে একবার event তোলে:

| Outcome | Event |
|---|---|
| employment created | `EmploymentCreated` |
| employment `wasChanged()` | `EmploymentUpdated` |
| org assignment নতুন | `OrganizationAssignmentCreated` |
| department বদল / current row বদল | `EmployeeTransferred` |
| unchanged | কিছু না |

**Change detection Eloquent-এর `wasChanged()` দিয়ে** — হাতে before/after তুলনা করলে date string vs cast Carbon-এর format নিয়ে ভুল হতো। Repository যে instance-এ লেখে সেটাই dirty state ধরে রাখে, তাই duplicate logic নেই।

### Ordering

Event **`ensure*`-এর ভিতরে তোলা হয়নি** — employment org assignment-এর আগে লেখা হয়, তাই ওখানে তুললে department লেখার আগেই reevaluation চলত (additive বলে শেষমেশ ঠিক হতো, কিন্তু দুইবার কাজ)। এখন row সম্পূর্ণ হলে তবেই।

### Transaction

Event row-এর transaction-এর ভিতরে ওঠে, দুই দিকেই নিরাপদ — queued listener-এ `afterCommit` আছে, আর যে row throw করে সে event তোলার জায়গা পর্যন্ত পৌঁছায়ই না।

### 🔴 নতুন করে ধরা পড়ল — cache invalidation production-এ কাজ করে না

F3-এর test array store-এ পাস করে (tag support আছে)। কিন্তু **dev ও production-এ `CACHE_STORE=database`, যেটা tag support করে না** — আর `ClearAssignmentResolutionCache`-এর fallback branch ভুল key মোছে:

```
resolver লেখে   : assignment_resolution_{company}_{employee}_{date}
listener মোছে   : assignment_resolution_version_employee_{employee}   ← কখনো মেলে না
```

**যাচাই করা:** database store-এ real key বসিয়ে fallback branch চালানোর পর key **অক্ষত** — invalidation পুরোপুরি no-op।

মানে F3 event contract ঠিক করেছে, কিন্তু **listener-এর নিজের defect-এর কারণে production-এ cache এখনো stale থাকবে**। Key-নাম দেখে মনে হয় version-key scheme-এর অর্ধেক implementation — সম্পূর্ণ করা মানে নতুন cache mechanism, যেটা F3-এর scope-এ নিষিদ্ধ।

**→ আলাদা task (F4) হিসেবে নিচে তোলা হলো। আমি ছুঁইনি।** Redis-এ গেলে এমনিতেই ঠিক হয়ে যাবে (tag support আছে)।

### Test (নতুন ১৫টা — import-এর প্রথম test)

`Modules/Employee/tests/Feature/EmployeeImportPolicyGroupTest.php` — create/update/unchanged event · employment data ছাড়া row · placement vs transfer · **cache invalidation** · reevaluation নতুন department দেখে · ordering · failed row-এ কোনো event নেই · discarded row-এ কিছু টেকে না · imported record `probation` · **re-import HR-এর confirmation নষ্ট করে না**

### Gate

`php artisan test` — **602 passed / 1 failed** (সেই পুরনো `MonthlyAttendanceApprovalServiceTest`)।

### Queue implication

Row-প্রতি ২টা পর্যন্ত event → ২টা reevaluation job। ৫০০০ row = সর্বোচ্চ ১০,০০০ job। Idempotent, কিন্তু background mode-ই স্বাভাবিক পথ। Sync mode-এ import ধীর হবে।

### জানা সীমা

Personal info (**gender**) eligibility-তে যায়, কিন্তু `EmployeePersonalInfoUpdated` বলে কোনো event নেই — তাই import-এ শুধু gender বদলালে reevaluation হবে না। নতুন event বানানো "existing event only" constraint-এর বাইরে, তাই লিখে রাখলাম।

---

## 🔧 F4 (নতুন) — `ClearAssignmentResolutionCache` non-tag store-এ no-op

**Policy Group-এর অংশ নয়, F3-ও এটা সারায়নি।** Pre-existing, `git`-এ untouched।

`CACHE_STORE=database` (dev + production default) tag support করে না, তাই listener fallback-এ যায় আর **ভুল key** মোছে। ফলে assignment resolution cache (TTL ২৪ ঘণ্টা) **কখনো invalidate হয় না** — assignment বদলালে বা employee transfer হলেও পুরনো shift/policy resolve হতে থাকে।

দুটো পথ: (ক) version-key scheme সম্পূর্ণ করা (key-নাম দেখে সেটাই মূল নকশা মনে হয়), (খ) Redis-এ যাওয়া (tag support আছে, তখন কোড বদল লাগে না)। **সিদ্ধান্ত দরকার।**

---

## ✅ F4 — CLOSED (no cache redesign)

**সিদ্ধান্ত:** version-key scheme বানানো হয়নি। Production Redis-এ যাবে, তখন tag support-এ এমনিতেই কাজ করবে।

### Read-only check-এর ফল

| # | প্রশ্ন | উত্তর |
|---|---|---|
| ১ | Production Redis ব্যবহার করে? | ⚠️ **না।** Redis compose-এ আছে ও healthy (`REDIS_HOST=redis`), কিন্তু **কিছুই ওটা ব্যবহার করে না** |
| ২ | ইচ্ছাকৃত `database` cache path আছে? | **না — কিন্তু `.env.example`-এ `CACHE_STORE=database`**, অর্থাৎ প্রতিটা নতুন environment default-এ ভাঙা invalidation পায়। CI/entrypoint-এ কোনো override নেই |

মানে "production Redis-এ যাবে" এখন **intent, configuration নয়**।

### যা যোগ করা হলো (cache key বা listener ছোঁয়া হয়নি)

- **`.env.example`-এ `CACHE_STORE`-এর উপরে ব্যাখ্যা** — কেন tag-capable store লাগে, কী ভাঙে, redis যে ইতিমধ্যেই আছে
- **`GET /api/v1/attendance/health` এখন cache diagnostics দেয়** — store যদি tag support না করে তবে `status: degraded` + স্পষ্ট কারণ। নীরব ভুল configuration এখন দৃশ্যমান
- **২টা test** — tag-capable-এ `ok`, না হলে `degraded`

Redis-এ গেলে health নিজেই `ok` বলবে; কোড বদলাতে হবে না।

---

## 🔴 F5 (নতুন, গুরুতর) — দুটো scheduled command registered নয়, তাই কোনোদিন চলেনি

F1-এর infrastructure দেখতে গিয়ে ধরা পড়ল।

`routes/console.php` তিনটে command hourly schedule করে:

```
attendance:schedule-close-day            ✅ registered
attendance:schedule-leave-accrual        ❌ NOT registered
attendance:schedule-leave-carry-forward  ❌ NOT registered
```

`AttendanceServiceProvider::boot()`-এ `$this->commands([...])`-এ **কেবল `ScheduleCloseDayCommand`** আছে। Module command auto-discover হয় না (ওই ফাইলেই comment দিয়ে লেখা), তাই বাকি দুটো artisan-এর কাছে **অস্তিত্বহীন**।

**যাচাই:** `php artisan attendance:schedule-leave-accrual` → "command not found" (did-you-mean prompt দেয়)। অথচ `schedule:list`-এ তিনটেই hourly দেখায়।

**ফল: monthly leave accrual আর year-end carry-forward কোনোদিন চলেনি।** প্রতি ঘণ্টায় scheduler অস্তিত্বহীন command ডেকে ব্যর্থ হচ্ছে।

**সমাধান তুচ্ছ** — provider-এ দুটো class যোগ করা। কিন্তু **F1-এর আগেই সারানো দরকার**, নাহলে নতুন sweep command-ও একই ফাঁদে পড়বে। Accrual/carry-forward না চলার ফলে balance-এ কী প্রভাব পড়েছে সেটাও দেখতে হবে।

---

## ✅ F5 — Scheduled command registration (19 Aug 2026 · Siam)

### Code fix (১ ফাইল)

`AttendanceServiceProvider::boot()`-এ দুটো class যোগ:

```php
$this->commands([
    ScheduleCloseDayCommand::class,
    ScheduleLeaveAccrualCommand::class,        // ← ছিল না
    ScheduleLeaveCarryForwardCommand::class,   // ← ছিল না
]);
```

**Business logic ছোঁয়া হয়নি** — দুটো command-এর ভিতরে এক লাইনও বদলায়নি।

### Verification

| Check | ফল |
|---|---|
| `artisan list` | তিনটেই দেখা যায় ✅ |
| `schedule:list` | তিনটেই hourly ✅ |
| সরাসরি invocation | দুটোই exit 0 (local hour না মেলায় সঠিকভাবে skip করে) ✅ |
| `LeaveAccrualJobTest` + `LeaveBalanceServiceTest` | ৮/৮ পাস ✅ |

### Regression guard (২টা test)

`AttendanceScheduledCommandsTest` — `routes/console.php`-এ schedule করা **প্রতিটা** artisan command Artisan-এ registered কিনা মেলায়, সাথে তিনটে নাম explicit ভাবে।

**Guard কাজ করে প্রমাণ করা:** fix সাময়িকভাবে revert করে test চালানো হয়েছে → **ফেল করেছে** সঠিক বার্তা সহ, তারপর restore করে → পাস। অর্থাৎ ভবিষ্যতে কেউ command বাদ দিলে বা নতুন schedule যোগ করে register করতে ভুললে test ধরবে।

### 📊 Historical data impact — কিছু হারায়নি

**কবে থেকে অকার্যকর:** `826c8dcb` (**17 Aug 2026**) — command class ও schedule line একই commit-এ এসেছে, কিন্তু provider registration কখনোই হয়নি। Provider-এ `ScheduleCloseDayCommand` যোগ হয়েছে পরদিন (`e67141c7`, 18 Aug), বাকি দুটো বাদ পড়ে যায়। **অকার্যকর জানালা: 17 → 19 Aug, ~২ দিন।**

**ওই জানালায় কতবার চলার কথা ছিল:**

| Command | Trigger | Window-এ প্রত্যাশিত run |
|---|---|---|
| leave-accrual | মাসের **১ তারিখ** 01:xx local | **০** |
| leave-carry-forward | **১ জানুয়ারি** 02:xx local | **০** |

১৭–১৯ অগাস্টের মধ্যে মাসের ১ তারিখও পড়েনি, ১ জানুয়ারিও না। **তাই একটাও execution মিস হয়নি।**

**Data যাচাই (dev DB, read-only):** `leave_balances` ০ · ledger ০ · monthly-accrual ledger ০ · monthly accrual_method-এর policy **০টা** · active leave assignment ২টা। কোনো employee accrual মিস করেনি, কারণ accrual করার মতো কিছুই এখনো নেই।

**পরের trigger: ১ সেপ্টেম্বর ২০২৬** — তার আগেই ধরা পড়েছে।

### ⚠️ Data repair লাগবে না — তবে production-এ একই audit চালাতে হবে

উপরের সংখ্যাগুলো **dev database-এর**। Production-এ employee/policy বেশি থাকলে একই read-only audit চালিয়ে নিশ্চিত হতে হবে — বিশেষ করে **১ সেপ্টেম্বরের আগে deploy না হলে** window বড় হয়ে যাবে আর তখন সত্যিই একটা accrual মিস হবে।

**অর্থাৎ: আলাদা data-repair task এখন দরকার নেই, কিন্তু deploy-এর তাড়া আছে।**

### Gate

`php artisan test` — **606 passed / 1 failed** (সেই পুরনো `MonthlyAttendanceApprovalServiceTest`)। F2 · F3 · F4 অপরিবর্তিত। F1 শুরু হয়নি।

---

## ✅ F1 — Nightly eligibility sweep (19 Aug 2026 · Siam)

তারিখ আসাটাই একমাত্র পরিবর্তন যার কোনো event নেই। বাকি সব কিছু কেউ একজন করে — HR confirm করে, employment লেখে, department বদলায় — আর প্রতিটাই তখনই event তোলে। **তিন সপ্তাহ পরের তারিখে করা transfer event তোলে যেদিন record করা হয়**, তখনো employee পুরনো department-এ। যেদিন সত্যিই effective হয়, কিছুই ঘটে না।

### নতুন ফাইল (৪)

`EligibilitySweepService` (+ contract) · `ScheduleEligibilitySweepCommand` · `EligibilitySweepTest`
**বদলানো:** `config.php` · `routes/console.php` · `AttendanceServiceProvider` · `PolicyGroupAutoAssignmentService` (নিচে দেখো)

### Sweep query

```php
EmployeeOrganizationAssignment::where('company_id', $companyId)
    ->whereDate('effective_from', '<=', $date)                  // company-local আজ
    ->whereDate('effective_from', '>=', $windowStart)           // আজ − lookback
    ->whereColumn('created_at', '<', 'effective_from')          // ← যেগুলো লেখার সময়ই ভবিষ্যতের
    ->distinct()->pluck('employee_id');
```

শেষ শর্তটাই মূল কথা: **যে row যেদিন effective সেদিনই লেখা হয়েছে সে ইতিমধ্যে normal path-এ event তুলেছে**, তাকে আবার sweep করা মানে প্রতিটা busy joining day-তে নিছক duplicate কাজ। পুরো workforce কখনো load হয় না — শুধু id, chunk করে।

### Timezone ও lookback

Hourly command, প্রতি company তার **নিজের timezone-এ 03:xx** হলে চলে (close-day 02, accrual 01 — সংঘর্ষ এড়াতে)। Lookback ৭ দিন (config): scheduler এক রাত না চললে ওই employee-রা চিরকাল বাদ পড়ে যেত, কারণ তারিখ একবার পেরোলে কেউ ফিরে দেখে না। পুনরাবৃত্তি বিনামূল্যে — reevaluation idempotent।

### Event flow — নতুন কিছু বানানো হয়নি

```
sweep → EmployeeTransferred → ReevaluatePolicyGroupsForEmployee  (policy group)
                            → ClearAssignmentResolutionCache     (stale resolution)
```

`EmployeeTransferred` **যাচাই করে** বেছে নেওয়া: এটাই একমাত্র event যার listener দুটো কাজই করে। ওতে from/to নেই — listener-দের কাছে এর মানে "এই employee-র organisational অবস্থান এখন বদলেছে", যেটা আজ সত্যিই ঘটেছে, row যেদিনই লেখা হোক। তাই transfer আর প্রথমবার placement — দুটোর জন্যই সঠিক।

### 🔴 F1 একটা আসল production bug বের করে এনেছে (Phase 3-এ)

Sweep company-local তারিখ (`2026-09-01`) হিসাব করে, কিন্তু `reevaluateEmployee()` date না পেলে **`now()`** ব্যবহার করত — server timezone-এ।

**প্রমাণ:** Asia/Dhaka-তে 03:00 on 2026-09-01 মানে UTC-তে **2026-08-31 21:00**। তাই reevaluation ৩১ তারিখ ধরে খুঁজত, support assignment তখনো effective নয় → `skipReason: 'No policy group applies'` → **কিছুই assign হতো না**।

UTC-র পূর্বের প্রতিটা company-র জন্য sweep নীরবে অকেজো হতো। **এটা শুধু sweep-এর সমস্যা নয়** — মধ্যরাতের কাছাকাছি করা সাধারণ HR transfer-ও একই ভুল দিন দেখত।

**সবচেয়ে ছোট সমাধান:** `reevaluateEmployee()` date না পেলে এখন **company-র timezone-এ আজকের তারিখ** নেয় (`CompanyRepositoryInterface`, যেটা এই module আগে থেকেই ঠিক এই কাজে ব্যবহার করে)। ৭ লাইন, আচরণ বদল নয় — bug fix।

⚠️ **এটা Phase 3-এর ফাইল ছুঁয়েছে।** "F3 ছোঁয়া যাবে না" নির্দেশ ছিল, কিন্তু **এটা ছাড়া F1 কাজই করে না** — non-UTC company-তে sweep নীরবে কিছুই করত না। রিভিউতে দেখে নিও।

### Idempotency ও concurrency

Custom dedup নেই — `assignOrGet` + Phase 1-এর unique index। Schedule-এ `withoutOverlapping()`, সাথে command-এ **per company+date cache lock** (একটা company দুবার sweep হয় না, এমনকি হাতে চালালেও)।

### Test (১৫টা, সব পাস)

তোমার ১১টা দাবিই কভার। উল্লেখযোগ্য: lookback missed run ধরে · একই দিনে লেখা row বাদ যায় · lookback configurable · অন্য tenant ছোঁয় না · দুবার sweep = duplicate নেই · timezone gate · lock ধরা থাকলে চলে না · cache invalidate হয় · command registered (F5-এর guard)।

### Performance

Company-প্রতি **১টা query** affected id বের করতে — সাধারণত দিনে হাতে-গোনা row। Employee-প্রতি ১টা event → ১টা queued reevaluation। বড় reorg-এর দিনে chunk (config, default ২০০) কাজে লাগে।

### Gate

`php artisan test` — **621 passed / 1 failed** (সেই পুরনো `MonthlyAttendanceApprovalServiceTest`)। F2 · F3 · F4 · F5 অপরিবর্তিত (Phase 3-এর ওই একটা timezone fix ছাড়া)।

---

## Production speed / Docker ops (3 Sep 2026) — card নয়, points অপরিবর্তিত

Lane বণ্টন বদলায় না। Docs এখন `docs/attendance-payroll/`। চালানো: local `make up`, server `make prod-up`. বিস্তারিত [ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md](./ATTENDANCE_PAYROLL_BUILD_SEQUENCE.md) শেষ সেকশন।


## Ops note (3 Sep 2026) — Real client IP

Completed: edge bind + CIDR whitelist + client-ip helper + punch IP hardening. Deploy checklist in root README.


## Cross-module note (14 Sep 2026) — PMS Phase 1 schema lock

Attendance/payroll lanes unchanged. PMS planning added **PMS-SCHEMA-1** (full Phase 1 DB schema for all tables) → [docs/project-management/wbs/phase1-database-schema.md](../project-management/wbs/phase1-database-schema.md); task cards updated in [PMS_PHASE1_TASK_CARDS.md](../projects/PMS_PHASE1_TASK_CARDS.md).

## Cross-module note (16 Sep 2026) — Commissioning C3

Attendance/payroll lanes unchanged. Commissioning **C3** (allocation, redistribution, approve/post, ledger) complete on stub — [COMMISSIONING_IMPLEMENTATION_PLAN.md](../project-management/commission/COMMISSIONING_IMPLEMENTATION_PLAN.md). No payroll consume yet.

## Cross-module note (16 Sep 2026) — Commissioning C4

Attendance/payroll lanes unchanged. Commissioning **C4** (fund unlock + employee fund + exit settlement) complete — same plan doc.

## Cross-module note (16 Sep 2026) — Commissioning C5

Attendance/payroll lanes unchanged. Commissioning **C5** (RealProjectContext + reports + PaymentCleared + outbound salary/fund events) complete — Payroll can later listen to `CommissionPostedToSalary`.

## Cross-module note (16 Sep 2026) — Commissioning post-C5 + Payroll hook

Commissioning tenant `company_id` fix + real-mode smoke test. Payroll **`HandleCommissionPostedToSalary`** skeleton listener wired in `Modules/Payroll` (log-only until commission earning component contract is defined).

## Cross-module note (16 Sep 2026) — Commission payout mode

Immediate commission can be **with salary** (Payroll event) or **separate** (Commissioning payout queue + mark paid). Fund portion still unlocks on schedule.

## Cross-module note (16 Sep 2026) — Commissioning rule form UX

Attendance/payroll lanes unchanged. Commissioning rule form UX clarified for non-technical users (examples/hints; “pool” → commission share labels).

## Cross-module note (16 Sep 2026) — Commissioning rule drawer UX

Attendance/payroll lanes unchanged. Commissioning rule drawer enlarged + scope pickers (named dropdowns via `scope-options` API).

## Cross-module note (16 Sep 2026) — Commissioning advanced rule policy UI

Attendance/payroll lanes unchanged. Commissioning rule form now exposes designation/eligibility/redistribution/fund unlock/termination policy fields.

## Cross-module note (16 Sep 2026) — Commissioning rule form bilingual toggle

Attendance/payroll lanes unchanged. Rule drawer **English / বাংলা** toggle; full form copy switches by locale (`commissionRuleFormCopy.ts`).
