<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FT-BILL-FUNDED-STATUS-01 · Đồng Bộ & Hiển Thị Đúng Trạng Thái "Funded" Của Hóa Đơn</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Noto+Sans+SC:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.9.0/dist/mermaid.min.js"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'Noto Sans SC', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'ui-monospace', 'monospace'],
},
colors: {
ink: { 50:'#fafafa', 100:'#f5f5f5', 200:'#e5e7eb', 300:'#d4d4d4', 400:'#a3a3a3', 500:'#737373', 600:'#525252', 700:'#404040', 800:'#262626', 900:'#171717', 950:'#0a0a0a' },
brand: { 50:'#eef4ff', 100:'#dbe6ff', 200:'#bfd1ff', 300:'#94b1ff', 400:'#6286ff', 500:'#3b5bfd', 600:'#2940e0', 700:'#1f31b3', 800:'#1c2a8c', 900:'#1c2870' },
accent: { 500:'#ea580c', 600:'#c2410c' },
ok: '#16a34a', warn: '#d97706', danger: '#dc2626', info: '#0284c7'
},
maxWidth: { 'prose-65': '65ch' }
}
}
}
</script>
<style>
html { scroll-behavior: smooth; }
body { font-feature-settings: "ss01","cv11"; }
.font-cjk { font-family: 'Noto Sans SC', 'Inter', sans-serif; }
/* Sidebar nav active state */
.nav-link.active { background: #eef4ff; color: #1f31b3; border-color:#bfd1ff; }
.dark .nav-link.active { background:#1c2870; color:#bfd1ff; border-color:#1f31b3; }
.toc-link.active { color:#2940e0; border-left-color:#2940e0; font-weight:600; }
.dark .toc-link.active { color:#94b1ff; border-left-color:#94b1ff; }
/* Code block */
pre.code-block { background:#0a0a0a; color:#fafafa; border-radius:14px; padding: 2.75rem 1.25rem 1.25rem; position:relative; overflow:auto; font-family:'JetBrains Mono', monospace; font-size: 13px; line-height: 1.65; border:1px solid #262626; }
pre.code-block .lang-tag { position:absolute; top:10px; left:14px; font-size:11px; letter-spacing:.08em; text-transform:uppercase; color:#a3a3a3; }
pre.code-block .copy-btn { position:absolute; top:8px; right:8px; font-size:11px; padding:4px 10px; border-radius:8px; background:#262626; color:#e5e7eb; border:1px solid #404040; cursor:pointer; transition:all .15s; }
pre.code-block .copy-btn:hover { background:#404040; color:#fff; }
pre.code-block .copy-btn.copied { background:#16a34a; color:#fff; border-color:#16a34a; }
/* Syntax tokens */
.tk-kw { color:#c084fc; }
.tk-str { color:#86efac; }
.tk-num { color:#fbbf24; }
.tk-com { color:#737373; font-style:italic; }
.tk-fn { color:#7dd3fc; }
.tk-op { color:#fda4af; }
/* Callouts */
.callout { border-left:4px solid; border-radius: 12px; padding: 14px 18px; display:flex; gap:12px; align-items:flex-start; }
.callout-info { background:#f0f9ff; border-color:#0284c7; color:#0c4a6e; }
.callout-warn { background:#fffbeb; border-color:#d97706; color:#78350f; }
.callout-danger { background:#fef2f2; border-color:#dc2626; color:#7f1d1d; }
.callout-success { background:#f0fdf4; border-color:#16a34a; color:#14532d; }
.dark .callout-info { background:#082f49; color:#bae6fd; }
.dark .callout-warn { background:#451a03; color:#fde68a; }
.dark .callout-danger { background:#450a0a; color:#fecaca; }
.dark .callout-success { background:#052e16; color:#bbf7d0; }
/* Tables */
table.doc-table { width:100%; border-collapse:separate; border-spacing:0; font-size:14px; }
table.doc-table th { text-align:left; background:#fafafa; font-weight:600; color:#262626; padding: 10px 14px; border-bottom:1px solid #e5e7eb; font-size: 12.5px; letter-spacing:.02em; text-transform:uppercase; }
table.doc-table td { padding: 12px 14px; border-bottom:1px solid #f5f5f5; color:#404040; vertical-align: top; }
table.doc-table tr:last-child td { border-bottom:none; }
.dark table.doc-table th { background:#171717; color:#fafafa; border-color:#262626; }
.dark table.doc-table td { color:#d4d4d4; border-color:#262626; }
/* Pang-gu spacing utility kept native (we rely on Noto's spacing) */
/* Chip */
.chip { display:inline-flex; align-items:center; gap:6px; padding:2px 10px; border-radius:999px; font-size:11.5px; font-weight:500; border:1px solid; line-height:1.6; }
.chip-brand { background:#eef4ff; color:#1f31b3; border-color:#bfd1ff; }
.chip-warn { background:#fffbeb; color:#92400e; border-color:#fde68a; }
.chip-danger { background:#fef2f2; color:#991b1b; border-color:#fecaca; }
.chip-ok { background:#f0fdf4; color:#166534; border-color:#bbf7d0; }
.chip-ink { background:#f5f5f5; color:#404040; border-color:#e5e7eb; }
.dark .chip-brand { background:#1c2870; color:#bfd1ff; border-color:#1f31b3; }
.dark .chip-warn { background:#451a03; color:#fde68a; border-color:#78350f; }
.dark .chip-danger { background:#450a0a; color:#fecaca; border-color:#7f1d1d; }
.dark .chip-ok { background:#052e16; color:#bbf7d0; border-color:#14532d; }
.dark .chip-ink { background:#262626; color:#d4d4d4; border-color:#404040; }
/* h2/h3 scroll offset for sticky header */
h2[id], h3[id] { scroll-margin-top: 90px; }
/* mermaid */
.mermaid { background:#fafafa; border:1px solid #e5e7eb; border-radius: 14px; padding: 18px; overflow:auto; }
.dark .mermaid { background:#171717; border-color:#262626; }
/* search highlight */
mark.hl { background: #fde68a; color:#78350f; padding: 0 2px; border-radius: 3px; }
.dark mark.hl { background:#78350f; color:#fde68a; }
/* focus */
a:focus-visible, button:focus-visible, input:focus-visible { outline: 2px solid #3b5bfd; outline-offset: 2px; border-radius: 6px; }
/* hide scrollbars softly */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-thumb { background:#d4d4d4; border-radius:4px; }
.dark ::-webkit-scrollbar-thumb { background:#404040; }
</style>
</head>
<body class="bg-ink-50 dark:bg-ink-950 text-ink-800 dark:text-ink-100 font-sans antialiased">
<!-- ============ TOP BAR ============ -->
<header class="sticky top-0 z-40 bg-white/85 dark:bg-ink-900/85 backdrop-blur border-b border-ink-200 dark:border-ink-800">
<div class="max-w-[1440px] mx-auto px-6 h-14 flex items-center gap-4">
<div class="flex items-center gap-2.5 min-w-0">
<div class="w-7 h-7 rounded-lg bg-brand-600 text-white grid place-items-center font-bold text-sm">N</div>
<div class="min-w-0">
<div class="text-[13px] font-semibold truncate">NolimitHub Docs</div>
<div class="text-[11px] text-ink-500 dark:text-ink-400 truncate">Billing · Sync · Risk</div>
</div>
</div>
<nav class="hidden md:flex items-center text-[13px] gap-1 text-ink-500 dark:text-ink-400 ml-2">
<span>›</span>
<span class="text-ink-700 dark:text-ink-200">Engineering RFC</span>
<span>›</span>
<span class="text-ink-900 dark:text-white font-medium">FT-BILL-FUNDED-STATUS-01</span>
</nav>
<div class="flex-1"></div>
<div class="relative w-64 hidden lg:block">
<input id="searchInput" type="search" placeholder="Search trong tài liệu… ( / )"
class="w-full h-9 pl-9 pr-3 rounded-lg bg-ink-100 dark:bg-ink-800 border border-ink-200 dark:border-ink-700 text-[13px] focus:bg-white dark:focus:bg-ink-900 focus:border-brand-400" />
<svg class="absolute left-2.5 top-2.5 w-4 h-4 text-ink-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
</div>
<div class="flex items-center gap-2 text-[12px]">
<span class="chip chip-brand">v1.0 · Draft</span>
<span class="chip chip-ink hidden sm:inline-flex">2026-06-03</span>
<button id="themeToggle" aria-label="Toggle theme"
class="w-9 h-9 grid place-items-center rounded-lg border border-ink-200 dark:border-ink-700 hover:bg-ink-100 dark:hover:bg-ink-800 transition">
<svg class="w-4 h-4 dark:hidden" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
<svg class="w-4 h-4 hidden dark:block" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
</button>
</div>
</div>
</header>
<!-- ============ LAYOUT ============ -->
<div class="max-w-[1440px] mx-auto px-6 grid grid-cols-12 gap-8 pt-8 pb-24">
<!-- ============ LEFT NAV ============ -->
<aside class="col-span-12 lg:col-span-3 xl:col-span-2">
<div class="sticky top-20">
<div class="text-[11px] font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400 mb-3 px-2">Tài liệu</div>
<nav id="sectionNav" class="space-y-0.5 text-[13.5px]">
<a href="#s1" class="nav-link block px-3 py-2 rounded-lg border border-transparent hover:bg-ink-100 dark:hover:bg-ink-800 transition">
<span class="text-ink-400 mr-2">1</span> Thông Tin Chung
</a>
<a href="#s2" class="nav-link block px-3 py-2 rounded-lg border border-transparent hover:bg-ink-100 dark:hover:bg-ink-800 transition">
<span class="text-ink-400 mr-2">2</span> Bối Cảnh & Mục Tiêu
</a>
<a href="#s3" class="nav-link block px-3 py-2 rounded-lg border border-transparent hover:bg-ink-100 dark:hover:bg-ink-800 transition">
<span class="text-ink-400 mr-2">3</span> Luồng Nghiệp Vụ
</a>
<a href="#s4" class="nav-link block px-3 py-2 rounded-lg border border-transparent hover:bg-ink-100 dark:hover:bg-ink-800 transition">
<span class="text-ink-400 mr-2">4</span> Yêu Cầu Chức Năng
</a>
<a href="#s5" class="nav-link block px-3 py-2 rounded-lg border border-transparent hover:bg-ink-100 dark:hover:bg-ink-800 transition">
<span class="text-ink-400 mr-2">5</span> Thiết Kế Kỹ Thuật
</a>
<a href="#s6" class="nav-link block px-3 py-2 rounded-lg border border-transparent hover:bg-ink-100 dark:hover:bg-ink-800 transition">
<span class="text-ink-400 mr-2">6</span> Nghiệm Thu & UAT
</a>
<a href="#s7" class="nav-link block px-3 py-2 rounded-lg border border-transparent hover:bg-ink-100 dark:hover:bg-ink-800 transition">
<span class="text-ink-400 mr-2">7</span> Bàn Giao & Review
</a>
</nav>
<div class="text-[11px] font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400 mt-8 mb-3 px-2">Tài nguyên</div>
<div class="space-y-1 text-[12.5px] px-3 text-ink-600 dark:text-ink-300">
<div class="flex items-center gap-2"><span class="w-1.5 h-1.5 rounded-full bg-brand-500"></span> facebook-credential-architecture.md</div>
<div class="flex items-center gap-2"><span class="w-1.5 h-1.5 rounded-full bg-brand-500"></span> debug-guide.md</div>
</div>
<div class="mt-8 p-4 rounded-xl bg-gradient-to-br from-brand-50 to-white dark:from-brand-900/30 dark:to-ink-900 border border-brand-100 dark:border-brand-800">
<div class="text-[11px] uppercase tracking-wider text-brand-700 dark:text-brand-200 font-semibold mb-1">Priority</div>
<div class="text-sm font-semibold">P1 · High</div>
<div class="text-[12px] text-ink-600 dark:text-ink-300 mt-1.5">Sai trạng thái tài chính hiển thị cho người dùng + làm hỏng tính năng rủi ro <em>Funded bills on starred</em>.</div>
</div>
</div>
</aside>
<!-- ============ MAIN ARTICLE ============ -->
<article id="articleBody" class="col-span-12 lg:col-span-9 xl:col-span-7 min-w-0">
<!-- HERO -->
<div class="mb-12">
<div class="flex flex-wrap items-center gap-2 mb-4">
<span class="chip chip-danger">P1 · High</span>
<span class="chip chip-warn">Tách cột + Migration</span>
<span class="chip chip-brand">FT-BILL-FUNDED-STATUS-01</span>
<span class="chip chip-ink">Draft</span>
</div>
<h1 class="text-[34px] sm:text-[40px] leading-[1.15] font-bold tracking-tight text-ink-950 dark:text-white">
Đồng Bộ & Hiển Thị Đúng Trạng Thái <span class="text-brand-600 dark:text-brand-300">"Funded"</span> Của Hóa Đơn
</h1>
<p class="mt-4 text-[16px] text-ink-600 dark:text-ink-300 max-w-prose-65 leading-relaxed">
Bill thanh toán bằng <strong>credit / quỹ trả trước</strong> được Facebook gắn nhãn <strong>"Funded"</strong>, nhưng hệ thống đang hiển thị <strong>"Thành công"</strong> (<code>Paid</code>).
Nguyên nhân: 2 nguồn dữ liệu FB (API / cookie) trả ra 2 bộ status khác nhau nhưng đang dùng <strong>chung một cột</strong> <code>payment_status</code>, sync API ghi đè dần nhãn cookie.
Tài liệu này tách cột để 2 path ghi độc lập, reader <strong>ưu tiên cookie</strong>.
</p>
<div class="mt-6 grid grid-cols-2 sm:grid-cols-4 gap-3 text-[12.5px]">
<div class="p-3 rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<div class="text-ink-500">Owner</div><div class="font-semibold mt-0.5">CTO</div>
</div>
<div class="p-3 rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<div class="text-ink-500">Bill ví dụ</div><div class="font-semibold mt-0.5 font-mono text-[12px]">6ET7ZP9N52</div>
</div>
<div class="p-3 rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<div class="text-ink-500">Bảng dữ liệu</div><div class="font-semibold mt-0.5 font-mono text-[12px]">ad_account_bills</div>
</div>
<div class="p-3 rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<div class="text-ink-500">Row cookie cũ</div><div class="font-semibold mt-0.5">~7,354 row</div>
</div>
</div>
</div>
<!-- ============ SECTION 1 ============ -->
<section id="s1" class="mb-16">
<h2 id="s1-info" class="text-2xl font-bold tracking-tight mb-1">1. Thông Tin Chung</h2>
<p class="text-ink-500 text-sm mb-6">Metadata cơ bản, scope của RFC.</p>
<div class="rounded-2xl border border-ink-200 dark:border-ink-800 overflow-hidden bg-white dark:bg-ink-900">
<table class="doc-table">
<tbody>
<tr><td class="w-1/3 font-medium text-ink-900 dark:text-white">Mã tính năng (Feature ID)</td><td><code class="font-mono text-[13px]">FT-BILL-FUNDED-STATUS-01</code></td></tr>
<tr><td class="font-medium text-ink-900 dark:text-white">Người yêu cầu (Owner / PO)</td><td>CTO</td></tr>
<tr><td class="font-medium text-ink-900 dark:text-white">Độ ưu tiên</td><td><span class="chip chip-danger">P1 · High</span> — sai trạng thái tài chính hiển thị cho người dùng + làm hỏng tính năng rủi ro <em>Funded bills on starred</em>.</td></tr>
<tr><td class="font-medium text-ink-900 dark:text-white">Trạng thái</td><td><span class="chip chip-warn">Draft</span></td></tr>
<tr><td class="font-medium text-ink-900 dark:text-white">Ngày tạo</td><td>03/06/2026</td></tr>
<tr><td class="font-medium text-ink-900 dark:text-white">Ngày cập nhật cuối</td><td>03/06/2026</td></tr>
<tr><td class="font-medium text-ink-900 dark:text-white">Hệ thống</td><td>Dashboard thuê TKQC — module <code class="font-mono text-[12.5px]">workspaces</code> / <code class="font-mono text-[12.5px]">billing</code> / <code class="font-mono text-[12.5px]">risk</code>. Bảng chính: <code class="font-mono text-[12.5px]">ad_account_bills</code>.</td></tr>
<tr><td class="font-medium text-ink-900 dark:text-white">Quy mô</td><td>Tách trạng thái crawl về thành <strong>2 cột riêng</strong> (API FB / cookie), 2 path ghi độc lập, reader <strong>ưu tiên cột cookie</strong>. Kèm migration tách dữ liệu cột cũ.</td></tr>
</tbody>
</table>
</div>
</section>
<!-- ============ SECTION 2 ============ -->
<section id="s2" class="mb-16">
<h2 class="text-2xl font-bold tracking-tight mb-1">2. Bối Cảnh & Mục Tiêu</h2>
<p class="text-ink-500 text-sm mb-6">Hai nguồn FB cho ra 2 bộ status khác nhau, vì sao nhãn cookie đang bị xoá dần.</p>
<h3 id="s2-1" class="text-lg font-semibold mt-8 mb-3">2.1. Bối cảnh (Context)</h3>
<p class="max-w-prose-65 leading-relaxed text-ink-700 dark:text-ink-200">
Bill thanh toán bằng <strong>credit / quỹ trả trước</strong> được Facebook gắn nhãn <strong>"Funded"</strong>, nhưng hệ thống đang hiển thị <strong>"Thành công"</strong> (<code>Paid</code>). Ví dụ: bill <code>6ET7ZP9N52</code> (TKQC <code>1683821496292014</code>) — billing_hub hiển thị <strong>Funded</strong>, DB lưu <code>payment_status='Paid'</code>.
</p>
<p class="max-w-prose-65 leading-relaxed text-ink-700 dark:text-ink-200 mt-4">
<strong>Nguyên nhân — 2 nguồn dữ liệu FB cho ra 2 bộ status khác nhau:</strong>
</p>
<h4 class="text-[15px] font-semibold mt-8 mb-3 text-ink-900 dark:text-white">Hai nguồn crawl trả 2 bộ status khác nhau</h4>
<div class="rounded-2xl border border-ink-200 dark:border-ink-800 overflow-hidden bg-white dark:bg-ink-900">
<table class="doc-table">
<thead><tr><th>Nguồn crawl</th><th>Trả status</th><th>Phân biệt được Funded?</th></tr></thead>
<tbody>
<tr>
<td class="font-semibold">API FB (<code>GET /act_{id}/transactions</code>)</td>
<td><code>COMPLETED</code> / <code>FAILED</code> / <code>PROCESSING</code></td>
<td>Không — Funded vẫn = <code>COMPLETED</code></td>
</tr>
<tr>
<td class="font-semibold">Cookie (crawl <code>billing_hub.php</code>)</td>
<td><code>Paid</code> / <code>Funded</code> / <code>Pending</code> / <code>Failed</code> + nhãn tiếng Việt</td>
<td>Có</td>
</tr>
</tbody>
</table>
</div>
<p class="max-w-prose-65 leading-relaxed text-ink-700 dark:text-ink-200 mt-6">
Sync hiện tại <strong>chỉ dùng API FB</strong>, map <code>COMPLETED → Paid</code> / còn lại <code>→ Unpaid</code> nên <strong>không bao giờ tạo ra <code>Funded</code></strong> → re-sync vô ích vì input API không hề có thông tin Funded.
</p>
<div class="callout callout-danger mt-6">
<svg class="w-5 h-5 mt-0.5 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 9v4M12 17h.01M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/></svg>
<div>
<strong class="block mb-1">Tệ hơn — bom hẹn giờ</strong>
DB hiện còn <strong>~7.354 row</strong> nhãn từ crawl cookie cũ (gồm <code>Funded</code> 4 row, <code>Đã thanh toán</code>, <code>Đang chờ</code>, <code>Pending</code>, <code>Không thành công</code>, <code>Failed</code>) <strong>nằm lẫn cùng một cột <code>payment_status</code></strong> với kết quả API. Mỗi lần sync API ghi đè chính cột đó → <strong>đang xoá dần nhãn cookie</strong>.
</div>
</div>
<h3 id="s2-2" class="text-lg font-semibold mt-12 mb-3">2.2. Mục tiêu (Objectives)</h3>
<div class="space-y-3">
<div class="flex gap-3 p-4 rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<div class="w-7 h-7 rounded-lg bg-brand-100 dark:bg-brand-900 text-brand-700 dark:text-brand-200 grid place-items-center font-semibold shrink-0">O1</div>
<div class="text-[14px] leading-relaxed">Bill Funded hiển thị đúng <strong>"Funded"</strong> trên <code>/dashboard/billing</code>, <code>/finance/bills</code>, <code>/admin/risk/funded-bills-on-starred</code>.</div>
</div>
<div class="flex gap-3 p-4 rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<div class="w-7 h-7 rounded-lg bg-brand-100 dark:bg-brand-900 text-brand-700 dark:text-brand-200 grid place-items-center font-semibold shrink-0">O2</div>
<div class="text-[14px] leading-relaxed">Khôi phục độ chính xác tính năng <strong>Funded bills on starred</strong> (đã announce ở changelog v3.62.0).</div>
</div>
<div class="flex gap-3 p-4 rounded-xl border border-brand-200 dark:border-brand-800 bg-brand-50/40 dark:bg-brand-900/20">
<div class="w-7 h-7 rounded-lg bg-brand-600 text-white grid place-items-center font-semibold shrink-0">O3</div>
<div class="text-[14px] leading-relaxed">2 nguồn crawl (API / cookie) <strong>không bao giờ ghi đè nhau</strong> → không mất dữ liệu.</div>
</div>
</div>
</section>
<!-- ============ SECTION 3 ============ -->
<section id="s3" class="mb-16">
<h2 class="text-2xl font-bold tracking-tight mb-1">3. Luồng Nghiệp Vụ & Sơ Đồ Hoạt Động</h2>
<p class="text-ink-500 text-sm mb-6">Actor và các edge cases khi triển khai.</p>
<h3 id="s3-1" class="text-lg font-semibold mt-6 mb-3">3.1. Các đối tượng tham gia</h3>
<div class="grid sm:grid-cols-2 gap-3">
<div class="p-4 rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<div class="flex items-center gap-2 mb-1">
<span class="w-2 h-2 rounded-full bg-info"></span>
<div class="font-semibold">Người dùng workspace</div>
</div>
<div class="text-[13px] text-ink-600 dark:text-ink-300">Bấm "Đồng bộ hóa đơn", xem trạng thái bill.</div>
</div>
<div class="p-4 rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<div class="flex items-center gap-2 mb-1">
<span class="w-2 h-2 rounded-full bg-accent-500"></span>
<div class="font-semibold">Super Admin</div>
</div>
<div class="text-[13px] text-ink-600 dark:text-ink-300">Soát trang <em>Funded bills on starred</em> (TKQC gắn sao có bill Funded = bất thường tài chính).</div>
</div>
<div class="p-4 rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<div class="flex items-center gap-2 mb-1">
<span class="w-2 h-2 rounded-full bg-ok"></span>
<div class="font-semibold">Sync API (writer 1) & Crawl cookie (writer 2)</div>
</div>
<div class="text-[13px] text-ink-600 dark:text-ink-300">Mỗi bên ghi 1 cột riêng — không bao giờ đụng cột của nhau.</div>
</div>
<div class="p-4 rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<div class="flex items-center gap-2 mb-1">
<span class="w-2 h-2 rounded-full bg-brand-500"></span>
<div class="font-semibold">Reader</div>
</div>
<div class="text-[13px] text-ink-600 dark:text-ink-300">Funded feature / dashboard billing / billing filter — đọc trạng thái ưu tiên cookie.</div>
</div>
</div>
<h3 id="s3-2" class="text-lg font-semibold mt-10 mb-3">3.2. Xử lý các kịch bản lỗi (Edge cases)</h3>
<div class="callout callout-warn mb-5">
<svg class="w-5 h-5 mt-0.5 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 8v4M12 16h.01"/><circle cx="12" cy="12" r="10"/></svg>
<div>Dev tự tư duy & bổ sung thêm khi triển khai.</div>
</div>
<div class="space-y-3">
<details class="group rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900 overflow-hidden">
<summary class="cursor-pointer px-4 py-3 flex items-center gap-3 hover:bg-ink-50 dark:hover:bg-ink-800 transition list-none">
<span class="chip chip-danger">EC-01</span>
<span class="font-medium flex-1">Cookie / token chết</span>
<span class="text-ink-400 group-open:rotate-180 transition">▾</span>
</summary>
<div class="px-4 pb-4 pt-1 text-[13.5px] text-ink-600 dark:text-ink-300 leading-relaxed">
TKQC <code>1683821496292014</code> đang <code>disabled</code>, token đã chết (test Graph API trực tiếp → fail). Crawl cookie phải dùng <strong>cookie + proxy của VIA còn quyền</strong> trên BM, không phụ thuộc token TKQC.
</div>
</details>
<details class="group rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900 overflow-hidden">
<summary class="cursor-pointer px-4 py-3 flex items-center gap-3 hover:bg-ink-50 dark:hover:bg-ink-800 transition list-none">
<span class="chip chip-danger">EC-02</span>
<span class="font-medium flex-1">billing_hub đổi cấu trúc HTML</span>
<span class="text-ink-400 group-open:rotate-180 transition">▾</span>
</summary>
<div class="px-4 pb-4 pt-1 text-[13.5px] text-ink-600 dark:text-ink-300 leading-relaxed">
Parser dễ vỡ → log lỗi rõ + cảnh báo về <strong>NolimitHub</strong>.
</div>
</details>
<details class="group rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900 overflow-hidden">
<summary class="cursor-pointer px-4 py-3 flex items-center gap-3 hover:bg-ink-50 dark:hover:bg-ink-800 transition list-none">
<span class="chip chip-warn">EC-03</span>
<span class="font-medium flex-1">Thống nhất nhãn</span>
<span class="text-ink-400 group-open:rotate-180 transition">▾</span>
</summary>
<div class="px-4 pb-4 pt-1 text-[13.5px] text-ink-600 dark:text-ink-300 leading-relaxed">
Hai cột cần map nhãn FB về <strong>chung 1 bộ canonical</strong> (tránh đẻ biến thể), nhưng giữ độc lập theo nguồn.
</div>
</details>
<details class="group rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900 overflow-hidden">
<summary class="cursor-pointer px-4 py-3 flex items-center gap-3 hover:bg-ink-50 dark:hover:bg-ink-800 transition list-none">
<span class="chip chip-warn">EC-04</span>
<span class="font-medium flex-1">Lệch timezone / tiền tệ</span>
<span class="text-ink-400 group-open:rotate-180 transition">▾</span>
</summary>
<div class="px-4 pb-4 pt-1 text-[13.5px] text-ink-600 dark:text-ink-300 leading-relaxed">
Khi đối chiếu 2 nguồn theo <code>tracking_id</code>.
</div>
</details>
</div>
</section>
<!-- ============ SECTION 4 ============ -->
<section id="s4" class="mb-16">
<h2 class="text-2xl font-bold tracking-tight mb-1">4. Chi Tiết Yêu Cầu Chức Năng</h2>
<p class="text-ink-500 text-sm mb-6">User stories & business rules.</p>
<h3 id="s4-1" class="text-lg font-semibold mt-6 mb-4">4.1. Danh sách chức năng (User Stories)</h3>
<div class="rounded-2xl border border-ink-200 dark:border-ink-800 overflow-hidden bg-white dark:bg-ink-900">
<table class="doc-table">
<thead>
<tr><th class="w-16">ID</th><th class="w-44">Vai trò</th><th>Muốn</th><th>Để</th><th class="w-20">Ưu tiên</th></tr>
</thead>
<tbody>
<tr>
<td><code>US-01</code></td>
<td>Người dùng workspace</td>
<td>Bill trả bằng credit hiện đúng "Funded"</td>
<td>Biết đúng bản chất giao dịch</td>
<td><span class="chip chip-danger">P1</span></td>
</tr>
<tr>
<td><code>US-02</code></td>
<td>Super Admin</td>
<td>Trang <em>Funded bills on starred</em> đếm / liệt kê đúng</td>
<td>Soát đúng rủi ro tài chính</td>
<td><span class="chip chip-danger">P1</span></td>
</tr>
<tr>
<td><code>US-03</code></td>
<td>Hệ thống</td>
<td>Crawl API & cookie không ghi đè nhau</td>
<td>Không mất dữ liệu khi sync</td>
<td><span class="chip chip-danger">P0</span></td>
</tr>
</tbody>
</table>
</div>
<h3 id="s4-2" class="text-lg font-semibold mt-10 mb-4">4.2. Quy tắc nghiệp vụ (Business Rules)</h3>
<div class="space-y-4">
<div class="p-5 rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<div class="flex items-center gap-2 mb-2">
<span class="chip chip-brand">BR-01</span>
<strong>Tách nguồn — tách cột</strong>
</div>
<p class="text-[14px] leading-relaxed text-ink-700 dark:text-ink-200">
Trạng thái từ <strong>API FB</strong> và từ <strong>cookie</strong> lưu vào <strong>2 cột riêng</strong>; mỗi path <strong>chỉ ghi cột của mình</strong>, không đụng cột còn lại.
</p>
</div>
<div class="p-5 rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<div class="flex items-center gap-2 mb-2">
<span class="chip chip-brand">BR-02</span>
<strong>Reader ưu tiên cookie</strong>
</div>
<p class="text-[14px] leading-relaxed text-ink-700 dark:text-ink-200">
Giá trị hiển thị = cột cookie nếu có, không thì rơi về cột API (<code>cookie ?? api</code>). Funded chỉ tồn tại ở cột cookie.
</p>
</div>
<div class="p-5 rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<div class="flex items-center gap-2 mb-2">
<span class="chip chip-brand">BR-03</span>
<strong>Canonical chung</strong>
</div>
<p class="text-[14px] leading-relaxed text-ink-700 dark:text-ink-200">
Hai cột dùng chung bộ giá trị (<code>Paid</code>, <code>Unpaid</code>, <code>Funded</code>, <code>Pending</code>, <code>Failed</code> + nhãn tiếng Việt tương ứng); không phát sinh biến thể mới.
</p>
</div>
</div>
</section>
<!-- ============ SECTION 5 ============ -->
<section id="s5" class="mb-16">
<h2 class="text-2xl font-bold tracking-tight mb-1">5. Thiết Kế Kỹ Thuật Sơ Bộ</h2>
<p class="text-ink-500 text-sm mb-6">Database 2 cột, writer 2 path, reader ưu tiên cookie, tài liệu tham chiếu.</p>
<div class="callout callout-warn mb-8">
<svg class="w-5 h-5 mt-0.5 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 8v4M12 16h.01"/><circle cx="12" cy="12" r="10"/></svg>
<div>
Path <code>app/...</code> nằm ở <strong>repo product</strong> (không phải repo tài liệu này); số dòng gần đúng, dev mở repo code đối chiếu.
</div>
</div>
<h3 id="s5-1" class="text-lg font-semibold mb-3">5.1. Database — thêm 2 cột trạng thái</h3>
<div class="rounded-2xl border border-ink-200 dark:border-ink-800 overflow-hidden bg-white dark:bg-ink-900 mb-5">
<table class="doc-table">
<thead><tr><th>Cột</th><th>Type</th><th>Mục đích</th></tr></thead>
<tbody>
<tr><td><code>payment_status_api</code></td><td>text</td><td>Kết quả map từ API FB (<code>Paid</code> / <code>Unpaid</code>).</td></tr>
<tr><td><code>payment_status_cookie</code></td><td>text</td><td>Nhãn crawl từ billing_hub qua cookie (<code>Funded</code> / <code>Paid</code> / <code>Pending</code> / <code>Failed</code> / nhãn tiếng Việt).</td></tr>
</tbody>
</table>
</div>
<p class="text-[14px] leading-relaxed text-ink-700 dark:text-ink-200">
<strong>Giá trị hiển thị</strong> = <code>COALESCE(payment_status_cookie, payment_status_api)</code>. Gợi ý: tạo cột generated / <code>view</code> hoặc xử lý ở query để <strong>reader đọc 1 chỗ</strong>, ưu tiên cookie. (Dev tự chọn cách tối ưu.)
</p>
<h4 class="text-[15px] font-semibold mt-6 mb-3 text-ink-900 dark:text-white">Migration cột <code>payment_status</code> cũ (idempotent)</h4>
<ul class="space-y-1.5 list-disc pl-5 text-[13.5px] text-ink-700 dark:text-ink-200">
<li>Giá trị <code>Paid</code> / <code>Unpaid</code> (do API sinh) → đổ sang <code>payment_status_api</code>.</li>
<li>Các nhãn còn lại (<code>Funded</code>, <code>Đã thanh toán</code>, <code>Đang chờ</code>, <code>Pending</code>, <code>Không thành công</code>, <code>Failed</code>) là crawl cookie cũ → đổ sang <code>payment_status_cookie</code>.</li>
</ul>
<h3 id="s5-2" class="text-lg font-semibold mt-12 mb-3">5.2. Writer — 2 path ghi độc lập</h3>
<div class="space-y-4">
<div class="p-5 rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<div class="flex items-center gap-2 mb-2">
<span class="chip chip-brand">Writer 1</span>
<strong>Sync API</strong>
</div>
<p class="text-[14px] leading-relaxed text-ink-700 dark:text-ink-200">
<code class="font-mono text-[12.5px]">app/api/workspaces/[workspaceId]/ad-accounts/[adAccountId]/check-bill/route.ts</code>, ≈ dòng 240–262: đổi để ghi vào <strong><code>payment_status_api</code></strong>, <strong>không</strong> đụng <code>payment_status_cookie</code>. → diệt luôn bug ghi đè.
</p>
</div>
<div class="p-5 rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<div class="flex items-center gap-2 mb-2">
<span class="chip chip-brand">Writer 2</span>
<strong>Crawl cookie</strong> (billing_hub)
</div>
<p class="text-[14px] leading-relaxed text-ink-700 dark:text-ink-200">
Mở <code class="font-mono text-[12px]">https://business.facebook.com/billing_hub.php?asset_id=act_{id}</code> bằng cookie + proxy của VIA còn quyền, parse HTML lấy nhãn theo <code>tracking_id</code>, ghi vào <strong><code>payment_status_cookie</code></strong>. (Nếu path này chưa có, dev dựng theo pattern các sync cookie khác — xem 5.4.)
</p>
</div>
</div>
<div class="callout callout-warn mt-5">
<svg class="w-5 h-5 mt-0.5 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 8v4M12 16h.01"/><circle cx="12" cy="12" r="10"/></svg>
<div>
<strong>Khoá row (dev xác minh):</strong> UPSERT đang dùng khoá <code>transaction_id</code>, còn billing_hub trả nhãn theo <code>tracking_id</code> → cần xác minh quan hệ <code>tracking_id</code> ↔ <code>transaction_id</code> trên <code>ad_account_bills</code> để ghi nhãn cookie vào đúng row.
</div>
</div>
<h3 id="s5-3" class="text-lg font-semibold mt-12 mb-3">5.3. Reader — đọc ưu tiên cookie</h3>
<p class="text-[14px] text-ink-700 dark:text-ink-200 mb-4">
Các điểm sau hiện query <code>payment_status='Funded'</code>, đổi sang đọc giá trị ưu tiên (cookie-first):
</p>
<div class="rounded-2xl border border-ink-200 dark:border-ink-800 overflow-hidden bg-white dark:bg-ink-900">
<table class="doc-table">
<thead><tr><th>Nơi đọc</th><th>File (trong repo product)</th></tr></thead>
<tbody>
<tr><td>Funded bills aggregation & detail</td><td class="font-mono text-[12px]">risk/funded-bills-on-starred/route.ts (~90) & .../[adAccountId]/route.ts (~54)</td></tr>
<tr><td>Dashboard billing panel</td><td class="font-mono text-[12px]">dashboard/billing/route.ts (~163)</td></tr>
<tr><td>Billing page filter trạng thái</td><td class="font-mono text-[12px]">billing/route.ts (~24–27)</td></tr>
<tr><td>UI page risk</td><td class="font-mono text-[12px]">app/(dashboard)/w/[workspaceId]/admin/risk/funded-bills-on-starred/page.tsx</td></tr>
</tbody>
</table>
</div>
<h3 id="s5-4" class="text-lg font-semibold mt-12 mb-3">5.4. Tài liệu tham chiếu (repo product)</h3>
<div class="space-y-2">
<div class="flex items-start gap-3 p-3 rounded-lg border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<svg class="w-4 h-4 mt-1 text-brand-500 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
<div class="text-[13.5px]">Cookie / EAAB / EAAG cho crawl cookie: <code class="font-mono text-[12px]">docs/FORAI/facebook-credential-architecture.md</code></div>
</div>
<div class="flex items-start gap-3 p-3 rounded-lg border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<svg class="w-4 h-4 mt-1 text-brand-500 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
<div class="text-[13.5px]">Decrypt token + gọi Graph API trực tiếp (đã fail với TKQC token chết → cần cookie / proxy): <code class="font-mono text-[12px]">docs/FORAI/debug-guide.md</code></div>
</div>
</div>
</section>
<!-- ============ SECTION 6 ============ -->
<section id="s6" class="mb-16">
<h2 class="text-2xl font-bold tracking-tight mb-1">6. Tiêu Chí Nghiệm Thu & Kịch Bản Kiểm Thử (UAT)</h2>
<p class="text-ink-500 text-sm mb-6">Acceptance criteria + test cases.</p>
<h3 id="s6-1" class="text-lg font-semibold mt-6 mb-4">6.1. Tiêu chí nghiệm thu (Acceptance Criteria)</h3>
<div class="space-y-2.5">
<label class="flex items-start gap-3 p-3.5 rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<input type="checkbox" class="mt-1 w-4 h-4 rounded accent-brand-600">
<span class="text-[14px] leading-relaxed">Bill <code>6ET7ZP9N52</code> (và các bill Funded khác) hiển thị đúng <strong>"Funded"</strong> (lấy từ cột cookie).</span>
</label>
<label class="flex items-start gap-3 p-3.5 rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<input type="checkbox" class="mt-1 w-4 h-4 rounded accent-brand-600">
<span class="text-[14px] leading-relaxed">Trang <em>Funded bills on starred</em> đếm đúng = các row có <code>payment_status_cookie='Funded'</code>, đối chiếu khớp billing_hub.</span>
</label>
<label class="flex items-start gap-3 p-3.5 rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<input type="checkbox" class="mt-1 w-4 h-4 rounded accent-brand-600">
<span class="text-[14px] leading-relaxed"><strong>Sync API chạy lại KHÔNG làm đổi <code>payment_status_cookie</code></strong> (2 cột độc lập).</span>
</label>
<label class="flex items-start gap-3 p-3.5 rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<input type="checkbox" class="mt-1 w-4 h-4 rounded accent-brand-600">
<span class="text-[14px] leading-relaxed">Reader đọc đúng theo ưu tiên cookie (<code>cookie ?? api</code>); không phát sinh biến thể nhãn mới.</span>
</label>
<label class="flex items-start gap-3 p-3.5 rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<input type="checkbox" class="mt-1 w-4 h-4 rounded accent-brand-600">
<span class="text-[14px] leading-relaxed">Migration tách đúng dữ liệu cột cũ sang 2 cột (idempotent, chạy lại không sai lệch).</span>
</label>
<label class="flex items-start gap-3 p-3.5 rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<input type="checkbox" class="mt-1 w-4 h-4 rounded accent-brand-600">
<span class="text-[14px] leading-relaxed">Crawl cookie lỗi (cookie chết / HTML đổi) → không crash, log rõ + cảnh báo <strong>NolimitHub</strong>.</span>
</label>
</div>
<h3 id="s6-2" class="text-lg font-semibold mt-12 mb-4">6.2. Kịch bản kiểm thử (Test Cases)</h3>
<div class="space-y-5">
<div class="rounded-2xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900 overflow-hidden">
<div class="px-5 py-3 border-b border-ink-100 dark:border-ink-800 flex items-center gap-2 bg-brand-50/50 dark:bg-brand-900/20">
<div class="w-7 h-7 rounded-md bg-brand-600 text-white grid place-items-center font-bold text-xs">R</div>
<strong class="text-[14px]">Reader ưu tiên cookie</strong>
</div>
<div class="p-5 text-[13.5px] leading-relaxed text-ink-700 dark:text-ink-200 space-y-3">
<div class="flex items-start gap-3"><span class="chip chip-brand shrink-0">TC-01</span><div>Row có <code>cookie=Funded</code>, <code>api=Paid</code> → reader hiển thị <strong>Funded</strong>.</div></div>
<div class="flex items-start gap-3"><span class="chip chip-brand shrink-0">TC-02</span><div>Row chỉ có <code>api=Paid</code> (chưa crawl cookie) → reader hiển thị <strong>Paid</strong> (fallback).</div></div>
</div>
</div>
<div class="rounded-2xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900 overflow-hidden">
<div class="px-5 py-3 border-b border-ink-100 dark:border-ink-800 flex items-center gap-2 bg-accent-500/10">
<div class="w-7 h-7 rounded-md bg-accent-500 text-white grid place-items-center font-bold text-xs">W</div>
<strong class="text-[14px]">Writer độc lập</strong>
</div>
<div class="p-5 text-[13.5px] leading-relaxed text-ink-700 dark:text-ink-200 space-y-3">
<div class="flex items-start gap-3"><span class="chip chip-warn shrink-0">TC-03</span><div>Trước có <code>cookie=Funded</code> → chạy sync API → <code>payment_status_cookie</code> <strong>giữ nguyên</strong>, chỉ <code>payment_status_api</code> cập nhật.</div></div>
<div class="flex items-start gap-3"><span class="chip chip-warn shrink-0">TC-04</span><div>Crawl cookie TKQC chứa <code>6ET7ZP9N52</code> → <code>payment_status_cookie='Funded'</code> → trang Risk hiển thị đúng.</div></div>
</div>
</div>
<div class="rounded-2xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900 overflow-hidden">
<div class="px-5 py-3 border-b border-ink-100 dark:border-ink-800 flex items-center gap-2 bg-ink-100/60 dark:bg-ink-800/40">
<div class="w-7 h-7 rounded-md bg-ink-600 text-white grid place-items-center font-bold text-xs">M</div>
<strong class="text-[14px]">Migration</strong>
</div>
<div class="p-5 text-[13.5px] leading-relaxed text-ink-700 dark:text-ink-200">
<div class="flex items-start gap-3"><span class="chip chip-ink shrink-0">TC-05</span><div>Chạy migration 2 lần → kết quả không đổi (idempotent).</div></div>
</div>
</div>
<div class="callout callout-info">
<svg class="w-5 h-5 mt-0.5 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>
<div><strong>TC bổ sung:</strong> dev tự dựng test cho lệch timezone / tiền tệ, cookie chết, HTML billing_hub đổi cấu trúc.</div>
</div>
</div>
</section>
<!-- ============ SECTION 7 ============ -->
<section id="s7" class="mb-12">
<h2 class="text-2xl font-bold tracking-tight mb-1">7. Quy Trình Bàn Giao & Review (CTO Handover)</h2>
<p class="text-ink-500 text-sm mb-8">Các bước chốt & deploy.</p>
<ol class="relative border-l-2 border-ink-200 dark:border-ink-800 ml-3 space-y-6">
<li class="ml-6">
<span class="absolute -left-[11px] w-5 h-5 rounded-full bg-brand-600 ring-4 ring-white dark:ring-ink-950"></span>
<div class="p-5 rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<div class="flex items-center gap-2 mb-2"><span class="chip chip-brand">Decision</span><strong>Quyết định cần chốt</strong></div>
<p class="text-[14px] leading-relaxed text-ink-700 dark:text-ink-200">Bill <code>6ET7ZP9N52</code> — chờ crawl cookie hay update tay <code>payment_status_cookie='Funded'</code> để hiển thị đúng ngay.</p>
</div>
</li>
<li class="ml-6">
<span class="absolute -left-[11px] w-5 h-5 rounded-full bg-ok ring-4 ring-white dark:ring-ink-950"></span>
<div class="p-5 rounded-xl border border-ok/30 bg-ok/5">
<div class="flex items-center gap-2 mb-2"><span class="chip chip-ok">Bàn giao</span><strong>Test Report + Demo + Approve</strong></div>
<p class="text-[14px] leading-relaxed text-ink-700 dark:text-ink-200">Sau khi xong + UAT trên staging, dev chuẩn bị <strong>Test Report</strong> kèm bằng chứng (ảnh / video: <code>6ET7ZP9N52</code> hiện đúng Funded; demo sync API không làm mất nhãn cookie). Họp review trực tiếp với CTO duyệt cơ chế 2 cột + ưu tiên cookie + migration. <strong>Chỉ khi CTO Approve mới deploy Production.</strong></p>
</div>
</li>
</ol>
<div class="mt-12 p-6 rounded-2xl border border-ink-200 dark:border-ink-800 bg-gradient-to-br from-brand-50 to-white dark:from-brand-900/30 dark:to-ink-900">
<div class="text-[12px] font-semibold uppercase tracking-wider text-brand-700 dark:text-brand-200 mb-2">End of document</div>
<div class="text-[14px] text-ink-700 dark:text-ink-200">FT-BILL-FUNDED-STATUS-01 · Draft · Last reviewed 2026-06-03 · Owner: CTO</div>
<div class="text-[12.5px] text-ink-500 dark:text-ink-400 mt-2">Tách trạng thái crawl thành <strong>2 cột riêng</strong> (API FB / cookie), 2 path ghi độc lập, reader <strong>ưu tiên cột cookie</strong>. Kèm migration tách dữ liệu cột cũ.</div>
</div>
</section>
</article>
<!-- ============ RIGHT TOC ============ -->
<aside class="hidden xl:block xl:col-span-3">
<div class="sticky top-20">
<div class="text-[11px] font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400 mb-3">On this page</div>
<nav id="toc" class="space-y-1 text-[12.5px] border-l border-ink-200 dark:border-ink-800">
<a href="#s1" class="toc-link block pl-4 py-1.5 border-l-2 border-transparent text-ink-600 dark:text-ink-300 hover:text-ink-900 dark:hover:text-white transition">1. Thông Tin Chung</a>
<a href="#s2" class="toc-link block pl-4 py-1.5 border-l-2 border-transparent text-ink-600 dark:text-ink-300 hover:text-ink-900 dark:hover:text-white transition">2. Bối Cảnh & Mục Tiêu</a>
<a href="#s2-1" class="toc-link block pl-7 py-1 border-l-2 border-transparent text-ink-500 hover:text-ink-900 dark:hover:text-white transition">2.1 Bối cảnh</a>
<a href="#s2-2" class="toc-link block pl-7 py-1 border-l-2 border-transparent text-ink-500 hover:text-ink-900 dark:hover:text-white transition">2.2 Mục tiêu</a>
<a href="#s3" class="toc-link block pl-4 py-1.5 border-l-2 border-transparent text-ink-600 dark:text-ink-300 hover:text-ink-900 dark:hover:text-white transition">3. Luồng Nghiệp Vụ</a>
<a href="#s3-1" class="toc-link block pl-7 py-1 border-l-2 border-transparent text-ink-500 hover:text-ink-900 dark:hover:text-white transition">3.1 Đối tượng tham gia</a>
<a href="#s3-2" class="toc-link block pl-7 py-1 border-l-2 border-transparent text-ink-500 hover:text-ink-900 dark:hover:text-white transition">3.2 Edge cases</a>
<a href="#s4" class="toc-link block pl-4 py-1.5 border-l-2 border-transparent text-ink-600 dark:text-ink-300 hover:text-ink-900 dark:hover:text-white transition">4. Yêu Cầu Chức Năng</a>
<a href="#s4-1" class="toc-link block pl-7 py-1 border-l-2 border-transparent text-ink-500 hover:text-ink-900 dark:hover:text-white transition">4.1 User stories</a>
<a href="#s4-2" class="toc-link block pl-7 py-1 border-l-2 border-transparent text-ink-500 hover:text-ink-900 dark:hover:text-white transition">4.2 Business rules</a>
<a href="#s5" class="toc-link block pl-4 py-1.5 border-l-2 border-transparent text-ink-600 dark:text-ink-300 hover:text-ink-900 dark:hover:text-white transition">5. Thiết Kế Kỹ Thuật</a>
<a href="#s5-1" class="toc-link block pl-7 py-1 border-l-2 border-transparent text-ink-500 hover:text-ink-900 dark:hover:text-white transition">5.1 Database 2 cột</a>
<a href="#s5-2" class="toc-link block pl-7 py-1 border-l-2 border-transparent text-ink-500 hover:text-ink-900 dark:hover:text-white transition">5.2 Writer 2 path</a>
<a href="#s5-3" class="toc-link block pl-7 py-1 border-l-2 border-transparent text-ink-500 hover:text-ink-900 dark:hover:text-white transition">5.3 Reader ưu tiên cookie</a>
<a href="#s5-4" class="toc-link block pl-7 py-1 border-l-2 border-transparent text-ink-500 hover:text-ink-900 dark:hover:text-white transition">5.4 Tài liệu tham chiếu</a>
<a href="#s6" class="toc-link block pl-4 py-1.5 border-l-2 border-transparent text-ink-600 dark:text-ink-300 hover:text-ink-900 dark:hover:text-white transition">6. Nghiệm Thu & UAT</a>
<a href="#s6-1" class="toc-link block pl-7 py-1 border-l-2 border-transparent text-ink-500 hover:text-ink-900 dark:hover:text-white transition">6.1 Acceptance criteria</a>
<a href="#s6-2" class="toc-link block pl-7 py-1 border-l-2 border-transparent text-ink-500 hover:text-ink-900 dark:hover:text-white transition">6.2 Test cases</a>
<a href="#s7" class="toc-link block pl-4 py-1.5 border-l-2 border-transparent text-ink-600 dark:text-ink-300 hover:text-ink-900 dark:hover:text-white transition">7. Bàn Giao & Review</a>
</nav>
<div class="mt-8 p-4 rounded-xl border border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<div class="text-[11px] font-semibold uppercase tracking-wider text-ink-500 dark:text-ink-400 mb-2">Quick refs</div>
<div class="space-y-1.5 text-[12px] text-ink-600 dark:text-ink-300">
<div><strong>Bill ví dụ:</strong> <code class="font-mono">6ET7ZP9N52</code></div>
<div><strong>TKQC:</strong> <code class="font-mono">1683821496292014</code></div>
<div><strong>Row cookie cũ:</strong> ~7,354</div>
<div><strong>Funded rows:</strong> 4</div>
<div><strong>Changelog ref:</strong> v3.62.0</div>
</div>
</div>
</div>
</aside>
</div>
<!-- ============ FOOTER ============ -->
<footer class="border-t border-ink-200 dark:border-ink-800 bg-white dark:bg-ink-900">
<div class="max-w-[1440px] mx-auto px-6 py-6 flex flex-wrap items-center justify-between gap-3 text-[12px] text-ink-500">
<div>© 2026 NolimitHub — Engineering Docs</div>
<div class="flex items-center gap-4">
<span>Last updated 2026-06-03</span>
<span>·</span>
<span>FT-BILL-FUNDED-STATUS-01</span>
</div>
</div>
</footer>
<script>
// ===== Theme toggle =====
const root = document.documentElement;
const stored = localStorage.getItem('theme');
if (stored === 'dark' || (!stored && window.matchMedia('(prefers-color-scheme: dark)').matches)) root.classList.add('dark');
document.getElementById('themeToggle').addEventListener('click', () => {
root.classList.toggle('dark');
localStorage.setItem('theme', root.classList.contains('dark') ? 'dark' : 'light');
if (window.mermaid) initMermaid();
});
// ===== Mermaid =====
function initMermaid() {
const dark = root.classList.contains('dark');
mermaid.initialize({
startOnLoad: false,
theme: dark ? 'dark' : 'default',
themeVariables: {
fontFamily: 'Inter, Noto Sans SC, sans-serif',
primaryColor: dark ? '#1c2870' : '#eef4ff',
primaryTextColor: dark ? '#fafafa' : '#171717',
primaryBorderColor: dark ? '#1f31b3' : '#bfd1ff',
lineColor: dark ? '#737373' : '#a3a3a3',
}
});
document.querySelectorAll('.mermaid').forEach(el => {
if (!el.dataset.original) el.dataset.original = el.textContent;
el.removeAttribute('data-processed');
el.innerHTML = el.dataset.original;
});
mermaid.run({ querySelector: '.mermaid' });
}
window.addEventListener('load', initMermaid);
// ===== Copy buttons =====
document.querySelectorAll('[data-copy]').forEach(btn => {
btn.addEventListener('click', () => {
const code = btn.parentElement.innerText.replace(/^Copy\s*/, '').trim();
navigator.clipboard.writeText(code).then(() => {
btn.textContent = 'Copied!';
btn.classList.add('copied');
setTimeout(() => { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 1400);
});
});
});
// ===== Scroll spy =====
const navLinks = document.querySelectorAll('#sectionNav .nav-link');
const tocLinks = document.querySelectorAll('#toc .toc-link');
const sections = Array.from(document.querySelectorAll('section[id], h3[id], h2[id]'));
function spy() {
const y = window.scrollY + 120;
let topId = null;
sections.forEach(s => {
if (s.offsetTop <= y) topId = s.id;
});
if (!topId) return;
// top-level section id (s1, s2, …)
const sec = topId.match(/^s\d+/)?.[0] || topId;
navLinks.forEach(l => l.classList.toggle('active', l.getAttribute('href') === '#' + sec));
tocLinks.forEach(l => l.classList.toggle('active', l.getAttribute('href') === '#' + topId));
}
window.addEventListener('scroll', spy, { passive: true });
spy();
// ===== Search (highlight + scroll to first match) =====
const search = document.getElementById('searchInput');
const article = document.getElementById('articleBody');
let originalHTML = null;
search?.addEventListener('input', e => {
const q = e.target.value.trim();
if (originalHTML === null) originalHTML = article.innerHTML;
if (!q) { article.innerHTML = originalHTML; originalHTML = null; return; }
if (q.length < 2) return;
article.innerHTML = originalHTML;
const walker = document.createTreeWalker(article, NodeFilter.SHOW_TEXT, null);
const re = new RegExp(q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
const nodes = [];
let n; while (n = walker.nextNode()) if (re.test(n.nodeValue)) nodes.push(n);
nodes.forEach(node => {
const span = document.createElement('span');
span.innerHTML = node.nodeValue.replace(re, m => `<mark class="hl">${m}</mark>`);
node.parentNode.replaceChild(span, node);
});
const first = article.querySelector('mark.hl');
if (first) first.scrollIntoView({ behavior:'smooth', block:'center' });
});
// "/" focuses search
document.addEventListener('keydown', e => {
if (e.key === '/' && document.activeElement !== search) {
e.preventDefault(); search?.focus();
}
});
</script>
</body>
</html>
Số dòng: 801