// Represents: nothing — /payments/development is prototype-only
/* Traced Aug 2026 (architecture item 2), and the trace is deliberately empty.
   AdminPaymentsController's route constraint lists ten sections and
   `development` is not among them: this is a CTO briefing document the
   prototype adds, not a screen the real platform has. Same for
   /payments/frontend (Frontend.jsx). Recorded so nobody looks for a
   controller that was never there. */
/* NO DATA: this page is PROSE. It is a CTO briefing document — phase-organised
   task cards, each written as Title · Why this matters · Key terms · Acceptance
   criteria · Inputs · How it works. There is no record behind any of it and
   there is no query that would produce one; the text IS the deliverable. It is
   not a screen with hardcoded rows, and giving it a feed would mean inventing a
   table to hold a briefing.

   AdminPaymentsController's route constraint lists ten sections and
   `development` is not among them: the real platform has no such screen. */
/* Development — CTO briefing.
   Long-form, phase-organised spec of every shipped feature in PayBO.
   Every task is written as: Title · Why this matters · Key terms ·
   Acceptance criteria · Inputs · How it works (step by step). Stack
   choices are intentionally not prescribed — the implementing team
   owns those calls. */

/* ------------ Shared building blocks ------------ */

const DV_SECTION_HEAD = ({ kicker, title, subtitle }) => (
  <div style={{marginTop:28, marginBottom:14}}>
    <div style={{fontSize:11, fontWeight:800, letterSpacing:".12em", textTransform:"uppercase", color:"var(--p-600, #1e40af)"}}>{kicker}</div>
    <div style={{fontSize:22, fontWeight:800, color:"var(--paybo-heading, #1e3a8a)", marginTop:2}}>{title}</div>
    {subtitle && <div style={{fontSize:13, color:"var(--text-secondary)", marginTop:4, lineHeight:1.55, maxWidth:780}}>{subtitle}</div>}
  </div>
);

/* TaskCard — renders one CTO-ready task in the six-part shape the
   business asked for: Title · Why this matters · Key terms · Acceptance
   criteria · Inputs · How it works. */
const DV_TaskCard = ({ idx, task }) => (
  <div className="paybo-devcard" style={{padding:"18px 20px"}}>
    <div style={{display:"flex", alignItems:"flex-start", gap:12, marginBottom:10}}>
      <span style={{flexShrink:0, display:"inline-grid", placeItems:"center", width:28, height:28, borderRadius:8, background:"var(--p-50)", color:"var(--p-700)", fontSize:12, fontWeight:800}}>
        {idx}
      </span>
      <div style={{flex:1, minWidth:0}}>
        <div style={{fontSize:15.5, fontWeight:700, color:"var(--paybo-heading, #1e3a8a)", lineHeight:1.3}}>{task.title}</div>
        {task.subtitle && <div style={{fontSize:12, color:"var(--text-tertiary)", marginTop:2}}>{task.subtitle}</div>}
        {task.path && (
          <div style={{marginTop:6, display:"flex", alignItems:"center", gap:6, flexWrap:"wrap"}}>
            <span style={{fontSize:10, fontWeight:700, color:"var(--text-tertiary)", textTransform:"uppercase", letterSpacing:".05em"}}>Proposed path</span>
            <code style={{fontSize:11.5, padding:"2px 8px", borderRadius:6, background:"var(--n-25)", border:"1px solid var(--border-subtle)", color:"var(--p-700, #1e3a8a)", fontFamily:"var(--font-mono)"}}>{task.path}</code>
          </div>
        )}
      </div>
      <span style={{fontSize:10.5, fontWeight:700, padding:"3px 9px", borderRadius:999, background:"var(--ok-50, #d1fae5)", color:"var(--ok-700, #065f46)", letterSpacing:".05em"}}>SHIPPED</span>
    </div>

    <DV_Block label="Why this matters">{task.why}</DV_Block>

    {task.terms && task.terms.length > 0 && (
      <DV_Block label="Key terms">
        <ul style={{margin:0, paddingLeft:18, lineHeight:1.6}}>
          {task.terms.map(([k, v]) => (
            <li key={k}><strong>{k}</strong> — {v}</li>
          ))}
        </ul>
      </DV_Block>
    )}

    <DV_Block label="Acceptance criteria (testable)">
      <ul style={{margin:0, paddingLeft:18, lineHeight:1.6}}>
        {task.acceptance.map((a, i) => <li key={i}>{a}</li>)}
      </ul>
    </DV_Block>

    {task.inputs && task.inputs.length > 0 && (
      <DV_Block label="Inputs (fields & parameters)">
        <table style={{width:"100%", borderCollapse:"collapse", fontSize:12}}>
          <thead>
            <tr style={{textAlign:"left", color:"var(--text-tertiary)", fontWeight:700, textTransform:"uppercase", fontSize:10.5, letterSpacing:".05em"}}>
              <th style={{padding:"6px 8px", borderBottom:"1px solid var(--border-default)", width:"30%"}}>Field</th>
              <th style={{padding:"6px 8px", borderBottom:"1px solid var(--border-default)"}}>Notes · validation · defaults · edge cases</th>
            </tr>
          </thead>
          <tbody>
            {task.inputs.map((row, i) => (
              <tr key={i} style={{verticalAlign:"top"}}>
                <td style={{padding:"6px 8px", borderBottom:"1px dashed var(--border-subtle)", fontWeight:600, color:"var(--paybo-heading, #1e3a8a)"}}>
                  <span style={{display:"inline-flex", alignItems:"center"}}>
                    {row.name}
                    {row.desc && <Tip>{row.desc}</Tip>}
                  </span>
                </td>
                <td style={{padding:"6px 8px", borderBottom:"1px dashed var(--border-subtle)", color:"var(--text-secondary)", lineHeight:1.5}}>{row.notes}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </DV_Block>
    )}

    <DV_Block label="How it works (step by step)" noMargin>
      <ol style={{margin:0, paddingLeft:18, lineHeight:1.6}}>
        {task.howItWorks.map((s, i) => <li key={i}>{s}</li>)}
      </ol>
    </DV_Block>
  </div>
);

const DV_Block = ({ label, children, noMargin }) => (
  <div style={{marginTop: noMargin ? 12 : 14}}>
    <div style={{fontSize:10.5, fontWeight:800, letterSpacing:".05em", textTransform:"uppercase", color:"var(--text-tertiary)", marginBottom:6}}>{label}</div>
    <div style={{fontSize:12.5, color:"var(--text-secondary)", lineHeight:1.55}}>{children}</div>
  </div>
);

/* ============================================================
   PHASE DATA — every task expressed as plain data, so the markup
   stays a single component (TaskCard) and the file reads top-down.
   ============================================================ */

const PHASE_1_TASKS = [
  {
    title: "Set up the PSP / Provider directory",
    subtitle: "Settings → Providers",
    path: "/payments/settings?tab=providers",
    why: "Every routing decision the platform makes needs a complete picture of each PSP (cost, settlement speed, supported methods and geos, license coverage, live wallet balance, and health). Without this directory, routing cannot rank or filter providers, treasury cannot manage liquidity, and ops cannot tell a healthy PSP from a degraded one.",
    terms: [
      ["PSP / Provider", "Third party that actually moves the money — Stripe, Adyen, Trustly, Worldpay, etc."],
      ["Settlement (T+0 / T+1 / T+2)", "Days between a successful transaction and funds clearing into the operator's bank account."],
      ["Floor / Ceiling", "Per-PSP wallet thresholds. Floor = top up before withdrawals fail; Ceiling = sweep funds out so capital isn't locked."],
      ["Sweep %", "What % of the ceiling triggers the idle-capital alarm (e.g. 80% of a 2,000,000 ceiling = alarm fires at 1,600,000 in the PSP's wallet currency)."],
      ["MID coverage", "Per-license operations a PSP can authorise (deposit / withdrawal / refund) under each license (MGA, UKGC, Curaçao, Anjouan…)."],
    ],
    acceptance: [
      "Operator can Add / Edit / Pause / Delete a PSP from a single directory page.",
      "Every field needed by routing (cost, limits, liquidity, geos, licenses, supported methods, health, tech endpoint) is captured.",
      "Provider card shows a 7-day liquidity sparkline with floor + ceiling guide lines.",
      "Per-method overrides supported (tighter floor / ceiling on a specific instrument within the PSP).",
      "Health status surfaces inline with a click-through explainer (success rate, latency, timeout rate, last 5 errors).",
      "The directory is the single source of truth the routing engine, alarms, and reports read from.",
    ],
    inputs: [
      { name:"Provider name", desc:"The display name of the PSP — Stripe, Adyen, Trustly, Worldpay, etc. Shows up everywhere the provider is referenced (routes, transactions, reports, alarms).", notes:"Required string. 1–60 chars, unique within the tenant." },
      { name:"Kind", desc:"The PSP family. Card processors (Visa/MC acquirers), Wallets (Skrill, Neteller), Banks (Trustly, SEPA rails), Vouchers (Paysafe), Crypto (Coinify, MoonPay). Determines which payment methods this PSP can plausibly process.", notes:"Required enum: Card / Wallet / Bank / Voucher / Crypto. Drives which methods can plausibly route to this PSP." },
      { name:"Status", desc:"Active = the routing engine can pick this PSP. Paused = kept on file but excluded from routing without losing the config. Use Pause for short-term outages and Active for normal operation.", notes:"Required enum: active / paused. Default active. Paused PSPs stay on file but are excluded from routing." },
      { name:"Costs — Deposit fee / Withdrawal fee / Settle", desc:"What the PSP itself charges per deposit, per withdrawal, and per settlement to the bank. Drives the cost factor in the hybrid ranking and feeds the Effective-cost metric in alarms.", notes:"Required free-form per channel (e.g. \"1.4% + 0.25\"). Engine parses into pct + flat. Currency follows the PSP's settlement currency." },
      { name:"Limits — Per tx Min/Max · Daily · Monthly", desc:"The hard limits the PSP itself will accept per transaction and per period. Independent of the operator's brand × method limits — the routing engine validates against both.", notes:"Required strings; allow currency-prefixed values. PSP-side limits, independent of brand × method limits." },
      { name:"Liquidity — Wallet · Floor · Ceiling · Min withdrawable · Settlement · Payout · Sweep %", desc:"How much money is sitting in the PSP's wallet right now and the operator-set guardrails (Floor to top up by, Ceiling to sweep idle funds out of, minimum withdrawable amount the PSP itself will pay, settlement speed T+0 / T+1 / T+2, daily payout capacity, sweep % that triggers the idle-capital alarm).", notes:"Required block. Wallet balance reads live; floor / ceiling configurable; sweepPct default 80; settlement is T+0 / T+1 / T+2." },
      { name:"Geographies", desc:"Countries this PSP operates in. The routing engine intersects this with the route's GEO scope — any mismatched country drops the PSP out of the chain at runtime.", notes:"Required string[] of ISO codes. Empty = any country (any-geo PSPs); routing intersects with the route's GEO scope at runtime." },
      { name:"Licenses · operations", desc:"The regulatory licenses (MGA, UKGC, ADM/AAMS, Curaçao, Anjouan, etc.) the PSP can process under — and the operations (deposit / withdrawal / refund) each license authorises. Drives the License match in routing.", notes:"Optional. Per-license operations matrix: [{ name, ops: [\"dep\",\"wd\",\"refund\"] }]. Drives which licenses a route can match this PSP for." },
      { name:"Supported payment methods", desc:"The explicit list of payment instruments this PSP can process. Routing refuses to add this PSP to a chain for a method that isn't ticked here — and the route wizard's provider picker greys it out.", notes:"Required string[] of method ids. Routing engine refuses to add this PSP to a chain for a method it doesn't list." },
      { name:"Health — Success rate · Avg latency · Timeout rate · Last check", desc:"Live performance snapshot the engine uses to score the PSP. Online = healthy; Degraded = still routable but pushed down the chain; Offline = excluded entirely. Auto-flipped by alarm rules.", notes:"Live snapshot. status ∈ {online, degraded, offline}. Auto-degraded by alarm rules (e.g. 5xx > 2% in 10min)." },
      { name:"Tech — Endpoint · Timeout · Webhook", desc:"How PayBO talks to the PSP. Endpoint is the PSP's API base URL; Timeout is the wall-clock we wait for a response; Webhook is the URL the PSP calls back when a transaction changes state.", notes:"Required. endpoint = base URL; timeoutMs default 8000; webhook is the URL the PSP calls on state change." },
      { name:"Liquidity alarms", desc:"Per-PSP alarm rules that fire on this provider's wallet / settlement / performance metrics. Same shape as the main Alarms but scoped to one PSP. See Task 4 for the full catalog.", notes:"Optional list of per-PSP alarm rules (see Task 4). Auto-seeded from the legacy floorAlert flag on first edit." },
      { name:"Per-method overrides", desc:"Tighter Floor / Ceiling / Sweep % for one specific method on this PSP. Use when one instrument (e.g. high-value bank wire) needs a different safety margin than the provider-level defaults.", notes:"Optional [{ method, floor, ceiling, sweepPct }]. Tighter floor / ceiling on a specific method for this PSP. Empty = method uses provider-level thresholds." },
    ],
    howItWorks: [
      "Operator opens Settings → Providers and clicks Add provider.",
      "General tab — fills identity (name, kind, status), costs (deposit / withdrawal / settlement), per-PSP limits, the live health row, and the technical wiring (endpoint, timeout, webhook).",
      "Liquidity & thresholds tab — sets Floor / Ceiling / Min withdrawable, then adds one or more liquidity alarm rules with their own destinations (see Task 4). Per-method overrides are added below for any method that needs a tighter cap.",
      "Coverage tab — ticks every payment method this PSP can process; picks the countries (geos) where it operates; declares each license + the operations that license authorises.",
      "On Save, the directory is updated and the routing engine, alarm engine, and reports re-read from it on the next transaction.",
    ],
  },
  {
    title: "Build the Routes & Cascading wizard",
    subtitle: "Settings → Routes & cascading",
    path: "/payments/settings?tab=routes",
    why: "Routing decides which PSP gets the transaction; cascading decides what to do when a PSP fails. These two together are how the business maximises conversion, optimises cost and liquidity, and keeps the system running when one PSP goes down.",
    terms: [
      ["Route", "A rule that maps a (brand × method × currency × amount × geo × license) scope to an ordered list of PSPs (the chain)."],
      ["Chain", "The ordered PSP list inside a route. Position 1 is tried first; on a cascade trigger the engine walks to position 2, then 3."],
      ["Ranking", "How the chain order is interpreted: manual (use order as-is) or hybrid (use order as baseline, adjust on tie / health)."],
      ["Cascade trigger", "Provider-side outcome (decline, timeout, 5xx, psp_down, low_liquidity…) that causes the engine to retry the next PSP."],
      ["Cascade policy", "Per-route Error → Action matrix that overrides the default behaviour for each error type."],
      ["Execution mode", "AUTO (cascade runs without human intervention) vs MANUAL (first failure pauses as To-Confirm until an operator approves)."],
      ["Weighted split", "Send traffic by percentages instead of strict priority (e.g. 60/30/10). On failure, cascade still follows chain order."],
    ],
    acceptance: [
      "3-step wizard (Scope & routing → Cascading → Review & Save) gates Next on per-step validation.",
      "Step 1 lets the operator pick brand / method / currency / amount band / geo / license AND build the provider chain on the same page (so picking a method instantly filters the picker).",
      "Provider picker disables PSPs that don't support the chosen method (sorted last, greyed, with the reason in the label).",
      "Weighted split must total exactly 100% before Save is enabled.",
      "Cascade triggers are explained inline (one sentence per trigger) and the timeout is dynamic (preset OR custom number + sec/min/hr).",
      "Every save appends an immutable entry to the route's audit history (actor, ts, diff, full snapshot).",
    ],
    inputs: [
      { name:"Brand", desc:"Which brand this route fires for. \"any\" matches every tenant; a specific brand scopes it. When the top-right brand selector locks a tenant, this is forced to that tenant (auto-login).", notes:"Required string (\"any\" or a specific brand name). Locked to the top-right tenant when not \"All brands\"." },
      { name:"Method", desc:"The payment instrument this route covers (Visa, Mastercard, Apple Pay, SEPA, Bank wire, Crypto, etc.). One method per route. The provider picker below filters in real time to PSPs that support this method.", notes:"Required. One method per route. The provider picker filters on this — incompatible PSPs are disabled." },
      { name:"Currency", desc:"The transaction currency this route applies to. \"any\" matches all currencies; a specific ISO code scopes the route tighter (useful when one currency needs a different chain).", notes:"Required string (\"any\" or an ISO code). Default \"any\". Currency of the transaction the route matches." },
      { name:"Amount band", desc:"The transaction-amount range this route handles. Pick \"Any amount\" or set Min and / or Max in the route's currency. Most-specific band wins when routes overlap — a 0–5,000 route shadows a 0–any route on small tickets.", notes:"Required free-form band (\"any\", \"0 – 5,000\", \"≥ 25,000\"). Composed from the Any-amount / Custom range UI with Min and Max in the route currency." },
      { name:"GEO", desc:"Countries this route applies to (player country, resolved from IP or KYC address). Empty = any country. Intersected with each PSP's geos at runtime — mismatches drop the PSP out of the chain.", notes:"Optional string[] of ISO codes. Empty = any country. Intersected with each PSP's geos at runtime; mismatches are excluded." },
      { name:"License", desc:"Operator licenses this route applies to (MGA, UKGC, ADM/AAMS, Curaçao, Anjouan…). Use when the same brand operates under multiple licenses and needs a different chain per license.", notes:"Optional string[]. Empty = any license. Used when the same brand operates under multiple licenses with different chains." },
      { name:"Provider chain", desc:"The ordered list of PSPs the engine will try. Position 1 is tried first; on a cascade trigger the engine walks to position 2, then 3, etc. The order IS the operator's ranking.", notes:"Required string[] of PSP ids. Min length 1. PSPs that don't list this method as supported are disabled in the picker." },
      { name:"Weighted traffic split", desc:"Distribute traffic by percentage instead of strict priority (e.g. 60 / 30 / 10). The cascade still uses chain order on failure. Useful for A/B testing PSPs or for graduated rollout.", notes:"Optional boolean. Default false. When true, the per-PSP Weights (%) must sum to exactly 100." },
      { name:"Weights (%)", desc:"The percentage of traffic each PSP in the chain receives when Weighted split is on. Must total 100. If a PSP becomes unhealthy its weight redistributes proportionally to the remaining healthy PSPs.", notes:"Required when Weighted is on. Same length as the chain. Sum to 100. Drives the % of traffic each PSP receives." },
      { name:"Ranking mode", desc:"How the chain order is interpreted. Manual = use the operator's order as-is. Hybrid = use the operator order as the baseline, adjust for performance / cost / liquidity / health when two PSPs are tied or one is unhealthy.", notes:"Required enum: manual / hybrid. Default manual. hybrid blends operator order with live performance / cost / liquidity / health factors." },
      { name:"Ranking factors", desc:"Inputs to the hybrid score, weighted equally. Performance = live success rate; Cost = effective fee; Liquidity = wallet headroom vs floor; Health = degraded / offline status. Factors never override the operator's hard chain order — they only tie-break.", notes:"Optional booleans (performance / cost / liquidity / health). Only meaningful when ranking = hybrid. Default all true. Equal weight 0–100 score." },
      { name:"Execution mode", desc:"How the cascade is executed on failure. AUTO = retry the next PSP automatically (no human in the loop). MANUAL = pause the transaction in the To-Confirm queue after the first failure; an operator must approve before the cascade continues.", notes:"Required enum: AUTO / MANUAL. Default AUTO. MANUAL pauses on first failure for operator approval before the cascade tries the next PSP." },
      { name:"Cascade triggers", desc:"The provider-side outcomes that cause the engine to retry on the next PSP. Tick declined / error / network / psp_down / low_liquidity to retry; leave insufficient_funds and fraud_flag off because retrying a different PSP doesn't help on player-side issues.", notes:"Required subset of {declined, fraud_flag, error, network, psp_down, low_liquidity, insufficient_funds}. Default ['declined']." },
      { name:"Cascade timeout", desc:"How long the engine waits for a PSP response before giving up and moving to the next provider in the chain. Pick a preset (5s…60s, Never) or set a custom value in seconds / minutes / hours.", notes:"Required. Preset (5s…60s, Never) OR custom Nsec/min/hr. Drives the wall-clock timeout before declaring a timeout." },
      { name:"Cascade policy · Error → action", desc:"Per-error-type decision matrix that overrides the platform default. For each error (timeout, 5xx, declined, fraud_flag, insufficient_funds…) pick Retry next provider or Stop. Player-side errors usually Stop; provider-side errors usually Retry.", notes:"Required matrix [{ error, kind, why }]. Defaults from the platform-wide error matrix; per-route overrides are flagged as 'custom cascade' on the page." },
      { name:"Status", desc:"Active = the engine evaluates this route. Paused = the route is kept on file but never matched — transactions fall through to the next-most-specific rule (or to the \"any\" catch-all).", notes:"Required enum: active / paused. Default active. Paused routes are kept on file but never matched." },
    ],
    howItWorks: [
      "Operator opens Settings → Routes & cascading and clicks Add route.",
      "Step 1 (Scope & routing) — picks brand / method / currency / amount band (Any-amount or Min/Max), then GEO and License multi-selects. The provider chain is built on the same page; PSPs that don't support the chosen method are greyed and unselectable in the picker.",
      "Step 2 (Cascading) — picks one or more cascade triggers (each carries a one-sentence explainer of what it means and how it's detected), sets the wall-clock cascade timeout (preset or custom), and tweaks the Error → Action matrix if a per-route override is needed.",
      "Step 3 (Review & Save) — full summary cards (Scope · Routing · Cascading) plus the version-history timeline for existing routes. Operator clicks Create / Save changes.",
      "On save, the route is written to the directory; the routing engine resolves the most-specific route on every new transaction; on failure, the engine walks the cascade chain respecting triggers, policy, and the AUTO/MANUAL mode.",
    ],
  },
  {
    title: "Build the Alarms — rules + destinations + brand scope",
    subtitle: "Settings → Alarms",
    path: "/payments/settings?tab=alarms",
    why: "Alarms are early warnings. They fire when a metric crosses a threshold and fan out to Slack, email, in-app, or webhook so operators can act before the problem becomes a customer complaint.",
    terms: [
      ["Metric", "What the engine measures (decline rate, queue depth, total deposits in window, PSP wallet vs floor…)."],
      ["Threshold + window", "How and when the metric fires (e.g. > 15% over 1h)."],
      ["Severity", "low / medium / high / critical. Drives the chip colour and the top-bar banner for critical-severity alarms."],
      ["Destination", "Reusable endpoint (Slack channel, email inbox, Telegram chat, webhook URL, in-app inbox)."],
      ["Channel", "Delivery family the destination belongs to (Email / Slack / Telegram / WhatsApp / SMS / Webhook / In-app)."],
      ["Brand scope", "Which brands the alarm fires for. \"any\" matches every tenant; specific brands narrow the rule."],
    ],
    acceptance: [
      "30+ metric options grouped into Transactions / Volume & flow / Transaction status / Per method / Queue / Providers / Routes / Operators.",
      "Switching the metric pre-fills sensible defaults for op / threshold / window.",
      "Brand scope multi-select (All brands or one+ specific brands).",
      "Destinations multi-select grouped by channel; operator can Add destination, Edit destination, and toggle Enabled inline without leaving the alarm editor.",
      "Test fire button posts a sample event to the chosen destinations.",
      "Alarm rules table filterable by Group / Severity / Status / Provider / Brand; per-provider liquidity alarms are surfaced as read-only rows alongside the main rules.",
    ],
    inputs: [
      { name:"Name", desc:"A short human-readable label for the rule. Shows up in the alarm rules table and in the fired-alarm notifications. Pick something a colleague can read in 1 second and understand what fired — e.g. \"Decline rate spike\" or \"Stripe wallet near ceiling\".", notes:"Required. Short human-readable label that shows up in the rules table and in the fired-alarm notifications." },
      { name:"Metric", desc:"What the engine measures (decline rate, queue depth, total deposits in window, PSP wallet vs floor, etc.). The platform ships 30+ metrics grouped by area (Transactions / Volume & flow / Transaction status / Per method / Queue / Providers / Routes / Operators). Switching the metric pre-fills sensible defaults for operator / threshold / window.", notes:"Required key from the alarm-metric catalog. Switching the metric pre-fills Operator / Threshold / Window with the metric's suggested defaults." },
      { name:"Operator", desc:"The comparison: greater than, less than, ≥, ≤, =. Drives whether the rule fires when the metric goes up vs down. The default pre-filled per metric is usually correct (e.g. > for decline rate, < for approval rate).", notes:"Required enum: > / < / ≥ / ≤ / =. Default comes from the chosen metric." },
      { name:"Threshold", desc:"The value the metric is compared against. Units depend on the metric — percentage for rates, count for queue depth, currency amount for volume, milliseconds for latency, hours for delays, an event-token for state-flip metrics.", notes:"Required number or string. Value the metric is compared against. Units depend on the metric (%, count, currency amount, ms, hours, event-token)." },
      { name:"Window", desc:"The rolling time window the metric is computed over. \"—\" for event-based metrics that fire instantly. Shorter windows (5m / 15m) catch fast-moving spikes; longer windows (1h / 6h / 24h) smooth out noise.", notes:"Optional rolling window: \"—\" / 5m / 15m / 30m / 1h / 6h / 24h / 3d / custom Nm-h-d." },
      { name:"Severity", desc:"How urgent the alarm is. Low = informational; Medium = worth checking today; High = check now; Critical = drop everything (top-bar banner surfaces until acknowledged). Drives chip colour everywhere and the alert sound on Slack / Telegram.", notes:"Required enum: low / medium / high / critical. Default medium. Critical surfaces as a top-bar banner until acknowledged." },
      { name:"Brand scope", desc:"Which brands the alarm fires for. \"All brands\" matches every tenant; one or more specific brands narrow the rule. Useful when one brand has a different SLA than the rest. Locked to the top-right tenant when the operator is in auto-login mode.", notes:"Required. Default [\"any\"] (all brands). Picking specific brands removes the \"any\" sentinel automatically. Locked to the top-right tenant when set." },
      { name:"Scope · payment method", desc:"Only appears when the chosen metric needs a method scope — e.g. \"per-method decline rate\" needs to know whether you mean Visa or Bank wire. For global metrics this field is hidden.", notes:"Optional method id. Only shown / required when the chosen metric requires a method scope (e.g. per-method decline rate)." },
      { name:"Destinations · send to", desc:"Where the alarm gets posted when it fires. Pick one or more reusable destinations (Slack channel, email inbox, Telegram chat, SMS number, webhook URL, in-app inbox). Fan-out is parallel — every picked destination is notified.", notes:"Required list of destination ids. Min length 1 — at least one destination required before Save enables." },
      { name:"Channels (derived)", desc:"Computed automatically from the destinations you pick — used to render the channel chips in the rules table. You don't set this directly.", notes:"Derived automatically from picked destinations; persisted on the row for the rules-table chip render." },
      { name:"Status", desc:"On = the rule is being evaluated and can fire. Off = the rule is paused; metric is still measured but no notification fires. Use Off for temporary silences during planned PSP maintenance.", notes:"Required enum: on / off. Default on. off keeps the rule on file without firing." },
    ],
    howItWorks: [
      "Operator opens Settings → Alarms and clicks Add alarm.",
      "Picks a metric — the editor explains in plain English what the metric measures and where the value comes from.",
      "Picks operator + threshold + window (defaults pre-filled from the metric). Picks severity.",
      "Picks brand scope (All brands or one+ specific tenants). When the top-right brand selector locks a tenant, this is forced to that tenant.",
      "Picks one or more destinations from the grouped multi-select. Add destination opens the destination editor inline; Edit / On-Off chips manage existing destinations without leaving the alarm.",
      "On Save, the rule is written to the alarm registry. The engine monitors the metric continuously; when the condition holds, it fans out to every destination in parallel, dedupes by a 5-minute window, and writes an entry to the Activity log.",
    ],
  },
  {
    title: "Provider Liquidity & Threshold Alarms",
    subtitle: "Settings → Providers → Liquidity & thresholds",
    path: "/payments/settings/providers/:pspId?tab=liquidity",
    why: "Treasury and ops need per-PSP eyes on wallet balance, settlement delays, and PSP-side performance without leaving the provider page. Same alarm shape as the main Alarms, scoped to one PSP's metrics, with the same destinations registry.",
    terms: [
      ["Floor / Ceiling / Min withdrawable", "Provider-level wallet thresholds (see Task 1)."],
      ["Wallet near ceiling", "% of ceiling at which the idle-capital alarm should fire (e.g. 80% of a 2,000,000 ceiling triggers at 1,600,000 in the PSP's wallet currency)."],
      ["Liquidity drop %", "Drop in wallet balance vs the start of the day (% delta). Catches sudden outflow / fraud rings."],
      ["Settlement delay", "Hours since funds should have settled (T+N) without acknowledgement from the bank rail."],
      ["Payout capacity", "Remaining daily payout allowance the PSP will honour."],
    ],
    acceptance: [
      "7 curated metric options — the most important and easiest to act on: Wallet below floor · Wallet near ceiling · Below minimum withdrawable · Settlement delay · PSP marked OFFLINE · PSP success rate dropped · Single transaction ≥ amount.",
      "Multi-rule per PSP — operator can add as many alarm rows as needed.",
      "Each rule has its own destinations (multi-select grouped by channel, with Add / Edit / Enabled toggle inline).",
      "Severity drives the chip colour and the same fan-out path as the main alarms.",
      "Rules surface in Settings → Alarms as read-only rows tagged with the originating PSP; clicking the row opens the provider editor on the Liquidity tab.",
      "Seeded floor + ceiling rules created on first edit if the legacy floorAlert flag was true.",
    ],
    inputs: [
      { name:"Floor", desc:"The minimum balance to keep in this PSP's wallet. Drop below it and withdrawals start failing for empty wallet. Set at roughly 10–20% of typical daily payout volume so there's headroom for the worst-day-of-the-week.", notes:"Required integer in the PSP's wallet currency (whichever currency this PSP settles in). Default ~10–20% of typical daily payout volume through this PSP." },
      { name:"Ceiling", desc:"The maximum balance we want sitting idle in this PSP's wallet. Hit it and treasury should sweep funds out into the bank account so capital isn't locked up. Set at ~2–3 days of payout volume.", notes:"Required integer in the PSP's wallet currency. Default ~2–3 days of payout volume." },
      { name:"Min withdrawable", desc:"The smallest single-withdrawal amount the PSP itself will accept. Below this the PSP rejects the request — useful to validate against before routing a withdrawal that can't be honoured.", notes:"Required integer in the PSP's wallet currency. Smallest single-withdrawal amount the PSP itself will accept." },
      { name:"Sweep %", desc:"How full the wallet gets before the idle-capital alarm fires. 80% of a 2,000,000 ceiling = alarm at 1,600,000. Lower it to be more conservative on capital efficiency; raise it to reduce alarm noise.", notes:"Optional integer 50–100. Default 80. Used as the suggested threshold for the Wallet near ceiling metric." },
      { name:"Liquidity alarms", desc:"The per-PSP alarm rules attached to this provider. Same shape as the main Alarms (name / metric / operator / threshold / severity / destinations / status) — just scoped to one PSP's metrics.", notes:"Optional list of per-PSP alarm rules. Each rule carries Name · Metric · Operator · Threshold · Severity · Destinations · Status." },
      { name:"Liquidity alarms → Metric", desc:"Which signal each rule watches. 7 curated options — the most important and easiest to act on for treasury and routing: Wallet below floor (top-up signal), Wallet near ceiling (sweep signal), Below minimum withdrawable (PSP will reject payouts), Settlement delay (T+N SLA breached), PSP marked OFFLINE (critical), PSP success rate dropped (earliest degradation signal), Single transaction ≥ amount (VIP / fraud desk).", notes:"Required enum (7 keys). Each carries a detailed plain-English description inside the editor." },
      { name:"Liquidity alarms → Destinations", desc:"Where each rule posts when it fires. Pick from the same global destinations registry used by Settings → Alarms. Add / Edit / On-Off inline so you don't have to leave the PSP editor to create a new Slack channel.", notes:"Required. Reuses the global destinations registry (same as Settings → Alarms). Add / Edit / On-Off inline without leaving the PSP editor." },
    ],
    howItWorks: [
      "Operator opens Settings → Providers, picks a PSP, clicks Edit, goes to Liquidity & thresholds.",
      "Sets Floor / Ceiling / Min withdrawable for the wallet.",
      "Clicks Add alarm — a new rule row appears with sensible defaults pulled from the chosen metric.",
      "Picks destinations from the grouped multi-select (or clicks Add destination to create one without leaving the PSP editor).",
      "On Save, the rules are persisted on the PSP record. The engine starts evaluating each rule on the live wallet / settlement / health stream and fans out to destinations when a condition holds.",
      "In Settings → Alarms, every provider-scoped rule appears as a read-only row tagged with the PSP name; clicking it deep-links back to the provider's Liquidity tab.",
    ],
  },
];

const PHASE_2_TASKS = [
  {
    title: "Build the Payment Method catalog",
    subtitle: "Payments → Payment methods",
    path: "/payments/methods",
    why: "One screen to see every payment method available across every brand, with the per-brand × per-method config (currencies, fees, limits, auto-approval, role access, PSP route) one click away.",
    terms: [
      ["Method", "Payment instrument exposed to the player (Visa, Mastercard, Apple Pay, SEPA, Trustly, Skrill, Crypto…)."],
      ["Kind", "Method family — Card / Wallet / Bank / Voucher / Crypto. Drives PSP compatibility."],
      ["Brand × Method config", "One row per (brand, method) pair, owning currencies + fees + limits + roles + auto-approval + PSP route."],
    ],
    acceptance: [
      "Grid lists every brand × method config; rows expandable to a summary panel.",
      "Filters (Brand / Kind / Status / Currency / Provider) match the row model and stay in sync with the top-right tenant selector (locks Brand to auto-login when a tenant is picked).",
      "Columns popover hides non-essential columns; visibility is persisted per operator via localStorage.",
      "Add method opens the editor pre-seeded with sensible blank defaults so the operator can fill in from scratch.",
      "Export downloads every row as a CSV.",
    ],
    inputs: [
      { name:"Brand (filter)", desc:"Narrow the grid to one brand's configurations. Hidden / locked when the top-right brand selector is already on a specific tenant (the auto-login scope already restricts the page).", notes:"Optional. Default \"all\". Locked to the active tenant when the top-right selector is not \"All brands\"." },
      { name:"Kind", desc:"Filter by method family — Card / Wallet / Bank / Voucher / Crypto. Useful when you want to mass-edit only your card portfolio or only your bank-wire rows.", notes:"Optional enum: Card / Wallet / Bank / Voucher / Crypto. Default \"all\"." },
      { name:"Status", desc:"Show only Enabled rows (live), only Disabled rows (paused), or both. Quick way to audit which methods are dark on a given brand.", notes:"Optional enum: All / Enabled / Disabled. Default \"all\"." },
      { name:"Currency", desc:"Limit the grid to method configs that accept this currency. Each row supports multiple currencies; this filter matches any of them.", notes:"Optional ISO code or \"all\". Limits the grid to rows that accept the selected currency." },
      { name:"Provider", desc:"Limit the grid to method configs whose routing chain (Settings → Routes) contains the selected PSP at any position. Use to find every method that depends on a degraded PSP.", notes:"Optional PSP id or \"all\". Limits the grid to rows whose routing chain contains the selected PSP." },
      { name:"Search", desc:"Free-text search across method name, kind, brand name and brand short code. Type a few letters of the method or brand to find the row.", notes:"Optional free-text. Matches across method name, kind, brand name, brand short." },
      { name:"Columns", desc:"Hide the columns you don't need to free up table width. Brand, Method and Actions always stay visible. Your layout is saved per operator so it sticks across sessions.", notes:"Optional. Per-operator visibility toggles for Provider · Status · Currencies · Min / Max · Deposit fee · Withdraw fee · Auto-approve. Persisted to localStorage." },
    ],
    howItWorks: [
      "Page loads MOCK.BRANDS × MOCK.METHODS and builds a row per (brand, method) pair with the seeded config.",
      "Operator narrows the grid with the unified filter row + search box; the result count + 'Clear all' chip update live.",
      "Click a row to expand the inline summary (limits + auto-approval + PSP route preview).",
      "Click Edit (or Edit rules) to open the per-row editor modal at the relevant tab.",
      "Click Add method to open the editor on a fresh seed row.",
    ],
  },
  {
    title: "Method editor — General + Currencies tabs",
    subtitle: "Payment methods → Edit method",
    path: "/payments/methods/:brandId/:methodId",
    why: "Decide whether the method accepts deposits and / or withdrawals and in which currencies, whether promo codes can be applied through it, and what the base-currency Min / Max are — so the routing engine has one clear contract per (brand, method, currency) triplet.",
    terms: [
      ["Base currency", "The brand's primary currency. Limits expressed in this currency unless a per-currency override exists."],
      ["Per-currency override", "Replaces the base Min / Max for one specific currency (e.g. EUR base + a tighter USD cap)."],
      ["Promo code (eligibility)", "Per-method on/off switch that decides whether players can enter a promotion / bonus code when paying with this method. The toggle controls eligibility only; the campaigns themselves live in Bonus campaigns."],
    ],
    acceptance: [
      "Status toggle enables / disables the method on the brand without losing config.",
      "Min and Max accept base-currency numbers; values are validated before save.",
      "Deposit / Withdrawal toggles independently enable each direction.",
      "Promo code toggle (Promotions block) controls whether the promo-code field appears for this method on the player frontend.",
      "Currencies tab lists every supported currency with a tickable card.",
      "Switching the active currency chip in the editor surfaces that currency's Min / Max override.",
    ],
    inputs: [
      { name:"Status (Enabled / Disabled)", desc:"Quick switch to take this method online or offline on this brand without losing the config. Disabled rows reject every new attempt but keep all the limits / fees / overrides on file.", notes:"Required boolean. Default Enabled on Add." },
      { name:"Min (base)", desc:"The lower bound on every transaction amount through this method, in the brand's base currency. The Currencies tab can override this per non-base currency.", notes:"Required number in the brand's base currency. Default ~10–20 for cards; higher for bank wires." },
      { name:"Max (base)", desc:"The upper bound on every transaction amount through this method, in the brand's base currency. Transactions above this fail at validation, before they ever reach a PSP.", notes:"Required number in the brand's base currency. Default ~50,000 for cards; lower for vouchers." },
      { name:"Deposit", desc:"Allow players to deposit using this method. Turning it off hides the method on the deposit page of the player frontend.", notes:"Required boolean. Default on." },
      { name:"Withdrawal", desc:"Allow players to cash out using this method. Withdrawals usually have stricter rules (KYC, higher manual-review thresholds). Set off for deposit-only methods like vouchers.", notes:"Required boolean. Default on. Off for deposit-only methods like vouchers." },
      { name:"Promo code (Allowed / Disabled)", desc:"When Allowed, players can apply a promotion / bonus code (e.g. WELCOME100) when paying with this method. Use to scope which methods are eligible for a campaign — e.g. enable on cards so a first-deposit bonus only fires on Visa / Mastercard, not crypto. The actual campaigns are configured in Bonus campaigns; this toggle only decides eligibility per method. Saved on the method as `promo_code_enabled`.", notes:"Required boolean. Default Allowed (true) on Add. Disabling it hides the promo-code field on the deposit page of the player frontend for this method." },
      { name:"Enabled currencies", desc:"Which currencies this method accepts on this brand. Each currency carries its own Min / Max (here) and its own fee overrides (Fees tab). Disabling a currency removes the method from the player frontend for that currency.", notes:"Required string[] of ISO codes. Min length 1. The brand's base currency is always included." },
      { name:"Per-currency overrides — Min / Max", desc:"Replaces the base-currency Min / Max for a specific non-base currency. Useful when a non-base market has different ticket sizes (e.g. EUR base 10–50,000, USD override 20–60,000).", notes:"Optional. Replaces the base Min / Max for one specific currency." },
    ],
    howItWorks: [
      "Operator opens the Edit method modal.",
      "General tab — toggles Status; sets base-currency Min / Max; toggles Deposit / Withdrawal availability; toggles Promo code eligibility in the Promotions block.",
      "Currencies tab — ticks every currency this method accepts on this brand.",
      "Picks a currency chip and (optionally) sets its Min / Max override for the picked currency.",
      "On Save, the engine reads the active config on every new tx, validates the amount against the matching currency's bounds, and surfaces / hides the promo-code field on the player deposit page based on `promo_code_enabled`.",
    ],
  },
  {
    title: "Method editor — Fees tab",
    subtitle: "Payment methods → Edit method → Fees",
    path: "/payments/methods/:brandId/:methodId?tab=fees",
    why: "Define the fee the operator charges on top of the PSP fee, and decide whether it comes out of the player's balance or the operator's revenue. This is one of two levers (the other being routing) the business has to control margin per method.",
    terms: [
      ["Percentage fee", "% of the transaction amount."],
      ["Fixed fee", "Flat amount per transaction, in base currency. Covers the PSP's per-tx flat charge."],
      ["Charged to", "Who absorbs the fee: Player (subtracted from balance) or Operator (deducted from revenue)."],
      ["Rounding", "How fractional cents are handled (nearest 0.01 or exact)."],
    ],
    acceptance: [
      "Deposit + Withdraw percentage AND fixed fee inputs.",
      "Charged-to dropdown switches between Player and Operator.",
      "Rounding dropdown picks the policy.",
      "Per-currency fee overrides exist (in the Currencies tab) and are honoured when set.",
    ],
    inputs: [
      { name:"Deposit fee %", desc:"The percentage of the deposit amount the operator charges (e.g. 1.5% on a 100 deposit = 1.50). Common range 0–3%. Set to 0 to absorb the cost yourself.", notes:"Required number, 0–10. Default 1.5." },
      { name:"Deposit fixed", desc:"A flat amount added on top of the percentage, in the base currency (e.g. 0.30 added to every deposit). Useful for low-ticket methods where a % alone doesn't cover the PSP's flat charge.", notes:"Required number in the base currency. Default 0.30." },
      { name:"Withdraw fee %", desc:"The percentage charged on every successful withdrawal. Usually higher than the deposit fee — operators want to discourage frequent low-value cash-outs that cost more in PSP fees than the player wagered.", notes:"Required number, 0–10. Default 1.75." },
      { name:"Withdraw fixed", desc:"Flat amount added on top of the withdrawal percentage. Covers the PSP's per-payout charge.", notes:"Required number in the base currency. Default 0.50." },
      { name:"Charged to", desc:"Who absorbs the fee. Player = subtracted from the player's balance (deposit of 100 → 99 balance with a 1% fee). Operator = the casino absorbs it from revenue (deposit of 100 → 100 balance, fee comes out of GGR).", notes:"Required enum: Player / Operator. Default Player." },
      { name:"Rounding", desc:"How fractional cents are handled. \"Nearest 0.01\" rounds to a clean cent. \"Exact\" keeps the raw float and surfaces a sub-cent figure where the PSP supports it.", notes:"Required enum: Nearest 0.01 / Exact. Default nearest 0.01." },
    ],
    howItWorks: [
      "On every successful transaction, engine computes effective fee = pct × amount + fixed.",
      "If charged_to = player, the fee is subtracted from the player's balance (deposit nets a smaller credit; withdrawal nets a smaller payout).",
      "If charged_to = operator, the player sees no fee, the casino absorbs it from revenue.",
      "Rounding is applied per policy and the result is written to the transaction record.",
      "Per-currency overrides take precedence over the base values when present.",
    ],
  },
  {
    title: "Method editor — Limits tab (money + count limits)",
    subtitle: "Payment methods → Edit method → Limits",
    path: "/payments/methods/:brandId/:methodId?tab=limits",
    why: "Cap how much money AND how many transactions a player can push through this method per period. Failed-count windows are particularly useful on cards — a player whose Visa fails 5 times a day is usually doing fraud, not making a mistake.",
    terms: [
      ["Money limit", "Per-period Min / Max cap in a specific currency."],
      ["Count limit", "Per-period cap on the number of transactions (deposits / failed deposits / withdrawals / failed withdrawals)."],
      ["Rolling window", "Daily (24h) / Weekly (7d) / Monthly (30d). Resets at 00:00 UTC."],
    ],
    acceptance: [
      "Money limits per currency for Daily / Weekly / Monthly windows, separately for Deposit and Withdrawal.",
      "Count limits global per method for 4 dimensions (n_deposits, n_failed_deposits, n_withdrawals, n_failed_withdrawals) across D / W / M.",
      "Crossing any limit blocks the next transaction on this method (no \"record only\" mode).",
      "Limits surface in real time in Player 360 → Limits with amber-at-80% / red-on-breach bars.",
    ],
    inputs: [
      { name:"General limits — Deposit / Withdrawal × Daily / Weekly / Monthly × Min / Max", desc:"Money caps in this currency per period. Daily / Weekly / Monthly windows reset at 00:00 UTC. Max blocks any transaction above the cap; Min blocks any transaction below. Switch the currency chip in the editor to set caps per currency.", notes:"Required per-currency × per-period × per-direction. Defaults from brand level on first save." },
      { name:"Count limits — Number of deposits / failed deposits / withdrawals / failed withdrawals × Daily / Weekly / Monthly", desc:"How many transactions a player can make per period across each of the four kinds. Failed-count windows are particularly useful on cards — a player whose Visa fails 5 times a day is usually doing fraud, not making a mistake. Always enforced.", notes:"Required integers per row. Defaults: deposits 20 / 60 / 200; failed deposits 5 / 15 / 40; withdrawals 5 / 15 / 50; failed withdrawals 3 / 10 / 25. Crossing a limit blocks the next transaction on this method." },
    ],
    howItWorks: [
      "On every new transaction, engine checks the player's running totals for the current rolling window against every applicable limit.",
      "If any money limit would be breached, the transaction is rejected before reaching the PSP with a clear reason.",
      "If any count limit would be breached, the transaction is rejected with the count reason.",
      "Failed-count windows track outcomes too — 5 failed Visa deposits in a day triggers the limit and blocks the next attempt.",
      "Limits surface in Player 360 → Limits with live progress bars (amber at ≥ 80% of cap, red on breach).",
    ],
  },
  {
    title: "Method editor — Roles + Auto-approval + PSP route",
    subtitle: "Payment methods → Edit method",
    path: "/payments/methods/:brandId/:methodId",
    why: "Restrict which network tiers can use this method, decide which transactions skip manual review, and surface (read-only) which PSP chain processes this method on this brand.",
    terms: [
      ["White-label pyramid", "Admin → Master → Promoter → Agent → Player. Each tier inherits permissions from the one above."],
      ["Auto-approval threshold", "Amount at or below which a transaction skips the To-Confirm queue."],
      ["PSP route (read-only here)", "The chain that processes this method on this brand; edited centrally in Settings → Routes & cascading."],
    ],
    acceptance: [
      "Per-role toggle blocks / allows the method for each tier. Admin is always allowed (forced_on).",
      "Bulk Enable / Disable for all tiers.",
      "Auto-approval threshold input in base currency.",
      "PSP route tab shows the matching routes (filter by brand + method) read-only, with a deep-link to Settings → Routes.",
    ],
    inputs: [
      { name:"Role access — per-tier toggle", desc:"Per white-label tier (Admin / Master / Promoter / Agent / Player) decide whether the method is available. Blocking a tier also blocks every tier below it. Admin is always allowed (forced on).", notes:"Optional. Default seeded from the method-level blocks list. Empty = available to every tier. Admin is always on (forced_on)." },
      { name:"General auto-approve threshold", desc:"The amount at or below which a transaction skips the To-Confirm queue and gets approved automatically. Above the threshold the transaction lands in manual review for an operator to look at. Set to 0 to disable auto-approval entirely (every transaction requires manual review).", notes:"Required number in the base currency. Default 1,000–1,500 for cards; lower for wires." },
    ],
    howItWorks: [
      "Operator picks the Roles tab and ticks / unticks each tier that should be allowed.",
      "Operator picks the Auto-approval rules tab and sets the threshold in base currency.",
      "On every new transaction: if the player's tier is in blocked_roles, the method is rejected at validation; otherwise, if amount ≤ auto_approve_under the transaction skips the To-Confirm queue and resolves immediately.",
      "The PSP route tab is a read-only mirror of the routes; clicking it leads to Settings → Routes & cascading where the chain is actually edited.",
    ],
  },
];

const PHASE_3_TASKS = [
  {
    title: "Build the Players list",
    subtitle: "Payments → Players",
    path: "/payments/players",
    why: "Find a player. Spot risk patterns (suspended accounts, restricted methods, players with operator notes). Do quick bulk actions (suspend, block all methods) without opening the 360.",
    terms: [
      ["Role", "White-label tier of the account (Master / Promoter / Agent / Player)."],
      ["State", "Account state — Active / Suspended / Restricted methods."],
      ["Notes", "Operator-written notes attached to the player record; visible to every operator working the player."],
    ],
    acceptance: [
      "Search by name / email / id / phone / country.",
      "Filters (Brand / Role / State / Notes / Country) match the row data model.",
      "Columns popover hides non-essential columns; visibility persisted per operator.",
      "Hover actions: Suspend (toggles a flag on the record) and Block all methods (writes disabled_methods array).",
      "Click a row to drill into Player 360.",
    ],
    inputs: [
      { name:"Search", desc:"Free-text search across name, email, user id, phone, and country. Type any fragment to narrow the list to matching accounts.", notes:"Optional free-text. Matches across name, email, user id, phone, country." },
      { name:"Brand", desc:"Limit the list to one brand's accounts. Hidden / locked when the top-right brand selector already picks a tenant.", notes:"Optional. Locked to the active tenant when the top-right selector is not \"All brands\"." },
      { name:"Role", desc:"The white-label network tier of the account: Master, Promoter, Agent, or Player. Internal back-office roles (Admin, Ops) are excluded from this list.", notes:"Optional. Excludes internal roles (Admin, Ops). Options: Master / Promoter / Agent / Player." },
      { name:"State", desc:"Active = normal account. Suspended = every transaction is blocked until reactivated. Restricted = active account but at least one payment method is explicitly disabled.", notes:"Optional enum: All / Active / Suspended / Restricted methods." },
      { name:"Notes", desc:"Show only accounts that have (or don't have) operator-written notes attached. Useful for finding players who've already been flagged by the team for context.", notes:"Optional enum: All / With notes / No notes." },
      { name:"Country", desc:"Resolved from the player's KYC address (or the IP at registration if KYC isn't completed). Use to find risky geos or to assemble a regional report.", notes:"Optional. Resolved from KYC address." },
      { name:"Columns", desc:"Hide columns you don't need (Role, Brand, Country, State, Notes, Deposits, Withdrawals, Net, Last active). ID, Name and Actions stay mandatory. Your layout is saved per operator across sessions.", notes:"Optional. Persisted to localStorage." },
    ],
    howItWorks: [
      "Page loads MOCK.PLAYERS_LIST and applies the filter chain (search → brand → role → state → notes → country).",
      "Sortable columns let the operator rank by deposits / withdrawals / net / last active.",
      "Hover actions:",
      "  · Suspend writes `suspended: true` on the player record and (if open) the live profile.",
      "  · Block all methods writes `disabled_methods: [all method ids]`.",
      "Clicking a row sets the selected player in app state and renders Player 360 (same route, different sub-view).",
    ],
  },
  {
    title: "Player 360 — header + lifetime KPIs + sub-tabs",
    subtitle: "Players → click a row",
    path: "/payments/players/:playerId",
    why: "One profile page per player. Three lifetime KPIs at the top tell the story in three numbers; five sub-tabs (Overview, Transactions, Limits, KYC, Notes) drill in further.",
    terms: [
      ["Lifetime deposits / withdrawals", "Sum across the full account history (settled only)."],
      ["Net deposits", "Lifetime deposits − Lifetime withdrawals. Positive = casino is up; negative = player is net-up."],
    ],
    acceptance: [
      "Back arrow returns to the Players list with the previous filter set intact.",
      "Three KPIs: Lifetime deposits / Lifetime withdrawals / Net deposits. Each carries an inline help tip.",
      "Five sub-tabs (Overview / Transactions / Limits / KYC / Notes). Each tab button has its own Tip describing what's inside.",
      "Header shows brand chip, role chip, country, last-active relative time, and the suspended badge if any.",
    ],
    inputs: [
      { name:"Player", desc:"The selected player record. Flows in from the Players list when an operator clicks a row, or from Reports when an operator clicks a player name in one of the ranked lists. Merged at load time with the rich profile (limits, saved methods, notes, KYC) so every sub-tab has the full picture.", notes:"Required. Merged at load time with the rich profile so every sub-tab has the full picture." },
    ],
    howItWorks: [
      "Selected player flows down via props from app.jsx.",
      "Profile is composed: brand / role / lifetime totals come from the selected record; limits / saved methods / notes come from the rich profile.",
      "Header KPIs render Money components in the brand's base currency.",
      "Tab buttons toggle local state; each tab content renders independently.",
    ],
  },
  {
    title: "Player 360 — Transactions sub-tab",
    subtitle: "Player 360 → Transactions",
    path: "/payments/players/:playerId?tab=transactions",
    why: "Full transaction log for this player, with the same status / type semantics as the global Transactions page. Lets ops investigate a single player's history without losing context.",
    terms: [
      ["Status", "balanced / to_confirm / pending / failed (rejected / declined / errored)."],
      ["Type", "Deposit / Withdrawal."],
    ],
    acceptance: [
      "Lists every transaction tied to this player_id (scoped to this brand).",
      "Status chips + type chips + amount in the player's currency.",
      "Click a row to open the transaction-detail drawer (status timeline + routing decision + retry history).",
    ],
    inputs: [
      { name:"Player transaction log", desc:"The full transaction log for this player, scoped to the current brand. Sorted newest first. Row click opens the same transaction-detail drawer used on the main Transactions page so the audit trail is consistent.", notes:"Required. Filtered automatically by player_id. Falls back to a curated mock log when no drill-in player is passed." },
    ],
    howItWorks: [
      "Filters MOCK.TRANSACTIONS where user_id matches the player; sorts by created_at desc.",
      "Renders the table with the shared status / type chips and Money component.",
      "Row click opens the same transaction-detail drawer used on the main Transactions page (single source of truth).",
    ],
  },
  {
    title: "Player 360 — Limits sub-tab (money + real-time count limits)",
    subtitle: "Player 360 → Limits",
    path: "/payments/players/:playerId?tab=limits",
    why: "See where the player stands against their caps right now, with progress bars that go amber at 80% and red on breach. Adjust per-player overrides when a VIP needs a higher daily cap, or reset to the level defaults.",
    terms: [
      ["Money limit", "Per-period Min / Max in the player's currency (Daily / Weekly / Monthly)."],
      ["Count limit (real-time)", "Per-period transaction count with running totals derived from PLAYER_TX (live)."],
    ],
    acceptance: [
      "Six money-limit progress bars (Deposit + Withdrawal × Daily/Weekly/Monthly).",
      "Four count-limit rows (n_deposits, n_failed_deposits, n_withdrawals, n_failed_withdrawals) × Daily/Weekly/Monthly, with bars.",
      "Bars turn amber at ≥ 80% of cap and red on breach.",
      "Edit player limits opens a per-player override editor; Reset to Gold defaults reverts to the level defaults.",
    ],
    inputs: [
      { name:"Deposit limits — Daily · Weekly · Monthly (used / cap)", desc:"Money limits for deposits across the three rolling windows. Each bar shows how much of the cap the player has already used. Bar turns amber at ≥ 80% and red on breach.", notes:"Required. Money limits plus running usage. Usage is computed from the player's transaction log at load time." },
      { name:"Withdrawal limits — Daily · Weekly · Monthly (used / cap)", desc:"Same as Deposit limits but scoped to withdrawals. Important for spotting players who are about to hit their monthly cash-out cap before the operator's payout schedule.", notes:"Required. Same shape as Deposit limits, scoped to withdrawals." },
      { name:"Count limits — Number of deposits / failed deposits / withdrawals / failed withdrawals × Daily / Weekly / Monthly", desc:"How many transactions of each kind the player can make per period. Real-time — counts are derived from the live transaction log every time the page loads. Useful for catching fraud bursts and bot rings.", notes:"Required. Per-method count caps (defaults inherited from Payment Methods → Limits)." },
      { name:"Edit player limits", desc:"Open the per-player override editor — set bespoke limits for one VIP player without touching the level defaults. The override sticks until you Reset.", notes:"Optional action. Opens the per-player override editor." },
      { name:"Reset to Gold defaults", desc:"Revert any per-player overrides on this account and recompute the bars from the player's level defaults (Bronze / Silver / Gold / Platinum). Use when a VIP override is no longer needed.", notes:"Optional action. Reverts to the player's level defaults and recomputes the bars." },
    ],
    howItWorks: [
      "Limits component renders the 6 LimitBar widgets for money limits using P.limits values.",
      "Count limits panel derives running totals from PLAYER_TX with `inWindow(t, DAY|WEEK|MONTH)` predicates and renders progress bars per row × period.",
      "Edit player limits opens the per-player override modal (admin-build wiring).",
      "Reset to Gold defaults reverts the override and the bars recompute from level defaults.",
    ],
  },
  {
    title: "Player 360 — Notes + KYC placeholder + Quick actions",
    subtitle: "Player 360 → Notes / KYC",
    path: "/payments/players/:playerId?tab=notes",
    why: "Capture operator context on the player and the relationship over time. KYC ships in v2; we keep a clean placeholder so the tab exists today without misleading anyone. Quick actions cover the two most-used flows: manual deposit and manual withdraw.",
    terms: [
      ["Operator note", "Free-form text attached to the player by an operator; immutable in v1 (delete only)."],
      ["KYC", "Know Your Customer — documents + screening; placeholder in v1, ships in v2."],
    ],
    acceptance: [
      "Notes list with author + timestamp + delete; empty state when no notes.",
      "Add note as the current operator; Cmd / Ctrl + Enter submits.",
      "Notes persist on the player record (notes_list) and survive re-opens.",
      "KYC tab shows a styled \"Coming in v2\" placeholder (icon + headline + description).",
      "Manual deposit + Manual withdraw quick actions visible in the header.",
    ],
    inputs: [
      { name:"Add note (editor)", desc:"Free-text editor for adding a new operator note to the player record. Write context that the next operator working this account will need (fraud history, VIP relationship, payout call you took). Cmd / Ctrl + Enter submits.", notes:"Optional free-text. Trimmed before save; empty notes are rejected." },
      { name:"Notes (list)", desc:"Every note that's ever been written on this player. Author + timestamp + full text. Persisted on the player record and visible to every operator working the account.", notes:"Required list. Persisted on the player record; defaults to a single system note when a legacy seed text is present." },
      { name:"Manual deposit / Manual withdraw (quick actions)", desc:"Header buttons that open a manual transaction creation flow — used when an operator needs to post a deposit / withdrawal on the player's behalf (e.g. recovering a bank-wire that didn't auto-match).", notes:"Optional header buttons. Disabled in v1 with the operator credit/debit endpoints named; the admin build wires the actual flow." },
    ],
    howItWorks: [
      "Operator types into the textarea; Cmd/Ctrl+Enter or the Add note button persists the note as the current operator.",
      "Each note is rendered as a card with delete affordance.",
      "KYC tab renders a static placeholder panel (no inputs in v1).",
      "Quick actions render disabled in v1 naming the endpoints they need; the admin build wires the actual flow.",
    ],
  },
];

const PHASE_4_TASKS = [
  {
    title: "Reports shell — unified filter row, date range, CSV export",
    subtitle: "Reports & analytics",
    path: "/payments/reports",
    why: "Every report on this page must be driven by the same control surface. One unified filter row + one date range + one Export-window button beats five different control bars.",
    terms: [
      ["scoped", "The shared transaction slice every report consumes."],
      ["Custom range popover", "Portaled date picker with From / To and quick presets (Last N min/hr/day)."],
      ["Promo code filter", "Four-mode filter (All / Any code applied / No code applied / Specific code) that narrows every report card to transactions that did / didn't / specifically used a given promotion code."],
    ],
    acceptance: [
      "Single-line filter row (Brand / Method / Provider / Country / Type / Status / Amount / Promo code). Brand is a normal dropdown.",
      "Daily / Weekly / Monthly segmented + Custom range button that opens the proper popover (no native prompt).",
      "Export window downloads scope as CSV with id / timestamp / brand / method / type / amount / currency / status / country / promo_code.",
      "Brand chip locks to the auto-login tenant when the top-right selector is not \"All brands\".",
      "Clear filters chip surfaces when any filter is set.",
      "Promo code filter has four modes; when \"Specific code\" is picked an autocomplete input appears with the full pool of codes ever seen in scope.",
    ],
    inputs: [
      { name:"Range — Daily / Weekly / Monthly / Custom", desc:"How far back every report reads. Daily = last 7 days (one bucket per day). Weekly = last 14 days. Monthly = last 30 days. Custom opens a From / To picker for an exact window.", notes:"Required. Default Weekly = last 14 days." },
      { name:"Custom range", desc:"From / To date picker for an exact reporting window. Useful for finance close-of-month reports or post-mortem on a specific incident window. Max 90 days.", notes:"Optional. Max 90 days." },
      { name:"Brand (filter)", desc:"Limit every report to one tenant. Pick \"All brands\" to see the network aggregate. Locked to a chip when the top-right brand selector is already on a specific tenant.", notes:"Optional. Locked when the top-right tenant is not \"All brands\"." },
      { name:"Method · Provider · Country · Type · Status", desc:"Same filter shape as Transactions. Method = payment instrument; Provider = the PSP that actually processed it; Country = player country; Type = Deposit vs Withdrawal; Status covers Balanced / To-Confirm / Pending / Failed (declined / rejected / errored grouped).", notes:"Optional. All default \"All\"." },
      { name:"Amount range — Min / Max", desc:"Narrow every report to transactions whose absolute amount falls inside the range. Leave either side blank for an open-ended range (e.g. ≥ 25,000 to find VIP-size transactions).", notes:"Optional. Open-ended when one side is left empty." },
      { name:"Promo code (filter)", desc:"Narrow every report to transactions that used a promotion / bonus code. Four modes: \"All transactions\" (no filter), \"Any code applied\" (only transactions that carry a promo code), \"No code applied\" (only transactions without one), and \"Specific code…\" (only transactions whose promo code matches the one you type). Picking \"Specific code\" reveals an autocomplete input listing every promo code ever seen in scope — useful for measuring exactly how a campaign performed (volume, profit, fees, conversion).", notes:"Optional. Default \"All transactions\". Matches the `promo_code` field on the transaction record; case-insensitive." },
      { name:"Export window", desc:"Download every transaction in the active scope as a single CSV — id, timestamp, brand, method, type, amount, currency, status, country, promo_code. Individual report cards have their own export buttons too.", notes:"Optional action. Downloads the full scope as CSV." },
    ],
    howItWorks: [
      "Page state holds the unified filter set + the range, including promo code mode + specific-code input.",
      "Every report receives a precomputed `scoped` array derived from the filters + window — including the promo code filter, so every tab (Financial, Fees, Conversion, etc.) automatically reflects the chosen campaign slice.",
      "Custom range button opens the CustomRangePopover; on apply, payload is adapted into { from, to, days, startMs, endMs } and the page recomputes.",
      "Export window downloads the entire scope as CSV.",
    ],
  },
  {
    title: "Fees tab — operator fee revenue per dimension",
    subtitle: "Reports → Fees",
    path: "/payments/reports?tab=fees",
    why: "Operators run on fee revenue. This tab makes the realised fee % per dimension (method, provider, brand, type, player, operator) instantly readable so the finance team can spot a method or PSP whose effective % has drifted out of the negotiated range — and the VIP / heavy-player desk can see who's generating the fees.",
    terms: [
      ["Operator fee revenue", "What the casino charged players in fees on settled transactions. fee = pct × amount + fixed."],
      ["Effective fee %", "Fees collected ÷ transaction volume. Compare to the negotiated headline rate per PSP."],
      ["Balanced = Approved", "Two names for the same terminal-success state in this engine. The Fees tab treats them identically and only sums fees on settled transactions."],
      ["Settled only", "Pending and to-confirm transactions aren't fees we've actually collected yet, so they're excluded; Failed transactions never produced any fee."],
    ],
    acceptance: [
      "3 headline KPI rows: Volume (Total deposits / Total withdrawals / Total volume / Effective fee %) and Fees (Total deposit fees / Total withdrawal fees / Total operator fees).",
      "6 drill-down tables with totals footer + CSV export: Fees by payment method / by provider / by brand / by transaction type / by player / by operator.",
      "Each row shows Deposit fees · Withdrawal fees · Total fees · Effective %.",
      "Tables sort by total fees descending.",
      "Honours the unified filter row at the top of the Reports page — changing the date range, method, provider, country, etc. recomputes every card.",
      "PSP cost / Net margin are NOT surfaced — the operator can't observe PSP cost directly, so the tab focuses on revenue collected.",
    ],
    inputs: [
      { name:"Settled transactions in scope", desc:"The Fees tab only sums fees on settled (Balanced / Approved) transactions — pending and to-confirm aren't fees we've actually collected yet, and failed transactions never produced any fee. The unified filter row still applies (narrow by method, provider, brand, country, etc.).", notes:"Required. The shared scope filtered to settled transactions for the fee totals." },
      { name:"Operator attribution", desc:"Each transaction maps to one back-office operator (the one who approved it, or the auto-approval engine when it was auto-approved). Drives the Fees by operator table. Real engine reads the Activity log entry; the v1 demo derives it deterministically by transaction id.", notes:"Optional. Falls back to a hash-based attribution in v1." },
    ],
    howItWorks: [
      "Filters the unified scope to settled-only (Balanced or Approved).",
      "Computes per-transaction operator fee (pct × amount + fixed) using the per-method fee model.",
      "Sums fee and volume by each dimension (method, provider, brand, type, player, operator).",
      "Renders the 2 KPI rows + 6 breakdown tables, each with a totals footer + CSV export.",
    ],
  },
  {
    title: "Financial Report tab — Total Deposits / Withdrawals / Profit",
    subtitle: "Reports → Financial Report",
    path: "/payments/reports?tab=financial",
    why: "Core CFO read of the platform: money in vs money out vs profit, plus fees collected on top. Broken down by brand, method, provider, type and across every status in the lifecycle so the operator can spot which dimension is dragging the number.",
    terms: [
      ["Profit", "Deposits − Withdrawals on settled transactions only. Excludes fees, bonuses, chargebacks."],
      ["Fees collected", "Operator fee revenue on the same settled set (4th headline KPI). Detail lives in the Fees tab."],
      ["Settled", "Balanced or Approved — the same terminal-success state in this engine. Pending / to-confirm are not money on the books yet."],
      ["Profit by status — all statuses", "Final table covers every status group (Balanced, Pending, To-Confirm, Failed) so non-settled traffic is still visible. Approved is folded into Balanced."],
    ],
    acceptance: [
      "4 KPIs at the top: Total deposits / Total withdrawals / Profit / Fees collected.",
      "4 dimension-breakdown tables with totals footer + CSV export: Profit by brand / by method / by provider / by transaction type.",
      "5th table: Profit by status — all statuses (Balanced = Approved, To-Confirm, Pending, Failed) with the tx count + per-direction split.",
      "Every table honours the unified filter row.",
      "Tables sort by profit descending.",
    ],
    inputs: [
      { name:"Settled transactions in scope", desc:"Financial Report only counts settled (Balanced) transactions toward the totals — pending and to-confirm transactions are not money on the books yet, and Failed transactions never reached the wallet. The unified filter row at the top of the page still applies (you can narrow by method, provider, etc.).", notes:"Required. The shared scope filtered to settled transactions for the financial totals." },
    ],
    howItWorks: [
      "Filters scoped to settled-only.",
      "Sums deposits and withdrawals; profit = deposits − withdrawals.",
      "Groups by each of the five dimensions and renders.",
    ],
  },
  {
    title: "Conversion tab — approval funnel",
    subtitle: "Reports → Conversion",
    path: "/payments/reports?tab=conversion",
    why: "Spot which method or PSP is silently dropping conversions. A 1-point drop in approval rate on a high-volume method is worth more than any other lever the team has.",
    terms: [
      ["Approval rate (operator decision)", "Approved ÷ (Approved + Rejected). Measures the operator decision step (auto-approval engine + manual approvals), NOT the PSP outcome. After approval a transaction is sent to the PSP and may still turn into Balanced or Failed."],
      ["Balanced (PSP outcome)", "Terminal success state — the PSP confirmed the transaction settled. Different from approval: a transaction can be approved by the operator and still end up Failed if the PSP later rejects it."],
      ["Decline reason", "Canonical reason code from the engine — LIMIT_IWAKIRI / LIMIT_PROVIDER / NETWORK_PROVIDER / TIMEOUT_PROVIDER / REJECTED_OPERATOR / NETWORK_IWAKIRI."],
    ],
    acceptance: [
      "Approval rate trend bars across the window.",
      "Top decline reasons ranked by count.",
      "Methods ranked by approval rate.",
      "PSP performance with p95 latency, decline mix bars, and paused flag.",
    ],
    inputs: [
      { name:"Window scope (from Reports shell)", desc:"The Conversion tab does not have its own controls — it consumes the unified filter row + date range at the top of the Reports page. Approval-rate trend, decline reasons, per-method ranking and PSP performance all recompute on the same scope.", notes:"Required. Receives the precomputed transaction slice + window length + start / end. Every card re-renders when any of these change." },
    ],
    howItWorks: [
      "Computes ok / (ok+bad) per day, per method, per PSP.",
      "Pulls DECLINE_REASONS canonical 6 and aggregates counts.",
      "PSP performance reads MOCK_PSP_PROFILES.health for live metrics.",
    ],
  },
  {
    title: "Players tab",
    subtitle: "Reports → Players",
    path: "/payments/reports?tab=players",
    why: "Who's contributing what. Top depositors and withdrawers are immediate VIP signal; net positions surface both the best customers and the biggest payout risks. Every name is clickable into Player 360 so the operator can act on it in one click.",
    terms: [
      ["Players in scope", "Distinct player accounts that appear in the unified transaction scope."],
      ["Most active", "Ranked by transaction count in the window — catches whales, bots, and promo grinders."],
      ["Top net positions", "Ranked by lifetime-in-window deposits − withdrawals. Positive = casino is up on that player; negative = player is net-up (heavy winner)."],
    ],
    acceptance: [
      "4-KPI totals strip: Players in scope · Total deposits · Total withdrawals · Net.",
      "4 ranked lists: most active, biggest depositors, biggest withdrawers, top net positions.",
      "Each list starts collapsed at top 15 and expands to the full sorted list on demand via a Show-all toggle.",
      "Player names are clickable; click routes to /payments/players/:playerId and opens Player 360 with the rich record.",
    ],
    inputs: [
      { name:"Window scope + drill-in handler", desc:"Players reads the same scope as every other report. The drill-in handler routes a click on a player name from Reports → Players list → Player 360 inside PayBO so the operator stays in one tab.", notes:"Required. The shared transaction scope drives every card; the drill-in handler routes Reports → Players list → Player 360." },
    ],
    howItWorks: [
      "Aggregates per user_id over the scope; computes count, deposit total, withdrawal total, net.",
      "Sorts four different ways for the four lists.",
      "On row click, resolves the rich player record from PLAYERS_LIST and calls onOpenPlayer — app.jsx sets the selected player + active page and Player 360 renders.",
    ],
  },
  {
    title: "Geo tab",
    subtitle: "Reports → Geo",
    path: "/payments/reports?tab=geo",
    why: "Where the money flows by player country. Surfaces high-volume geos, VIP-heavy countries (by avg ticket), and the deposits vs withdrawals net per country for capital-flow visibility.",
    terms: [
      ["Top countries", "Ranked by total transacted volume (deposits + withdrawals) in the window."],
      ["Avg deposit ticket by country", "Mean deposit size per country — flags VIP-heavy geos with smaller player counts but higher per-tx amounts."],
      ["Net by country", "Deposits − Withdrawals per country, sorted by net so positive flows surface first."],
    ],
    acceptance: [
      "Top countries card with a volume bar per country.",
      "Avg deposit ticket per country card with a separate bar viz.",
      "Deposits, withdrawals & net by country table with a totals footer + CSV export.",
      "All three honour the unified filter row.",
    ],
    inputs: [
      { name:"Window scope (from Reports shell)", desc:"Geo reads the unified scope. Set the Country filter in the row above to drill the Reports page to one country, or leave it on \"All countries\" to compare geos.", notes:"Required. Window scope provided by Reports." },
    ],
    howItWorks: [
      "Aggregates per ISO country code over the scope; ranks by total volume.",
      "Computes avg deposit ticket per country (deposit total ÷ deposit count).",
      "Renders the three cards with shared formatting.",
    ],
  },
  {
    title: "Liquidity tab — treasury view of every PSP",
    subtitle: "Reports → Liquidity",
    path: "/payments/reports?tab=liquidity",
    why: "Treasury needs one screen that says, today, which PSP wallets to sweep idle capital out of and which to top up before withdrawals start failing. Reads the same PSP directory the routing engine and the provider editor read from, so the numbers always agree.",
    terms: [
      ["Locked in PSP wallets", "Sum of every active PSP's live wallet balance — money that is not currently in the operator's bank account."],
      ["Free headroom", "Sum of (ceiling − wallet) across PSPs. The total capacity the network can absorb before sweep alarms start firing."],
      ["Sweep candidate", "A PSP whose wallet has reached the sweep line (wallet ≥ sweepPct × ceiling) and where treasury should pull funds out into the bank."],
      ["Below floor", "A PSP whose wallet has dropped below the operator-set floor. Withdrawals will start failing soon — top up."],
      ["Settlement aging", "Pending settled-but-not-yet-credited funds, and how many days they've been pending versus the PSP's committed T+N SLA."],
    ],
    acceptance: [
      "4 KPIs: Locked in PSP wallets · Free headroom · PSPs above floor (count + %) · Sweep candidates.",
      "Per-PSP wallet table — state chip (Healthy / Sweep due / Below floor), wallet vs floor → ceiling bar with sweep marker, wallet + free headroom + settlement + payout columns. Totals footer + CSV export.",
      "Recent sweep & top-up events list — last 30-ish days, with kind chip (Sweep / Top-up), amount, PSP, operator. CSV export.",
      "Settlement aging table — pending funds per PSP vs T+N SLA; breaches surface in red. CSV export.",
      "Honours the brand and scope context (active PSPs only; PSPs paused or unavailable are excluded automatically).",
    ],
    inputs: [
      { name:"PSP directory (live)", desc:"Reads MOCK_PSP_PROFILES — the same source the routing engine and the Provider editor read from. Wallet balance, floor, ceiling, sweep %, min withdrawable and settlement / payout settings all come from each PSP's record.", notes:"Required. Live snapshot." },
    ],
    howItWorks: [
      "For every active PSP, computes: sweep line = ceiling × sweepPct / 100; headroom = ceiling − wallet; state = below_floor if wallet < floor, needs_sweep if wallet ≥ sweep line, healthy otherwise.",
      "Renders the 4 KPIs from the aggregated rows.",
      "Per-PSP table renders the bar viz with two markers (floor in red, sweep in amber) so the treasury team reads state at a glance.",
      "Recent events list is synthesised in v1 against the PSP set; admin build wires the real treasury event stream.",
      "Settlement aging computes pending = wallet × seed; flags aging > SLA as breached.",
    ],
  },
  {
    title: "Ops & Queue tab",
    subtitle: "Reports → Ops & Queue",
    path: "/payments/reports?tab=ops",
    why: "Back-office productivity and the manual-review queue snapshot. Tells the COO whether the team is keeping up with the queue and which operator is the most active.",
    terms: [
      ["To-Confirm queue", "Manual-review queue snapshot at end of the window — count + age of oldest entry + total amount at risk."],
      ["Operator actions", "Approvals + Rejections + Method edits + PSP logins + Notes recorded in the Activity log."],
    ],
    acceptance: [
      "3-KPI strip: queue count + oldest age, oldest in queue, operator-action total with approve / reject mix.",
      "Per-action-type ranked list (Approvals, Rejections, PSP logins, Method edits, Notes).",
      "Honours the unified filter row.",
    ],
    inputs: [
      { name:"Window scope", desc:"Reads the unified scope to filter the To-Confirm snapshot and aggregates ACTIVITY entries for the same window.", notes:"Required. Window scope provided by Reports." },
    ],
    howItWorks: [
      "Snapshots the To-Confirm queue from scoped (status = to_confirm).",
      "Aggregates the ACTIVITY log by action.kind.",
      "Renders KPIs + the per-action bar list.",
    ],
  },
];

const PHASE_5_TASKS = [
  {
    title: "Dashboard — live KPIs + smart widgets",
    subtitle: "Dashboard",
    path: "/payments/dashboard",
    why: "Home page of the back office. Every operator opens this first thing in the morning. Has to be glanceable and actionable.",
    terms: [
      ["Status timeframe widget", "Donut + group bar + status pills tied to the same window."],
      ["Country breakdown widget", "Per-country deposits / withdrawals vertical bars with hover details."],
    ],
    acceptance: [
      "4 KPIs (Deposits / Withdrawals / Net / Approval) with sparklines.",
      "Volume bar panel (hourly / daily) with hover tooltips.",
      "Transaction status widget (donut + grouped bar + status pills).",
      "Decline reasons widget with drill-down per method / provider.",
      "Recent transactions strip.",
    ],
    inputs: [
      { name:"Timeframe", desc:"How far back the Dashboard reads. Pick a preset (1 hour, 24h, 7d, 14d, 30d) or Custom for an exact From / To window. Every widget on the page — KPIs, charts, country breakdown, decline reasons — recomputes in lock-step.", notes:"Required enum: 1h / 24h / 7d / 14d / 30d / Custom. Default 14d." },
      { name:"Custom range", desc:"Exact From / To window for the Dashboard. Useful when you want the dashboard scoped to a specific incident window or a financial close.", notes:"Optional. From / To window set by the popover when Timeframe = Custom." },
    ],
    howItWorks: [
      "Page holds tf + customRange state.",
      "Each widget receives the precomputed window + scope.",
      "DASH series are pre-aggregated; widgets render the slice.",
    ],
  },
  {
    title: "Transactions list + Withdrawals (To-Confirm) queue",
    subtitle: "Payments → Transactions / Withdrawals / Deposits",
    path: "/payments/transactions · /payments/deposits · /payments/withdrawals",
    why: "Operational log of everything that's hit the engine, with the manual-review queue spec baked in: To-Confirm sorts by amount DESC then created_at ASC so the biggest payouts surface first.",
    terms: [
      ["To-Confirm", "Withdrawals (and exceptional deposits) paused for operator approval."],
      ["Status timeline", "Per-tx audit of every state the transaction has been in, with the actor + reason."],
      ["Promo code column", "Per-row chip showing the promotion / bonus code applied at deposit time (or an em-dash if none). Hoverable for the full code; included in CSV export."],
    ],
    acceptance: [
      "Full transaction list with status / type / method / provider / country / amount / date filters.",
      "Column visibility popover; persisted per operator.",
      "Promo code column is optional in the visibility popover; renders a monospace chip when a code was applied and an em-dash when not.",
      "Promo code value is included in the CSV export.",
      "To-Confirm tab applies the spec-mandated sort (amount DESC, then created_at ASC).",
      "Approve / Reject actions write to the Activity log with operator id + reason.",
    ],
    inputs: [
      { name:"Filters — Search · Status · Brand · Method · Provider · Environment · Type · Amount range · Timeframe", desc:"Same model as the Reports filter row but applied to the live transactions log. Status covers the full lifecycle (Balanced / To-Confirm / Pending / Failed). Environment splits production from staging. The To-Confirm tab forces the spec-mandated queue sort: amount DESC, then created_at ASC (biggest payouts first).", notes:"Optional. Same model as the Reports filter row." },
      { name:"Columns", desc:"Hide columns you don't need to free up table width. Brand, ID and Actions stay mandatory. Promo code is opt-in. Layout is saved per operator across sessions.", notes:"Optional. Per-operator visibility toggles persisted to localStorage." },
      { name:"Promo code (column)", desc:"Read-only column that shows whether a promotion / bonus code was applied to the transaction. Deposits may carry a code (e.g. WELCOME100, RELOAD25); withdrawals never do. Renders as a monospace chip with the code and a check icon; hovering shows the full code. An em-dash means no code was applied. Useful for spotting promo-driven deposit traffic at a glance in the live transactions log; for aggregate analysis use the Reports promo code filter.", notes:"Optional column. Reads the `promo_code` field on the transaction record. Included in CSV export." },
      { name:"Approve / Reject (action)", desc:"Available on To-Confirm rows. Each action opens the decision modal (free-text note, min 10 chars) and writes an immutable Activity-log entry with the operator id + timestamp + full note. See Foundation 3 for the lifecycle details.", notes:"Optional. Available on To-Confirm rows. Both write an entry to the Activity log with operator id + note." },
    ],
    howItWorks: [
      "Filters MOCK.TRANSACTIONS by the unified filter set.",
      "To-Confirm tab applies window.PAYBO.queueSort (Amount DESC, then created_at ASC).",
      "Promo code column reads `t.promo_code` on each transaction; renders a chip when present, em-dash when null.",
      "Approve / Reject calls window.PAYBO.transitionStatus, which records the change in the Activity log and re-renders the table.",
    ],
  },
  {
    title: "Activity log — immutable audit",
    subtitle: "Activity log",
    path: "/payments/activity",
    why: "Compliance source-of-truth. Every operator decision (approve / reject / psp_login / method_edit / note) is recorded with actor + timestamp + diff. Read-only by spec — the log is regulatory.",
    terms: [
      ["Audit entry", "One row per operator action, immutable. action.kind drives the icon and chip."],
      ["PSP login", "An operator authenticated against a PSP's portal from inside PayBO — recorded for SOX / compliance."],
    ],
    acceptance: [
      "Search by operator / tx id / player.",
      "Operator filter dropdown.",
      "Custom date range via the same popover used elsewhere.",
      "CSV export bundles the filtered slice.",
      "Read-only — no edit / delete affordances anywhere on the page.",
    ],
    inputs: [
      { name:"Search", desc:"Free-text search across operator name, transaction id, and player name. Type any fragment to narrow the log to matching events.", notes:"Optional free-text. Matches across operator name, tx id, player name." },
      { name:"Operator", desc:"Drop the log to a single back-office user — useful when auditing one team member's approvals / rejections, or when investigating a suspected compromised account.", notes:"Optional. Drops the log to a single back-office user." },
      { name:"Custom range", desc:"Filter the log to events that happened in a specific From / To window. Same popover used everywhere else in the platform.", notes:"Optional From / To window set by the popover." },
      { name:"Export CSV", desc:"Download the filtered slice as a single CSV (timestamp · actor id · actor name · actor role · action · verb · transaction id · player · brand · amount · detail). Required for compliance evidence handover.", notes:"Optional action. Bundles the filtered slice into a single CSV." },
    ],
    howItWorks: [
      "Filters MOCK.ACTIVITY by the criteria above.",
      "Renders rows with operator chip, action verb, target (tx or player), reason, timestamp.",
      "Export packs the filtered slice into a single CSV.",
    ],
  },
  {
    title: "Top-bar brand switcher (auto-login) + Frontend preview",
    subtitle: "Top-right brand selector / Frontend",
    path: "/payments/frontend  ·  brand selector global on all paths",
    why: "Operator scopes the entire back office to one tenant with one click; the Frontend page mirrors what that brand's player sees. Same data, two perspectives.",
    terms: [
      ["Auto-login", "Picking a brand at the top right scopes every page to that tenant and locks per-page brand filters."],
      ["Frontend preview", "Player-facing UI rendered live from the active brand's method catalog."],
    ],
    acceptance: [
      "Switching brand scopes every page consistently.",
      "Per-page brand filters lock or hide when the top-right scope is set.",
      "Brand switcher hover surfaces an explainer tooltip about the auto-login behaviour.",
      "Frontend page mirrors the brand's active method catalog and currencies.",
    ],
    inputs: [
      { name:"Brand selector (top-right)", desc:"The single most important global control. Picking a specific tenant scopes the WHOLE back office to that brand (transactions, players, methods, routes, reports — everything) and locks every per-page brand filter. Picking \"All brands\" restores the aggregate network view where per-page filters become available again.", notes:"Required. Active brand drives every page. brand.isAll = aggregate network view." },
    ],
    howItWorks: [
      "app.jsx holds the active brand in state; switcher modal sets it.",
      "Every page consumes the `brand` prop; reads brand.isAll to toggle filter visibility.",
      "Frontend page filters MOCK.METHODS to the active brand and renders the player-side UI.",
    ],
  },
  {
    title: "Help system — Tip badges + Explainer panels everywhere",
    subtitle: "ui.jsx → window.Tip + window.Explainer",
    path: "Global UI primitive — used on every /payments/* path",
    why: "Tech team needs to self-serve without a docs site; operators need plain-English context on every control. Two reusable building blocks, ship them once, drop them in everywhere.",
    terms: [
      ["Tip", "Small italic-i badge that hovers / focuses to show a portaled popover. Never clipped by parents."],
      ["Explainer", "Soft-tinted info panel with title + lead + optional bullet list. The 'What this is, in plain English' card."],
    ],
    acceptance: [
      "Tip portaled to document.body, smart-positioned (flips above / below), repositions on scroll + resize.",
      "Explainer ships on every major page (Dashboard, Transactions, Methods, Players, Player 360, Reports, Activity, Settings).",
      "Every modal opens with an Explainer + Tips on every field.",
      "Both components exposed on window for future pages with zero imports.",
    ],
    inputs: [
      { name:"Tip badge — popover content", desc:"The free-form content shown inside the popover when the operator hovers / focuses the lowercase 'i' badge. Use plain English, one short paragraph, no jargon. Renders portaled to the body so it's never clipped by a parent's overflow.", notes:"Required. Max 300px wide; flips below the badge when there isn't enough room above." },
      { name:"Explainer panel — title, lead, bullets", desc:"The 'What this is, in plain English' card that sits at the top of every major page, tab, and modal. Use it to tell a brand-new operator what they're looking at in 3 sentences and a short bullet list of the key terms.", notes:"Title defaults to \"What this is, in plain English\". Lead is the main paragraph; bullets are an optional list under the lead." },
    ],
    howItWorks: [
      "Tip uses getBoundingClientRect + window dims to compute viewport coords; flips placement when there's no room above.",
      "Explainer renders a primary-tinted panel with an info icon, title, lead text, and an optional bulleted list.",
      "Both live in ui.jsx and are exposed on window.Tip / window.Explainer; ui.jsx loads before every page so the globals are always available.",
    ],
  },
];

/* ============================================================
   DevPhase — page render
   ============================================================ */

const DevPhase = ({ brand }) => {
  window.useLocale && window.useLocale();
  const T = window.T || ((k, fb) => fb || k);

  return (
    <div className="page">
      <div className="page__header">
        <div>
          <div className="page__title" style={{display:"inline-flex", alignItems:"center"}}>
            {T("page.devCto","Development · CTO briefing")}
            <Tip>The complete map of PayBO for the engineering lead. Every shipped feature is written up as a task with a plain-English title, the business reason, the key terms, testable acceptance criteria, every input field (type + validation + required), and a numbered logic flow. Two foundational panels at the top explain Routing &amp; Cascading and Providers in operational detail.</Tip>
          </div>
          <div className="page__subtitle">5 phases · {PHASE_1_TASKS.length + PHASE_2_TASKS.length + PHASE_3_TASKS.length + PHASE_4_TASKS.length + PHASE_5_TASKS.length} tasks · every one in Title · Why · Key terms · Acceptance · Inputs · How it works shape</div>
        </div>
        <div className="page__actions">
          <span className="paybo-v2__tag"><Icon name="info" size={10}/> Internal · CTO &amp; engineering</span>
        </div>
      </div>

      <div style={{display:"flex", alignItems:"center", gap:10, padding:"10px 14px", borderRadius:10,
        background:"var(--warn-50, #fef3c7)", border:"1px solid var(--warn-200, #fde68a)", marginBottom:14, fontSize:12.5, color:"var(--warn-700, #92400e)"}}>
        <Icon name="info" size={14}/>
        <div style={{flex:1, lineHeight:1.5}}>
          <strong>Informational preview page.</strong> This page is not part of the operator's daily workflow — it is a CTO / engineering reference. It opens in a separate browser tab so it does not interfere with the main back office. Live data is read-only.
        </div>
      </div>

      {/* ============================================================
          How to read this doc
          ============================================================ */}
      <div className="paybo-devcard">
        <h3>How to read this doc</h3>
        <p style={{fontSize:13, color:"var(--text-secondary)", lineHeight:1.6}}>
          Every task in every phase follows the same six-part shape your team agreed on:
        </p>
        <ol style={{fontSize:13, color:"var(--text-secondary)", lineHeight:1.7, marginTop:6}}>
          <li><strong>Task Title</strong> — plain English, action-first.</li>
          <li><strong>Why This Matters</strong> — 1–2 sentence business reason.</li>
          <li><strong>Key Terms Used</strong> — short glossary of the gambling / PSP terms in the task.</li>
          <li><strong>Acceptance Criteria</strong> — 3–6 testable bullets defining "done".</li>
          <li><strong>Inputs</strong> — every field / parameter with type, validation, required / optional, defaults and edge cases.</li>
          <li><strong>How It Works (Step by Step)</strong> — numbered logic flow in plain English.</li>
        </ol>
        <p style={{fontSize:12.5, color:"var(--text-tertiary)", marginTop:10, lineHeight:1.5}}>
          The two foundation panels below (Routing &amp; Cascading · Provider registry) belong to <em>every</em> task — they explain the core engine logic the rest of the platform plugs into.
        </p>
      </div>

      {/* ============================================================
          FOUNDATION 1 · Routing & Cascading — operational summary
          ============================================================ */}
      <DV_SECTION_HEAD
        kicker="Foundation 1"
        title="Routing & Cascading — flows, triggers, actions"
        subtitle="Operational summary the dev team can build against. Two engines: Routing (proactive optimisation — who gets the transaction first) and Cascading (failure recovery — what to do when a PSP fails)."/>

      <div className="paybo-devcard">
        <h3>1 · Objective</h3>
        <ul style={{lineHeight:1.7}}>
          <li>Maximise deposit conversion rate.</li>
          <li>Optimise costs and liquidity.</li>
          <li>Ensure operational continuity.</li>
          <li>Provide operator control over PSP allocation.</li>
        </ul>
      </div>

      <div className="paybo-devcard">
        <h3>2 · Routing</h3>
        <p><strong>Flow.</strong></p>
        <ol style={{lineHeight:1.7}}>
          <li>Receive request (deposit / withdrawal).</li>
          <li>Identify: payment method · country (GEO) · amount.</li>
          <li>Filter compatible PSPs.</li>
          <li>Apply constraints (limits, licenses, availability).</li>
          <li>Apply ranking / scoring.</li>
          <li>Select PSP.</li>
          <li>Send transaction.</li>
        </ol>

        <p style={{marginTop:14}}><strong>Provider Ranking (key feature).</strong> Ability to assign a ranking to providers supporting the same payment method.</p>
        <ul style={{lineHeight:1.7}}>
          <li><strong>Objective</strong> — give direct control to the operator; influence routing decisions beyond automated logic.</li>
          <li><strong>Ranking types</strong> — manual (fixed priority, e.g. PSP A &gt; PSP B &gt; PSP C); hybrid (manual + score); context-based (by GEO, by payment method, by transaction amount).</li>
        </ul>

        <p style={{marginTop:14}}><strong>Provider Configuration (key point).</strong> To support ranking and advanced routing, each PSP must have a complete configuration profile.</p>
        <ul style={{lineHeight:1.7}}>
          <li><strong>Costs</strong> — deposit fee (percentage / flat); withdrawal fee; settlement / transfer costs.</li>
          <li><strong>Limits</strong> — min/max deposit, min/max withdrawal, daily / monthly limits.</li>
          <li><strong>Liquidity</strong> — minimum transferable amount from PSP wallet to operator wallet (critical); payout capacity.</li>
          <li><strong>Supported GEOs</strong> — enabled countries.</li>
          <li><strong>Licenses</strong> — supported license types (MGA, Curaçao, Anjouan, …).</li>
          <li><strong>Supported payment methods</strong> — explicit method list.</li>
          <li><strong>Technical configuration</strong> — endpoints, timeout settings, fallback priority.</li>
        </ul>

        <p style={{marginTop:14}}><strong>Main triggers (routing).</strong></p>
        <ul style={{lineHeight:1.7}}>
          <li><strong>Performance</strong> — decreasing success rate; increasing timeout rate.</li>
          <li><strong>Costs</strong> — lower fees should increase priority.</li>
          <li><strong>Liquidity (key)</strong> — PSP wallet balance, transfer capacity, minimum thresholds.</li>
          <li><strong>Limits</strong> — automatic validation of min / max constraints.</li>
          <li><strong>GEO and License (key)</strong> — PSP must be compatible with user GEO and operator license.</li>
          <li><strong>Technical status</strong> — PSP downtime or instability leads to exclusion.</li>
        </ul>

        <p style={{marginTop:14}}><strong>Actions (routing).</strong></p>
        <ul style={{lineHeight:1.7}}>
          <li>Select optimal PSP.</li>
          <li>Apply operator-defined ranking.</li>
          <li>Balance traffic across providers.</li>
          <li>Allocate funds across PSPs (key requirement).</li>
        </ul>

        <p style={{marginTop:14}}><strong>Key points routing.</strong> Decision happens <em>before</em> transaction execution. Manual ranking is a strategic control lever. Requires complete PSP configuration. Must include liquidity, cost, GEO, and license considerations.</p>
      </div>

      <div className="paybo-devcard">
        <h3>3 · Cascading</h3>
        <p><strong>Flow.</strong></p>
        <ol style={{lineHeight:1.7}}>
          <li>Send transaction to PSP (from routing).</li>
          <li>Receive response.</li>
          <li>Success → stop.</li>
          <li>Failure → analyse error.</li>
          <li>Classify error.</li>
          <li>Decide retry (yes / no).</li>
          <li>If yes → select next PSP.</li>
          <li>Stop after max retries.</li>
        </ol>

        <p style={{marginTop:14}}><strong>Main triggers (cascading) — Error type matrix (key point).</strong></p>
        <table className="data-table" style={{marginTop:6}}>
          <thead><tr><th>Error type</th><th>Action</th></tr></thead>
          <tbody>
            <tr><td>Timeout / Network</td><td>Retry</td></tr>
            <tr><td>PSP down</td><td>Retry</td></tr>
            <tr><td>PSP limit reached</td><td>Retry with another PSP</td></tr>
            <tr><td>Insufficient funds</td><td>Stop</td></tr>
            <tr><td>Fraud decline</td><td>Depends</td></tr>
          </tbody>
        </table>

        <p style={{marginTop:14}}><strong>Retry policy.</strong></p>
        <ul style={{lineHeight:1.7}}>
          <li>Maximum 2–3 retries.</li>
          <li>Always use different PSPs.</li>
          <li>Avoid loops.</li>
        </ul>

        <p style={{marginTop:14}}><strong>Actions (cascading).</strong></p>
        <ul style={{lineHeight:1.7}}>
          <li>Automatic retry.</li>
          <li>Switch PSP (respecting ranking and constraints).</li>
          <li>Controlled stop.</li>
        </ul>

        <p style={{marginTop:14}}><strong>Key points cascading.</strong> Triggered after failure. Directly improves conversion rate. Must be selective (not all failures should be retried). Must respect ranking and PSP constraints.</p>
      </div>

      <div className="paybo-devcard">
        <h3>4 · Routing + Cascading (integrated view)</h3>
        <p><strong>Combined flow.</strong></p>
        <ol style={{lineHeight:1.7}}>
          <li>Routing selects PSP (based on ranking and scoring).</li>
          <li>Transaction attempt.</li>
          <li>On failure → cascading logic.</li>
          <li>Select next valid PSP (based on ranking and rules).</li>
          <li>Repeat until success or stop.</li>
        </ol>
        <p style={{marginTop:10}}><strong>Key points global.</strong> Routing = proactive optimisation. Cascading = failure recovery. Ranking = operator control. Provider configuration = system foundation.</p>
      </div>

      <div className="paybo-devcard">
        <h3>5 · Best practices</h3>
        <ul style={{lineHeight:1.7}}>
          <li>Enable operator-configurable ranking.</li>
          <li>Maintain complete and up-to-date PSP configuration.</li>
          <li>Use hybrid logic (manual + automated).</li>
          <li>Implement real-time monitoring.</li>
          <li>Integrate liquidity management (critical).</li>
          <li>Enforce GEO and license constraints.</li>
        </ul>
      </div>

      {/* ============================================================
          FOUNDATION 2 · Provider registry + liquidity management
          ============================================================ */}
      <DV_SECTION_HEAD
        kicker="Foundation 2"
        title="Provider registry — methods, processing times, license operations & live liquidity"
        subtitle="Disregarding Routing and Cascading, the provider registry also has to surface the information the operator needs when picking which PSPs to use for a given method — and the live wallet balance so liquidity stays under active control."/>

      <div className="paybo-devcard">
        <h3>1 · Provider registry — information the operator needs</h3>
        <p>The provider registry supports the methods. Certain information must be visible to the operator when defining the portfolio of providers to be used for the same method:</p>
        <ul style={{lineHeight:1.7}}>
          <li><strong>Processing times</strong> and <strong>minimum withdrawable amount</strong> per provider — e.g. <code>T+0</code>, <code>$5,000</code>.</li>
          <li><strong>Operations supported under each license</strong> — Anjouan, Curaçao, MGA, etc. (deposit, withdrawal, refund).</li>
        </ul>
      </div>

      <div className="paybo-devcard">
        <h3>2 · Live liquidity management</h3>
        <p>For the different providers, it is important to have real-time visibility of the available balance on each provider wallet, in order to allow timely intervention and active liquidity management.</p>
        <p><strong>Objective.</strong> Avoid keeping an excessive amount of funds locked within provider wallets, also considering the concrete risk that some providers may become unavailable or blocked.</p>
        <p><strong>Mechanism.</strong></p>
        <ul style={{lineHeight:1.7}}>
          <li>Define, for each provider and payment method, a <strong>maximum balance threshold</strong> for the wallet.</li>
          <li>The system supports <strong>configurable notifications</strong> via Slack or other channels — e.g. when 80% of the configured threshold is reached, allowing the operator to promptly plan withdrawal and fund rebalancing activities.</li>
          <li>This works in the <strong>opposite direction</strong> too: when withdrawal volumes exceed the average and some provider wallets are close to depletion, the operator is notified in advance and enabled to transfer funds to the provider wallets — preventing withdrawal requests from being rejected due to lack of liquidity.</li>
        </ul>
        <p><strong>Implementation today.</strong> Both directions are wired through the Provider Liquidity Alarms (Phase 1 · Task 4). Operator sets the Floor, Ceiling and Min withdrawable (currency-neutral integers — whatever the PSP's wallet currency is), then adds one or more rules from the 26 available metrics (Wallet near ceiling at 80%, Wallet below floor, Liquidity dropped by, Payout capacity below, Settlement delay, etc.) with their own Slack / email / Telegram destinations.</p>
      </div>

      {/* ============================================================
          FOUNDATION 3 · Transaction lifecycle (To-Confirm / Approve /
          Partial Approve / Reject)
          ============================================================ */}
      <DV_SECTION_HEAD
        kicker="Foundation 3"
        title="Transaction lifecycle — To-Confirm, Approve, Partial Approve, Reject"
        subtitle="What each status means, how the operator decisions move a transaction through the engine, and what the PSP step does after the operator decision."/>

      <div className="paybo-devcard">
        <h3>1 · Two distinct steps</h3>
        <p>Every transaction goes through <strong>two steps</strong> and they must not be confused:</p>
        <ol style={{lineHeight:1.7}}>
          <li><strong>Operator decision</strong> — auto-approval engine or human operator decides whether the transaction can be passed to the PSP. Outcomes: <em>Approved</em>, <em>Partial-approved</em>, or <em>Rejected</em>. <strong>Approval rate</strong> measures this step.</li>
          <li><strong>PSP outcome</strong> — only runs for transactions the operator approved. The PSP confirms (Balanced) or refuses / errors (Failed). <strong>Balanced</strong> measures this step.</li>
        </ol>
        <p>So a transaction can be <em>Approved</em> by the operator and still end up <em>Failed</em> if the PSP later rejects it. The two rates (Approval rate and Balanced rate) are not the same metric.</p>
      </div>

      <div className="paybo-devcard">
        <h3>2 · To Confirm</h3>
        <p><strong>Meaning.</strong> The transaction is paused in the manual-review queue waiting for an operator to take a decision (Approve, Partial Approve, or Reject). The PSP has not seen the transaction yet.</p>
        <p><strong>When a transaction lands in To Confirm.</strong></p>
        <ul style={{lineHeight:1.7}}>
          <li>Withdrawal amount &gt; the method's auto-approve threshold.</li>
          <li>Deposit / withdrawal flagged by a fraud rule (high-risk player, suspicious geo, velocity).</li>
          <li>Route's execution mode is <code>MANUAL</code> — the operator must approve every step of the cascade.</li>
          <li>Player is on a watchlist or operator-suspended list.</li>
        </ul>
        <p><strong>Queue sort.</strong> Spec-mandated: <em>amount DESC, then created_at ASC</em> — biggest payouts surface first, ties broken by oldest age.</p>
        <p><strong>SLA.</strong> Critical-severity alarm fires when the oldest To-Confirm transaction crosses 30 minutes (configurable in Settings → Alarms → <em>To-Confirm queue age</em>).</p>
      </div>

      <div className="paybo-devcard">
        <h3>3 · Approve (full)</h3>
        <p><strong>What it does.</strong> Operator releases the transaction from the To-Confirm queue at its full amount. It is then sent to the PSP through the route's chain.</p>
        <p><strong>Flow.</strong></p>
        <ol style={{lineHeight:1.7}}>
          <li>Operator clicks Approve on the transaction row → modal opens.</li>
          <li>Operator writes a free-text note (min 10 chars) explaining the decision.</li>
          <li>On Confirm, the engine writes an immutable Activity-log entry: operator ID + timestamp + before-state (<code>to_confirm</code>) + after-state (<code>approved</code>) + full note text.</li>
          <li>Transaction status flips to <code>approved</code> and the routing engine sends it to the first PSP in the chain.</li>
          <li>PSP outcome — if the PSP confirms, status becomes <strong>Balanced</strong>; if the PSP refuses / errors, status becomes <strong>Failed</strong> (cascade may retry the next PSP first, depending on the route's triggers).</li>
        </ol>
        <p><strong>Counts toward.</strong> Approval rate (numerator). Balanced rate counts it only if the PSP confirms in step 5.</p>
      </div>

      <div className="paybo-devcard">
        <h3>4 · Partial Approve</h3>
        <p><strong>What it does.</strong> Operator releases the transaction for a smaller amount than the player requested. The remainder is recorded as a partial reject.</p>
        <p><strong>When to use it.</strong> Common on withdrawals: a player asks to cash out 10,000 but their available-to-withdraw balance after wagering requirements is only 7,500 — operator partial-approves 7,500 and the system logs the 2,500 remainder as <em>Partial reject</em>.</p>
        <p><strong>Flow.</strong></p>
        <ol style={{lineHeight:1.7}}>
          <li>Operator clicks Partial Approve on the To-Confirm row → modal opens.</li>
          <li>Operator enters the <em>Approved amount</em> (must be &gt; 0 and &lt; the original amount).</li>
          <li>Operator writes the note (min 10 chars).</li>
          <li>On Confirm, two Activity-log entries are written:
            <ul>
              <li>OPERATOR action <code>OPERATOR_PARTIAL_APPROVED</code> with the approved amount.</li>
              <li>SYSTEM action <code>PARTIAL_REJECT</code> with the remainder, so the rejected portion is auditable.</li>
            </ul>
          </li>
          <li>The approved-amount slice is sent to the PSP exactly like a full approval; PSP outcome decides Balanced vs Failed.</li>
        </ol>
        <p><strong>Counts toward.</strong> Approval rate (numerator) for the approved portion. The remainder counts toward the Reject side of the operator-decision step.</p>
      </div>

      <div className="paybo-devcard">
        <h3>5 · Reject</h3>
        <p><strong>What it does.</strong> Operator refuses the transaction. It is <em>not</em> sent to the PSP. Terminal state.</p>
        <p><strong>When to use it.</strong> Failed KYC, hard fraud signal, exceeded operator-side limits the engine should have caught, manually-blocked method, requested-by-player cancellation, or any case where the transaction should not reach any PSP.</p>
        <p><strong>Flow.</strong></p>
        <ol style={{lineHeight:1.7}}>
          <li>Operator clicks Reject on the row → modal opens.</li>
          <li>Operator writes the note (min 10 chars) — this is the rationale that the player support team will reference if the player calls in.</li>
          <li>On Confirm, the Activity log captures operator ID + timestamp + before-state (<code>to_confirm</code>) + after-state (<code>rejected</code>) + full note text.</li>
          <li>Transaction status flips to <code>rejected</code>. The player's balance / wager / KYC record is unchanged. No PSP is involved.</li>
        </ol>
        <p><strong>Counts toward.</strong> The denominator of Approval rate (lowers the rate). Never counts toward Balanced.</p>
      </div>

      <div className="paybo-devcard">
        <h3>6 · Status summary — at-a-glance</h3>
        <table className="data-table" style={{marginTop:6}}>
          <thead>
            <tr>
              <th>Status</th>
              <th>Who sets it</th>
              <th>Next state</th>
              <th>Counts toward</th>
            </tr>
          </thead>
          <tbody>
            <tr><td><strong>Created</strong></td><td>Engine</td><td>→ Auto-approved · To-Confirm · Declined (limits)</td><td>—</td></tr>
            <tr><td><strong>To-Confirm</strong></td><td>Engine (when amount or risk flags)</td><td>→ Approved · Partial-approved · Rejected</td><td>Queue depth</td></tr>
            <tr><td><strong>Approved</strong></td><td>Auto-approval engine or operator</td><td>→ Sent to PSP → Balanced · Failed</td><td>Approval rate (numerator)</td></tr>
            <tr><td><strong>Partial-approved</strong></td><td>Operator</td><td>Same as Approved for the approved slice; remainder logged as Partial reject</td><td>Approval rate (numerator, partial)</td></tr>
            <tr><td><strong>Rejected</strong></td><td>Operator</td><td>Terminal</td><td>Approval rate (denominator only)</td></tr>
            <tr><td><strong>Pending</strong></td><td>Engine (after Sent to PSP)</td><td>→ Balanced · Failed</td><td>—</td></tr>
            <tr><td><strong>Balanced</strong></td><td>PSP webhook</td><td>Terminal — success</td><td>Balanced rate</td></tr>
            <tr><td><strong>Failed</strong></td><td>PSP webhook (or cascade timeout)</td><td>Terminal — failure (cascade may have retried first)</td><td>—</td></tr>
          </tbody>
        </table>
      </div>

      {/* ============================================================
          PHASE 1 · SETTINGS
          ============================================================ */}
      <DV_SECTION_HEAD
        kicker="Phase 1"
        title="Settings"
        subtitle={`The configuration foundation. ${PHASE_1_TASKS.length} tasks covering the provider registry, the routing & cascading engine, the alarms framework, and per-PSP liquidity alarms.`}/>
      {PHASE_1_TASKS.map((t, i) => <DV_TaskCard key={`p1-${i}`} idx={`1.${i+1}`} task={t}/>)}

      {/* ============================================================
          PHASE 2 · PAYMENT METHODS
          ============================================================ */}
      <DV_SECTION_HEAD
        kicker="Phase 2"
        title="Payment Methods"
        subtitle={`Per-brand × per-method configuration. ${PHASE_2_TASKS.length} tasks covering the catalog page and every sub-tab of the editor (General · Currencies · Fees · Limits · Roles · Auto-approval · PSP route).`}/>
      {PHASE_2_TASKS.map((t, i) => <DV_TaskCard key={`p2-${i}`} idx={`2.${i+1}`} task={t}/>)}

      {/* ============================================================
          PHASE 3 · PLAYERS
          ============================================================ */}
      <DV_SECTION_HEAD
        kicker="Phase 3"
        title="Players"
        subtitle={`Account management. ${PHASE_3_TASKS.length} tasks covering the searchable players list and the Player 360 sub-tabs (Overview · Transactions · Limits · KYC · Notes).`}/>
      {PHASE_3_TASKS.map((t, i) => <DV_TaskCard key={`p3-${i}`} idx={`3.${i+1}`} task={t}/>)}

      {/* ============================================================
          PHASE 4 · REPORTS & ANALYTICS
          ============================================================ */}
      <DV_SECTION_HEAD
        kicker="Phase 4"
        title="Reports & Analytics"
        subtitle={`Seven tab areas behind a single unified filter row. ${PHASE_4_TASKS.length} tasks covering the shell, the Financial Report (deposits / withdrawals / profit / fees), the Players ranked lists, the Geo country breakdowns, the Conversion funnel, the Fees breakdown (operator fee revenue, PSP cost, net margin, effective %), the Liquidity treasury view, and the Ops & Queue snapshot.`}/>
      {PHASE_4_TASKS.map((t, i) => <DV_TaskCard key={`p4-${i}`} idx={`4.${i+1}`} task={t}/>)}

      {/* ============================================================
          PHASE 5 · FULL PROJECT COMPLETE
          ============================================================ */}
      <DV_SECTION_HEAD
        kicker="Phase 5"
        title="Full project complete"
        subtitle={`Everything else the platform needs to ship as one cohesive product. ${PHASE_5_TASKS.length} tasks covering the Dashboard, Transactions + Withdrawals queue, Activity log, the brand switcher + Frontend preview, and the platform-wide help system.`}/>
      {PHASE_5_TASKS.map((t, i) => <DV_TaskCard key={`p5-${i}`} idx={`5.${i+1}`} task={t}/>)}

      {/* ============================================================
          Closing note
          ============================================================ */}
      <div className="paybo-devcard" style={{marginTop:24}}>
        <h3>Open decisions still on the table</h3>
        <ul style={{lineHeight:1.7}}>
          <li><strong>Stack</strong> — runtime, DB, queue, real-time channel. Intentionally not prescribed here.</li>
          <li><strong>KYC vendor</strong> — Player 360 → KYC ships as placeholder; pick a vendor (Sumsub / Onfido / Veriff) for v2.</li>
          <li><strong>Anomaly engine</strong> — Overview tab consumes a mock ANOMALIES stream; v1 needs the real detector.</li>
          <li><strong>Restore-to-version on routes</strong> — Route history is captured today; one-click restore is scoped for v2.</li>
          <li><strong>Bonus / FX cost in Profit</strong> — Profit excludes them today; CFO call on whether to add them in v2.</li>
        </ul>
      </div>
    </div>
  );
};

window.DevPhase = DevPhase;
