<?php
/**
 * Slice 8b-4b · Requisition Issue e2e (full DRAFT → ISSUED ladder).
 *
 *   1) Provision services project.
 *   2) PC drafts requisition, adds 3 lines (FUEL, MEALS, TRANSPORT), submits.
 *   3) Walk 4-step chain PC → PM → FM → Exec → APPROVED (via postDecisionHook).
 *   4) Notification: Accountant group received a REQUISITION_APPROVED ping pointing at
 *      /ServiceRequisitions/view/{id}.
 *   5) openForAccountant() returns the requisition.
 *   6) Guard: non-Accountant role tries to issue → BLOCKED.
 *   7) Guard: Accountant issues without reference → BLOCKED.
 *   8) Guard: Accountant issues > total → BLOCKED.
 *   9) Accountant issues at full amount with ref → ISSUED · fields recorded correctly.
 *  10) Second issue attempt (already ISSUED) → BLOCKED.
 *  11) Notification: PC (originator) received a REQUISITION_ISSUED ping.
 *  12) openForAccountant() no longer returns this requisition (out of queue).
 *  13) Partial issue path: separate requisition · Accountant issues 60% · ISSUED with correct amount.
 */
define('DS', DIRECTORY_SEPARATOR);
define('ROOT', dirname(__DIR__));
require_once ROOT . DS . 'config' . DS . 'config.php';
require_once ROOT . DS . 'core' . DS . 'Application.php';
require_once ROOT . DS . 'core' . DS . 'Model.php';
require_once ROOT . DS . 'core' . DS . 'DB.php';
$_SESSION = $_SESSION ?? [];
foreach ([
    'ApprovalChain.php','Tenders.php','TenderPricing.php','ProjectStarter.php','Notifications.php',
    'ServiceRequisitions.php',
] as $m) require_once ROOT . DS . 'app' . DS . 'models' . DS . $m;

function h(string $t): void { echo "\n==== $t ====\n"; }
function p(string $t): void { echo "  $t\n"; }
function ok(bool $c, string $m): void { echo ($c ? "  ✓ " : "  ✗ ") . $m . "\n"; }
function boom(string $why) { echo "\nFATAL: $why\n"; exit(1); }

$db = new DB();
$ac = new ApprovalChain();
$tm = new Tenders();
$sr = new ServiceRequisitions();
$nm = new Notifications();

$uidCrm = 100; $unmCrm = 'CRM Test User';       $ugCrm  = 'CRM';
$uidFm  = 3;   $unmFm  = 'Finance Manager Test'; $ugFm  = 'Finance Manager';
$uidExec= 2;   $unmExec= 'Executive Test';      $ugExec = 'Executive';
$accId  = 4;   $accNm  = 'Accountant Test';    $ugAcc = 'Accountant';
$procId = 8;   $procNm = 'Procurement Test';
$pmId   = 5;   $pmNm   = 'Project Manager Test';
$pcRow  = $db->mySelect("SELECT c0d, full_name FROM users WHERE user_group_name='Projects Coordinator' AND status='ACTIVE' AND deleted=0 ORDER BY c0d LIMIT 1", []);
$pcId   = (int)($pcRow[0]['c0d'] ?? 0);
$pcNm   = (string)($pcRow[0]['full_name'] ?? '');

$stamp = date('YmdHis');

/* ── 1 ── Provision services project ──────────────────────────────── */
h('1. Provision services project');
$fb = $db->insertUpdate(
    "INSERT INTO tender_tracker
        (tender_no, tender_title, source_type, buyer_name, project_type,
         date_received, estimated_value, currency, tender_stage, tender_status,
         created_by, created_by_name, created_on)
     VALUES (:tn, :tt, 'PRIVATE_CLIENT', 'Vodacom TZ', 'services',
         CURDATE(), 25000000, 'TZS', 'PREP', 'OPEN', :u, :un, NOW())",
    [':tn' => 'T-8B4B-' . $stamp, ':tt' => 'SLICE 8b-4b issue verify', ':u' => $uidCrm, ':un' => $unmCrm]
);
$tid = (int)($fb['last_id'] ?? 0);
foreach (['SUBMISSION','FOLLOW_UP','OPENING'] as $s) $tm->advanceStage($tid, $s, 'x', $uidCrm, $unmCrm);
$tm->recordResult($tid, 'WON', date('Y-m-d'), '', 25000000, '', '', $uidCrm, $unmCrm);
$tm->advanceStage($tid, 'AWARDED', 'a', $uidCrm, $unmCrm);
$tm->saveContractTerms($tid, ['contract_value'=>25000000], $uidFm, $ugFm, $unmFm);
$rC = $ac->submit(['chain_key'=>'TENDER_CONTRACT_APPROVAL','entity_type'=>'TENDER_CONTRACT','entity_id'=>$tid,'ref_label'=>'x','return_url'=>PROOT.'Tenders/view/'.$tid,'user_id'=>$uidCrm,'user_name'=>$unmCrm,'user_group'=>$ugCrm]);
$aidC = (int)$rC['approval_id'];
$ac->approve(['approval_id'=>$aidC,'user_id'=>$uidFm,'user_name'=>$unmFm,'user_group'=>$ugFm,'comment'=>'','assignments'=>['ACCOUNTANT'=>['user_id'=>$accId,'user_name'=>$accNm],'PROCUREMENT'=>['user_id'=>$procId,'user_name'=>$procNm]]]);
$ac->approve(['approval_id'=>$aidC,'user_id'=>$uidExec,'user_name'=>$unmExec,'user_group'=>$ugExec,'comment'=>'','assignments'=>['PROJECT_MANAGER'=>['user_id'=>$pmId,'user_name'=>$pmNm]]]);
$aRow = $ac->approvalById($aidC);
$stg  = $ac->decodePayload((string)($aRow['step_payload_json'] ?? ''));
$tm->onContractApproved($tid, $uidExec, $unmExec, $stg['assignments'] ?? []);
$t = $tm->find($tid); $pid = (int)$t['linked_project_id'];
ok($pid > 0, "Project provisioned (id=$pid)");

/* Track Accountant unread notif count baseline */
$accUnreadBefore = $nm->unreadCountForUser($accId);
$pcUnreadBefore  = $nm->unreadCountForUser($pcId);

/* ── 2 ── PC drafts + submits ─────────────────────────────────────── */
h('2. PC drafts + adds 3 lines + submits');
$reqId = $sr->draft($pid, ['title' => 'Site 03 · biweekly float', 'purpose' => 'Fuel + meals for tech visits'], $pcId, $pcNm);
$sr->addLine($reqId, 'FUEL',      ['description'=>'Diesel','qty'=>100,'unit'=>'L','unit_cost'=>3200]); // 320,000
$sr->addLine($reqId, 'MEALS',     ['description'=>'2 techs × 5 days','qty'=>10,'unit'=>'day','unit_cost'=>15000]); // 150,000
$sr->addLine($reqId, 'TRANSPORT', ['description'=>'Boda hire','qty'=>3,'unit'=>'day','unit_cost'=>25000]); // 75,000
$reqBefore = $sr->findById($reqId);
$expected  = 320000 + 150000 + 75000; // 545,000
p("Requisition {$reqBefore['req_no']} · total = " . number_format((float)$reqBefore['total_amount']));
ok(abs((float)$reqBefore['total_amount'] - $expected) < 0.01, 'Total 545,000');
$res = $sr->submitForApproval($reqId, $pcId, $pcNm, 'Projects Coordinator');
if (!$res['ok']) boom('Submit failed: ' . $res['msg']);
$aid = (int)$res['approval_id'];

/* ── 3 ── Walk chain → APPROVED ───────────────────────────────────── */
h('3. Walk 4-step chain PC → PM → FM → Exec');
$ac->approve(['approval_id'=>$aid,'user_id'=>$pcId, 'user_name'=>$pcNm, 'user_group'=>'Projects Coordinator','comment'=>'OK']);
$ac->approve(['approval_id'=>$aid,'user_id'=>$pmId, 'user_name'=>$pmNm, 'user_group'=>'Project Manager',     'comment'=>'OK']);
$ac->approve(['approval_id'=>$aid,'user_id'=>$uidFm,'user_name'=>$unmFm,'user_group'=>$ugFm,               'comment'=>'OK']);
$ac->approve(['approval_id'=>$aid,'user_id'=>$uidExec,'user_name'=>$unmExec,'user_group'=>$ugExec,         'comment'=>'OK']);
$aRow = $ac->approvalById($aid);
$sr->onChainDecision($reqId, (string)$aRow['status']);
$req = $sr->findById($reqId);
ok($req['status'] === 'APPROVED' && !empty($req['approved_on']), 'Status → APPROVED · approved_on stamped');

/* ── 4 ── Accountant received the ready-to-issue notification ───── */
h('4. Accountant got REQUISITION_APPROVED notification');
$accUnreadAfter = $nm->unreadCountForUser($accId);
$accRow = $db->mySelect("SELECT * FROM notifications WHERE recipient_user_id = :u AND kind = 'REQUISITION_APPROVED' AND entity_id = :e ORDER BY c0d DESC LIMIT 1", [':u' => $accId, ':e' => $reqId]);
p("Accountant unread: $accUnreadBefore → $accUnreadAfter");
ok(!empty($accRow), 'Accountant has REQUISITION_APPROVED row');
ok(strpos((string)$accRow[0]['target_url'], 'ServiceRequisitions/view/' . $reqId) !== false, 'Notification target URL is correct');

/* ── 5 ── openForAccountant() ─────────────────────────────────────── */
h('5. openForAccountant() shows this requisition');
$queue = $sr->openForAccountant();
$mine = array_filter($queue, fn($r) => (int)$r['c0d'] === $reqId);
ok(!empty($mine), 'Requisition is in the Accountant issue queue');

/* ── 6 ── Non-Accountant issue attempt → BLOCKED ─────────────────── */
h('6. PC tries to issue → BLOCKED (role guard)');
try {
    $sr->issue($reqId, ['issue_reference' => 'PC-TRY', 'issue_amount' => (float)$req['total_amount']], $pcId, $pcNm, 'Projects Coordinator');
    ok(false, 'PC was allowed to issue');
} catch (\Throwable $e) {
    ok(strpos($e->getMessage(), 'Only Accountant/FM/Admin') !== false, 'Blocked: ' . $e->getMessage());
}

/* ── 7 ── No reference → BLOCKED ─────────────────────────────────── */
h('7. Accountant issues without reference → BLOCKED');
try {
    $sr->issue($reqId, ['issue_reference' => '', 'issue_amount' => (float)$req['total_amount']], $accId, $accNm, $ugAcc);
    ok(false, 'Empty reference allowed');
} catch (\Throwable $e) {
    ok(strpos($e->getMessage(), 'Issue reference required') !== false, 'Blocked: ' . $e->getMessage());
}

/* ── 8 ── Over-issue → BLOCKED ────────────────────────────────────── */
h('8. Accountant issues > total → BLOCKED');
try {
    $sr->issue($reqId, ['issue_reference' => 'PV-2026-0001', 'issue_amount' => (float)$req['total_amount'] + 1], $accId, $accNm, $ugAcc);
    ok(false, 'Over-issue allowed');
} catch (\Throwable $e) {
    ok(strpos($e->getMessage(), 'cannot exceed') !== false, 'Blocked: ' . $e->getMessage());
}

/* ── 9 ── Successful issue ────────────────────────────────────────── */
h('9. Accountant issues full amount with reference');
$sr->issue($reqId, ['issue_reference' => 'PV-2026-0001', 'issue_amount' => (float)$req['total_amount'], 'issue_notes' => 'Cash collected in person'], $accId, $accNm, $ugAcc);
$reqAfter = $sr->findById($reqId);
p("Post-issue: status={$reqAfter['status']} · amount=" . number_format((float)$reqAfter['issue_amount']) . " · ref={$reqAfter['issue_reference']} · issued_on={$reqAfter['issued_on']} · by={$reqAfter['issued_by_name']}");
ok($reqAfter['status'] === 'ISSUED', 'Status → ISSUED');
ok((int)$reqAfter['issued_by'] === $accId && $reqAfter['issued_by_name'] === $accNm, 'issued_by/name recorded');
ok(abs((float)$reqAfter['issue_amount'] - (float)$req['total_amount']) < 0.01, 'issue_amount matches full total');
ok($reqAfter['issue_reference'] === 'PV-2026-0001', 'issue_reference stored');
ok(!empty($reqAfter['issued_on']), 'issued_on stamped');

/* ── 10 ── Second issue attempt → BLOCKED ─────────────────────────── */
h('10. Second issue on ISSUED requisition → BLOCKED');
try {
    $sr->issue($reqId, ['issue_reference' => 'PV-2026-0002', 'issue_amount' => 100], $accId, $accNm, $ugAcc);
    ok(false, 'Second issue allowed');
} catch (\Throwable $e) {
    ok(strpos($e->getMessage(), 'ISSUED') !== false || strpos($e->getMessage(), 'APPROVED') !== false, 'Blocked: ' . $e->getMessage());
}

/* ── 11 ── PC (originator) got REQUISITION_ISSUED ping ───────────── */
h('11. PC (originator) got REQUISITION_ISSUED notification');
$pcUnreadAfter = $nm->unreadCountForUser($pcId);
$pcRow = $db->mySelect("SELECT * FROM notifications WHERE recipient_user_id = :u AND kind = 'REQUISITION_ISSUED' AND entity_id = :e ORDER BY c0d DESC LIMIT 1", [':u' => $pcId, ':e' => $reqId]);
p("PC unread: $pcUnreadBefore → $pcUnreadAfter");
ok(!empty($pcRow), 'PC has REQUISITION_ISSUED row');
ok(strpos((string)$pcRow[0]['subject'], 'PV-2026-0001') === false /* subject has req_no, ref is in body */
   || strpos((string)$pcRow[0]['body'], 'PV-2026-0001') !== false, 'Notification carries the reference');

/* ── 12 ── openForAccountant() no longer includes this row ────── */
h('12. Queue no longer includes this requisition');
$queue2 = $sr->openForAccountant();
$stillHere = array_filter($queue2, fn($r) => (int)$r['c0d'] === $reqId);
ok(empty($stillHere), 'Requisition dropped out of Accountant queue');

/* ── 13 ── Partial issue path ─────────────────────────────────────── */
h('13. Partial issue: 60% of total');
$req2Id = $sr->draft($pid, ['title' => 'Site 04 float'], $pcId, $pcNm);
$sr->addLine($req2Id, 'FUEL', ['description'=>'Diesel','qty'=>200,'unit'=>'L','unit_cost'=>3200]); // 640,000
$res2 = $sr->submitForApproval($req2Id, $pcId, $pcNm, 'Projects Coordinator');
$aid2 = (int)$res2['approval_id'];
$ac->approve(['approval_id'=>$aid2,'user_id'=>$pcId, 'user_name'=>$pcNm, 'user_group'=>'Projects Coordinator','comment'=>'OK']);
$ac->approve(['approval_id'=>$aid2,'user_id'=>$pmId, 'user_name'=>$pmNm, 'user_group'=>'Project Manager',     'comment'=>'OK']);
$ac->approve(['approval_id'=>$aid2,'user_id'=>$uidFm,'user_name'=>$unmFm,'user_group'=>$ugFm,               'comment'=>'OK']);
$ac->approve(['approval_id'=>$aid2,'user_id'=>$uidExec,'user_name'=>$unmExec,'user_group'=>$ugExec,         'comment'=>'OK']);
$aRow2 = $ac->approvalById($aid2);
$sr->onChainDecision($req2Id, (string)$aRow2['status']);
// Partial issue: 60% of 640,000 = 384,000
$partial = 384000.0;
$sr->issue($req2Id, ['issue_reference' => 'PV-2026-0002', 'issue_amount' => $partial, 'issue_notes' => '60% partial · balance held pending confirmation'], $accId, $accNm, $ugAcc);
$req2After = $sr->findById($req2Id);
p("Partial: total=" . number_format((float)$req2After['total_amount']) . " · issued=" . number_format((float)$req2After['issue_amount']));
ok($req2After['status'] === 'ISSUED', 'Partial issue → ISSUED');
ok(abs((float)$req2After['issue_amount'] - $partial) < 0.01, 'issue_amount = 384,000');
ok(abs((float)$req2After['total_amount'] - 640000) < 0.01, 'total_amount unchanged (640,000)');

echo "\n=== END ===\n";
