<?php
/**
 * Slice 8e-Receipts · Link customer receipts back to IPCs · end-to-end.
 *
 * 1) Approve an IPC → sale draft created (invoice_workflow_status='DRAFT')
 * 2) Force sale to ISSUED (so Receipts::record accepts it)
 * 3) Record a partial receipt → arSummaryFor shows received > 0, outstanding correct
 * 4) Record final receipt → outstanding = 0
 * 5) Over-receive rejected by Receipts::record
 * 6) totalReceivedFor rolls up correctly at project level
 * 7) revenueSummary now exposes customer_received + customer_ar_gap
 * 8) Perms + view surface (Receipts card visible in IPC view)
 */
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 ([
    'WorksIpcs.php','WorksBoq.php','WorksRetention.php','WorksProjectReport.php',
    'WorksCustomerRetention.php','Sales.php','Receipts.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): void { echo "\nFATAL: $why\n"; exit(1); }

$db  = new DB();
$wi  = new WorksIpcs();
$wpr = new WorksProjectReport();
$wr  = new WorksRetention();
$rcp = new Receipts();

$pcId=100; $pcNm='Verify PC'; $pcG='Projects Coordinator';
$fmId=3;   $fmNm='Verify FM';

/* ── 0. Anchor + prime + purge ─────────────────────────────────────── */
h('0. Anchor + prime + purge');
$anchor = $db->mySelect(
    "SELECT p.c0d, p.project_customer_id, p.crm_status
       FROM c_projects p JOIN works_boqs b ON b.project_id = p.c0d AND b.deleted = 0
      WHERE p.project_type='works' AND p.deleted=0 ORDER BY p.c0d ASC LIMIT 1",
    []
);
if (empty($anchor)) boom('No works project with BOQ. Run slice8cSpine first.');
$prjId       = (int)$anchor[0]['c0d'];
$prevCustId  = (int)$anchor[0]['project_customer_id'];
$prevCrm     = (string)$anchor[0]['crm_status'];
$prevTerms   = $wr->termsFor($prjId) ?: ['pct' => 0.0, 'dlp_days' => 365];
$db->insertUpdate("UPDATE c_projects SET project_customer_id = 1, crm_status = 'APPROVED' WHERE c0d = :p", [':p' => $prjId]);
$wr->setTerms($prjId, 5.00, 90);

// Purge prior 8e artefacts (IPCs, sales, receipts)
$oldIpcs = $db->mySelect("SELECT c0d, sale_id FROM works_ipcs WHERE project_id = :p AND notes LIKE 'ReceiptsVerify %'", [':p' => $prjId]) ?: [];
foreach ($oldIpcs as $oi) {
    $id  = (int)$oi['c0d']; $sid = (int)$oi['sale_id'];
    if ($sid > 0) {
        $db->insertUpdate("DELETE FROM receipts    WHERE sale_id = :s", [':s' => $sid]);
        $db->insertUpdate("DELETE FROM sales_items WHERE sale_id = :s", [':s' => $sid]);
        $db->insertUpdate("DELETE FROM sales      WHERE c0d     = :s", [':s' => $sid]);
    }
    $db->insertUpdate("DELETE FROM works_ipc_lines WHERE ipc_id = :i", [':i' => $id]);
    $db->insertUpdate("DELETE FROM works_ipcs      WHERE c0d    = :i", [':i' => $id]);
}
$stray = $db->mySelect("SELECT c0d FROM works_ipcs WHERE project_id = :p AND status IN ('DRAFT','SUBMITTED')", [':p' => $prjId]) ?: [];
foreach ($stray as $s) {
    $db->insertUpdate("DELETE FROM works_ipc_lines WHERE ipc_id = :i", [':i' => (int)$s['c0d']]);
    $db->insertUpdate("DELETE FROM works_ipcs      WHERE c0d    = :i", [':i' => (int)$s['c0d']]);
}
p("Purged " . count($oldIpcs) . " prior IPCs + " . count($stray) . " stray drafts");

/* ── 1. Draft + approve an IPC → sale created ──────────────────────── */
h('1. Draft + certify + approve IPC → sale generated');
$dr = $wi->draft($prjId, ['notes' => 'ReceiptsVerify ' . date('YmdHis')], $pcId, $pcNm);
if (!$dr['ok']) boom('IPC draft failed: ' . ($dr['msg'] ?? '?'));
$ipcId = (int)$dr['id'];

// Pick a BOQ line and certify enough to produce ~10M gross
$lines = $wi->linesFor($ipcId);
$targetLine = null; $targetQty = 0;
foreach ($lines as $l) {
    $r = (float)$l['contract_rate'];
    if ($r > 0 && (10000000 / $r) <= (float)$l['contract_qty']) {
        $targetLine = $l; $targetQty = round(10000000 / $r, 4); break;
    }
}
if (!$targetLine) boom('no BOQ line has capacity for 10M IPC');
$wi->setLineQty($ipcId, (int)$targetLine['c0d'], $targetQty);
$appRes = $wi->approve($ipcId, $pcId, $pcNm, $pcG);
if (!$appRes['ok']) boom('approve failed: ' . ($appRes['msg'] ?? '?'));
$saleId = (int)$appRes['sale_id'];
$ipcRow = $wi->findById($ipcId);
$invoiceTot = (float)$ipcRow['invoice_total'];
p("IPC {$dr['ipc_no']} approved · sale=$saleId · invoice_total=" . number_format($invoiceTot));

// Force sale to ISSUED so Receipts::record accepts it
$db->insertUpdate("UPDATE sales SET invoice_workflow_status = 'ISSUED' WHERE c0d = :s", [':s' => $saleId]);

/* ── 2. arSummaryFor BEFORE any receipt ────────────────────────────── */
h('2. arSummaryFor before any receipt');
$ar0 = $wi->arSummaryFor($ipcId);
p(sprintf("invoiced=%s received=%s outstanding=%s retained=%s",
    number_format($ar0['invoiced']), number_format($ar0['received']),
    number_format($ar0['outstanding']), number_format($ar0['retained'])));
ok(abs($ar0['invoiced']    - $invoiceTot) < 0.01,      'invoiced = IPC invoice_total');
ok((float)$ar0['received'] === 0.0 || (float)$ar0['received'] === 0, 'received = 0');
ok(abs($ar0['outstanding'] - $invoiceTot) < 0.01,      'outstanding = invoiced');
ok((float)$ar0['retained']  > 0,                        'retained > 0 (5% of gross)');
ok((int)$ar0['sale_id']    === $saleId,                 'sale_id surfaced');

/* ── 3. Partial receipt (60%) ──────────────────────────────────────── */
h('3. Record partial receipt (60% of invoice_total)');
$part = round($invoiceTot * 0.6, 2);
$r1 = $rcp->record([
    'sale_id' => $saleId, 'amount' => $part, 'currency' => 'TZS',
    'bank_account_id' => 1, 'payment_method' => 'BANK_TRANSFER',
    'bank_reference' => 'RCP-VERIFY-1', 'receipt_date' => date('Y-m-d'),
], $fmId, $fmNm);
if (!$r1['ok']) boom('record 1 failed: ' . ($r1['msg'] ?? '?'));
$rcpId1 = (int)$r1['id'];
p("receipt 1: " . number_format($part));
$ar1 = $wi->arSummaryFor($ipcId);
ok(abs($ar1['received']    - $part)               < 0.01, 'received = partial');
ok(abs($ar1['outstanding'] - ($invoiceTot-$part)) < 0.01, 'outstanding = invoiced − partial');
ok(count($wi->receiptsFor($ipcId)) === 1,                  'receiptsFor returns 1 row');

/* ── 4. Final receipt (remainder) ──────────────────────────────────── */
h('4. Record final receipt');
$rest = round($invoiceTot - $part, 2);
$r2 = $rcp->record([
    'sale_id' => $saleId, 'amount' => $rest, 'currency' => 'TZS',
    'bank_account_id' => 1, 'payment_method' => 'CASH',
    'bank_reference' => 'RCP-VERIFY-2', 'receipt_date' => date('Y-m-d'),
], $fmId, $fmNm);
if (!$r2['ok']) boom('record 2 failed: ' . ($r2['msg'] ?? '?'));
$ar2 = $wi->arSummaryFor($ipcId);
ok(abs($ar2['received']    - $invoiceTot) < 0.01,  'received = invoiced');
ok((float)$ar2['outstanding'] <= 0.01,              'outstanding ≈ 0');
ok(count($wi->receiptsFor($ipcId)) === 2,           'receiptsFor returns 2 rows');

/* ── 5. Over-receive guard ─────────────────────────────────────────── */
h('5. Over-receive rejected');
$over = $rcp->record([
    'sale_id' => $saleId, 'amount' => 1000, 'currency' => 'TZS',
    'bank_account_id' => 1, 'payment_method' => 'CASH',
    'bank_reference' => 'over', 'receipt_date' => date('Y-m-d'),
], $fmId, $fmNm);
ok(!$over['ok'] && stripos($over['msg'], 'exceeds outstanding') !== false, 'exceeds outstanding refused');

/* ── 6. totalReceivedFor rollup ────────────────────────────────────── */
h('6. totalReceivedFor rollup');
$tot = $wi->totalReceivedFor($prjId);
p("project-wide received=" . number_format($tot['received']) . " · count=" . $tot['count']);
ok((float)$tot['received'] >= $invoiceTot, 'rollup received >= this IPC invoice_total');
ok((int)$tot['count']      >= 2,           'rollup count >= 2');

/* ── 7. revenueSummary picks up the new measures ───────────────────── */
h('7. revenueSummary exposes customer_received + customer_ar_gap');
$rev = $wpr->revenueSummary($prjId);
ok(isset($rev['customer_received']),   'customer_received present');
ok(isset($rev['customer_ar_gap']),     'customer_ar_gap present');
ok(isset($rev['receipts_count']),      'receipts_count present');
ok((float)$rev['customer_received'] >= $invoiceTot, 'customer_received includes our receipts');
// After full receive, AR gap should be 0 (or very close)
ok((float)$rev['customer_ar_gap']  <= 0.01,          'customer_ar_gap ≈ 0 after full receipt');

/* ── 8. Perms + view surface ───────────────────────────────────────── */
h('8. Perms + view surface');
$viewBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'WorksIpcs' . DS . 'view.php');
ok(strpos($viewBody, "\$this->data['receipts']") !== false, 'IPC view reads receipts');
ok(strpos($viewBody, "\$this->data['ar']")       !== false, 'IPC view reads ar summary');
ok(strpos($viewBody, 'AR position · sale #')     !== false, 'IPC view has AR card');
$listBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'WorksIpcs' . DS . 'list.php');
ok(strpos($listBody, "\$this->data['ar_by_ipc']") !== false, 'IPCs list reads ar_by_ipc');
ok(strpos($listBody, 'Outstanding')               !== false, 'IPCs list has Outstanding column');
$rptBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'views' . DS . 'WorksProjectReport' . DS . 'view.php');
ok(strpos($rptBody, 'customer_received') !== false, 'Project report view exposes customer_received');
ok(strpos($rptBody, 'customer_ar_gap')   !== false, 'Project report view exposes customer_ar_gap');
$ctrlBody = (string)file_get_contents(ROOT . DS . 'app' . DS . 'controllers' . DS . 'WorksIpcsController.php');
ok(strpos($ctrlBody, 'arSummaryFor')     !== false, 'controller calls arSummaryFor');
ok(strpos($ctrlBody, 'receiptsFor')      !== false, 'controller calls receiptsFor');
ok(strpos($ctrlBody, 'totalReceivedFor') !== false, 'controller calls totalReceivedFor');

/* ── Z. Restore state ─────────────────────────────────────────────── */
h('Z. Restore anchor state');
$db->insertUpdate("UPDATE c_projects SET project_customer_id = :cid, crm_status = :cs WHERE c0d = :p",
    [':cid' => $prevCustId, ':cs' => $prevCrm, ':p' => $prjId]);
$wr->setTerms($prjId, (float)$prevTerms['pct'], (int)$prevTerms['dlp_days']);
// Clean up this run's receipts + sale + IPC so downstream verifies see baseline
$db->insertUpdate("DELETE FROM receipts    WHERE sale_id = :s", [':s' => $saleId]);
$db->insertUpdate("DELETE FROM sales_items WHERE sale_id = :s", [':s' => $saleId]);
$db->insertUpdate("DELETE FROM sales      WHERE c0d     = :s", [':s' => $saleId]);
$db->insertUpdate("DELETE FROM works_ipc_lines WHERE ipc_id = :i", [':i' => $ipcId]);
$db->insertUpdate("DELETE FROM works_ipcs      WHERE c0d    = :i", [':i' => $ipcId]);
p("Restored: customer_id=$prevCustId · crm=$prevCrm · retention_pct={$prevTerms['pct']} · deleted IPC + sale + receipts");

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