<?php
declare(strict_types=1);

// B-only, nonproduction fixture. No execution without the exact approval and SHA.
// This script never invokes payment, refund, checkout, queue, printer or a gateway.
if (($argv[1] ?? '') !== '--execute-b'
    || getenv('CDX_CASHIER_B_APPROVAL') !== 'APPROVED_ONE_OWNED_ORDER_20261008'
    || !hash_equals((string) getenv('CDX_CASHIER_B_SHA'), (string) hash_file('sha256', __FILE__))) {
    echo json_encode(['status' => 'DENIED', 'business_http' => 0, 'business_dml' => 0]), PHP_EOL;
    exit(78);
}

const ROOT = '/private/tmp/zhct-http-ai-macos.x2CCZX';
const SOURCE = '/Users/wiscourper/Documents/workspace/local/dm8-full-acceptance.fF5yTy/ai_api';
const LOCAL_ENV = '/Users/wiscourper/Documents/workspace/zhct/zhctproject/ai_api/.env.local';
const ACTOR = 'CDXPARMOB838A9419';
const AI_ID = 44676;
const STORE_USER_ID = 97274;
const STORE_STAFF_ID = 107491;
const HISTORIC_ORDER_ID = 68323;
const PORT = 18101;
const PID = 13127;
const MAX_RESPONSE = 1048576;

function fail(string $message): void { throw new RuntimeException($message); }
function need(bool $condition, string $message): void { if (!$condition) fail($message); }
function one(PDO $db, string $sql, array $args = []): array {
    $statement = $db->prepare($sql);
    $statement->execute($args);
    return array_change_key_case((array) $statement->fetch(PDO::FETCH_ASSOC), CASE_LOWER);
}
function countRows(PDO $db, string $sql, array $args = []): int {
    $statement = $db->prepare($sql);
    $statement->execute($args);
    return (int) $statement->fetchColumn();
}
function moneyCents($value): int {
    $text = (string) $value;
    need((bool) preg_match('/^(0|[1-9][0-9]*)(?:\.([0-9]{1,2}))?$/D', $text, $parts),
        'unexpected money representation');
    return (int) $parts[1] * 100 + (int) str_pad($parts[2] ?? '', 2, '0');
}
function snapshot(PDO $ai, PDO $store, int $allowedPendingId = 0): array {
    $user = one($ai, 'SELECT USER_ID,STORE_ID,IS_DELETE,BALANCE,GIFT_BALANCE,'
        . 'SUBSIDY_BALANCE FROM yoshop_user WHERE USER_ID=? AND NICK_NAME=?', [AI_ID, ACTOR]);
    $staff = one($store, 'SELECT ID,AI_USER_ID,IS_SHOW FROM ydy_staff WHERE ID=? AND NAME=?',
        [STORE_STAFF_ID, ACTOR]);
    $storeUser = one($store, 'SELECT ID,AI_USER_ID,STAFF_UUID,IS_DELETE FROM ydy_user'
        . ' WHERE ID=? AND ACCOUNT=?', [STORE_USER_ID, ACTOR]);
    $historic = one($store, 'SELECT ID,USER_ID,SOURCE,PAY_STATUS,ORDER_STATUS,'
        . 'PAY_PRICE,ACCOUNT_SETTLEMENT_ID FROM ydy_meal_order WHERE ID=? AND REMARK=?',
        [HISTORIC_ORDER_ID, ACTOR]);
    need((int) ($user['user_id'] ?? 0) === AI_ID && (int) ($user['store_id'] ?? 0) === 10001
        && (int) ($user['is_delete'] ?? 1) === 0, 'AI synthetic actor mismatch');
    need((int) ($staff['id'] ?? 0) === STORE_STAFF_ID
        && (int) ($staff['ai_user_id'] ?? 0) === AI_ID
        && (int) ($staff['is_show'] ?? 0) === 1, 'Store staff mismatch');
    need((int) ($storeUser['id'] ?? 0) === STORE_USER_ID
        && (int) ($storeUser['ai_user_id'] ?? 0) === AI_ID
        && (int) ($storeUser['is_delete'] ?? 1) === 0, 'Store user mismatch');
    need((int) ($historic['id'] ?? 0) === HISTORIC_ORDER_ID
        && (int) ($historic['user_id'] ?? 0) === STORE_USER_ID
        && (int) ($historic['source'] ?? 0) === 4
        && (int) ($historic['pay_status'] ?? 0) === 20, 'Historic audit order mismatch');
    need(countRows($store, 'SELECT COUNT(*) FROM ydy_meal_order WHERE USER_ID=?'
        . ' AND PAY_STATUS=10 AND ORDER_STATUS=10 AND IS_DELETE=0 AND ID<>?',
        [STORE_USER_ID, $allowedPendingId]) === 0,
        'Actor already has a pending order');
    $accountCounts = [];
    foreach (['yoshop_account_settlement', 'yoshop_user_balance_log',
        'yoshop_user_subsidy_log'] as $table) {
        $accountCounts[$table] = countRows($ai,
            'SELECT COUNT(*) FROM ' . $table . ' WHERE USER_ID=?', [AI_ID]);
    }
    return [
        'actor_sha' => hash('sha256', json_encode([$user, $staff, $storeUser, $historic])),
        'account_counts' => $accountCounts,
        'cash_cents' => moneyCents($user['balance']),
        'gift_cents' => moneyCents($user['gift_balance']),
        'subsidy_cents' => moneyCents($user['subsidy_balance']),
    ];
}
function httpGet(string $url, ?string $token, string $method = 'GET'): array {
    need(in_array($method, ['GET', 'POST'], true), 'unsupported test HTTP method');
    $headers = "platform: H5\r\n";
    if ($token !== null) $headers .= 'Access-Token: ' . $token . "\r\n";
    $options = ['method' => $method, 'header' => $headers,
        'ignore_errors' => true, 'timeout' => 15, 'follow_location' => 0];
    if ($method === 'POST') {
        $options['header'] .= "Content-Type: application/json\r\n";
        $options['content'] = '{}';
    }
    $context = stream_context_create(['http' => $options]);
    $handle = @fopen($url, 'rb', false, $context);
    need(is_resource($handle), 'HTTP open failed');
    try { $raw = stream_get_contents($handle, MAX_RESPONSE + 1); }
    finally { fclose($handle); }
    need(is_string($raw) && strlen($raw) <= MAX_RESPONSE, 'HTTP response over limit');
    $statusLine = $http_response_header[0] ?? '';
    $status = preg_match('/^HTTP\/\S+\s+(\d{3})/', $statusLine, $match)
        ? (int) $match[1] : 0;
    $body = json_decode($raw, true);
    need(is_array($body), 'non-JSON HTTP response');
    return [$status, $body];
}

// Fixed source/runtime/port boundary. Do not use a browser or a non-loopback worker.
need(realpath(ROOT) === ROOT && realpath(ROOT . '/app') === SOURCE . '/app',
    'B isolated root or actual Composer source drift');
foreach ([
    'app/api/controller/Cashier.php' => 'a25c6b142e0010333d4f8aab14f2b911e11394922df063b0c9ae2482a09189e3',
    'app/api/service/Cashier.php' => '61dcb786c12eab1b1ecaec2aff0189169f8c86f4071119c7f63d3b3da04d2c6f',
    'app/api/service/cashier/MealPayment.php' => 'e18ab87f23d748df78564b0be4433c76268d69079029adb40bb9b6fcc6e47b65',
] as $relative => $sha) {
    need(hash_equals($sha, (string) hash_file('sha256', SOURCE . '/' . $relative)),
        'B approved source drift: ' . $relative);
}
$command = trim((string) shell_exec('ps -p ' . PID . ' -o command='));
need(strpos($command, '-S 127.0.0.1:' . PORT . ' -t /tmp/zhct-http-ai-macos.x2CCZX/public') !== false,
    'B worker PID/command drift');
$listening = (string) shell_exec('lsof -nP -iTCP:' . PORT . ' -sTCP:LISTEN -F pn 2>/dev/null');
need(strpos($listening, 'p' . PID . "\n") !== false
    && strpos($listening, 'n127.0.0.1:' . PORT . "\n") !== false
    && substr_count($listening, "\np") === 0, 'B socket owner or binding drift');
need(is_file(ROOT . '/config/cache.php')
    && hash_equals('98fa13484feb3db94bc4ebdd3de2f0b4ebc2c80d1cccb7a283a16f45eb9095a4',
        (string) hash_file('sha256', ROOT . '/config/cache.php')),
    'B private File cache config drift');
need(hash_equals('8c46a6d9cd277f2950818d09162f639a62f135a5eb6553e6c61ce2c19167fd02',
        (string) hash_file('sha256', ROOT . '/config/database.php'))
    && hash_equals('c1632f9105e355be219a1a0f3723954f968f312b8c3789c3aca7120a8160c27d',
        (string) hash_file('sha256', ROOT . '/env.php')),
    'B HTTP database/env code drift');

$config = parse_ini_file(LOCAL_ENV, true, INI_SCANNER_RAW);
need(is_array($config) && isset($config['DATABASE'], $config['DATABASE_ZHCT']),
    'code-local database configuration unavailable');
$processEnvironment = (string) shell_exec('ps eww -p ' . PID . ' -o command=');
$expectedHttpEnv = ['POC_DIALECT' => 'mysql'];
foreach (['DATABASE' => 'POC_AI_', 'DATABASE_ZHCT' => 'POC_'] as $section => $prefix) {
    foreach (['HOSTNAME' => 'HOST', 'HOSTPORT' => 'PORT', 'DATABASE' => 'SCHEMA',
        'USERNAME' => 'USER'] as $localKey => $httpKey) {
        $expectedHttpEnv[$prefix . $httpKey] = (string) $config[$section][$localKey];
    }
}
foreach ($expectedHttpEnv as $key => $expected) {
    need(preg_match('/(?:^|\s)' . preg_quote($key, '/') . '=([^\s]+)/',
        $processEnvironment, $match) === 1
        && hash_equals($expected, $match[1]), 'B HTTP worker database identity drift');
}
function connectLocal(array $config, string $section, string $hostSha, string $databaseSha): PDO {
    $d = $config[$section];
    need(strtolower((string) $d['TYPE']) === 'mysql' && (string) $d['HOSTPORT'] === '3306'
        && (string) $d['CHARSET'] === 'utf8'
        && hash_equals($hostSha, hash('sha256', (string) $d['HOSTNAME']))
        && hash_equals($databaseSha, hash('sha256', (string) $d['DATABASE'])),
        'code-local MySQL identity drift');
    $db = new PDO('mysql:host=' . $d['HOSTNAME'] . ';port=' . $d['HOSTPORT']
        . ';dbname=' . $d['DATABASE'] . ';charset=' . $d['CHARSET'],
        $d['USERNAME'], $d['PASSWORD'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_TIMEOUT => 5]);
    need((string) $db->query('SELECT DATABASE()')->fetchColumn() === (string) $d['DATABASE']
        && strpos((string) $db->query('SELECT VERSION()')->fetchColumn(), '5.6.') === 0,
        'connection not code-local MySQL 5.6');
    return $db;
}
$ai = connectLocal($config, 'DATABASE',
    '47e8be48b0cfd56dcdb9d9d426e6637b245f340b4efad4cd2f084ec5851ecc8e',
    'cf7756b3bb52cf72f625689134015b877fdab3ad8eb3bb2ddd962bb35f6efd66');
$store = connectLocal($config, 'DATABASE_ZHCT',
    '47e8be48b0cfd56dcdb9d9d426e6637b245f340b4efad4cd2f084ec5851ecc8e',
    'e8414690ae09be92817caeebf414e12ded85a2230818986fe9b866dbe4cfaef5');
$before = snapshot($ai, $store);

// Initialize only B's isolated cache for a <=300s synthetic token.
foreach (['DATABASE' => 'AI_', 'DATABASE_ZHCT' => ''] as $section => $prefix) {
    $d = $config[$section];
    foreach (['HOSTNAME' => 'HOST', 'HOSTPORT' => 'PORT', 'DATABASE' => 'SCHEMA',
        'USERNAME' => 'USER', 'PASSWORD' => 'PASSWORD'] as $source => $target) {
        putenv('POC_' . $prefix . $target . '=' . $d[$source]);
    }
}
putenv('POC_DIALECT=mysql');
define('TP_ENV', 'poc');
define('BUSINESS', 'aizhct');
define('ENTERPRISE_NO', '10001');
require ROOT . '/vendor/autoload.php';
(new \think\App(ROOT))->setEnvName('poc')->initialize();
need(strtolower((string) \think\facade\Config::get('cache.default')) === 'file'
    && realpath((string) \think\facade\Config::get('cache.stores.file.path'))
        === ROOT . '/runtime/cache', 'CLI and B HTTP File cache boundary differ');

$marker = 'CDXCOI' . strtoupper(bin2hex(random_bytes(5)));
$orderNo = $marker;
$token = $marker . 'T' . strtoupper(bin2hex(random_bytes(12)));
umask(0077);
$journalDir = '/Users/wiscourper/.codex/cashier-orderinfo-b-20261008';
need(!is_link($journalDir), 'journal directory symlink');
if (!is_dir($journalDir)) need(mkdir($journalDir, 0700), 'journal mkdir failed');
need(((int) fileperms($journalDir) & 0077) === 0, 'journal directory not private');
$journalPath = $journalDir . '/' . $marker . '.json';
$journal = static function (array $data, bool $initial = false) use ($journalPath): void {
    if (!$initial) need(is_file($journalPath) && !is_link($journalPath),
        'journal disappeared or became symlink');
    $h = fopen($journalPath, $initial ? 'xb' : 'ab');
    need(is_resource($h), 'journal path collision');
    chmod($journalPath, 0600);
    $record = json_encode($data, JSON_UNESCAPED_SLASHES) . "\n";
    need(fwrite($h, $record) === strlen($record), 'private journal write incomplete');
    fclose($h);
};
$orderId = 0;
$httpConclusive = false;
$tokenDeleted = false;
$journal(['phase' => 'prepared', 'marker' => $marker, 'token' => $token,
    'before' => $before, 'order_id' => 0,
    'actor_id' => AI_ID, 'store_user_id' => STORE_USER_ID,
    'source_sha' => hash_file('sha256', __FILE__)], true);
try {
    // The B worker has no /__poc_boundary route. The fixed PID/socket,
    // Composer source, local DB identities and private File cache above are
    // the actual boundary for this route; source review found no external call
    // in orderInfo() (unlike orderPay()). Do not demand C-only stub metadata.
    need(countRows($store, 'SELECT COUNT(*) FROM ydy_meal_order WHERE ORDER_NO=? OR REMARK=?',
        [$orderNo, $marker]) === 0, 'synthetic order marker collision');
    $historic = one($store, 'SELECT RESTAURANT_ID,MEAL_TIMES FROM ydy_meal_order'
        . ' WHERE ID=? AND REMARK=? AND USER_ID=?',
        [HISTORIC_ORDER_ID, ACTOR, STORE_USER_ID]);
    need((int) ($historic['restaurant_id'] ?? 0) > 0
        && (int) ($historic['meal_times'] ?? 0) > 0, 'historic synthetic meal source drift');
    $now = date('Y-m-d H:i:s');
    $insert = $store->prepare('INSERT INTO ydy_meal_order (ORDER_NO,USER_ID,RESTAURANT_ID,'
        . 'MEAL_DATE,MEAL_TIMES,BIND_TIME,SOURCE,PAY_STATUS,ORDER_STATUS,'
        . 'TOTAL_PRICE,ORI_TOTAL_PRICE,ORDER_PRICE,PAY_PRICE,MARKETING_STRATEGY_ID,'
        . 'REMARK,MSGID,IS_DELETE) VALUES (?,?,?,CURDATE(),?,?,4,10,10,'
        . "'0.80','0.80','0.80','0.80',0,?,?,0)");
    $insert->execute([$orderNo, STORE_USER_ID, (int) $historic['restaurant_id'],
        (int) $historic['meal_times'], $now, $marker, $marker]);
    $orderId = (int) $store->lastInsertId();
    need($orderId > 0, 'owned order insert lacked ID');
    $journal(['phase' => 'owned_order_inserted', 'marker' => $marker,
        'order_id' => $orderId, 'order_no' => $orderNo]);
    $rowBefore = one($store, 'SELECT * FROM ydy_meal_order WHERE ID=?', [$orderId]);
    need((int) $rowBefore['user_id'] === STORE_USER_ID
        && (int) $rowBefore['source'] === 4 && (int) $rowBefore['pay_status'] === 10
        && (int) $rowBefore['order_status'] === 10
        && (int) $rowBefore['account_settlement_id'] === 0
        && $rowBefore['order_no'] === $orderNo && $rowBefore['remark'] === $marker,
        'owned unpaid order mismatch');
    need(\think\facade\Cache::set($token, ['user' => ['user_id' => AI_ID],
        'password_version' => 0, 'login_type' => 'thirdparty'], 300) !== false,
        'isolated File token write failed');
    $path = '/api/cashier/orderInfo?storeId=10001&orderId=' . $orderId . '&client=H5';
    [$http, $body] = httpGet('http://127.0.0.1:' . PORT . $path, $token);
    $httpConclusive = true;
    $journal(['phase' => 'http_returned', 'marker' => $marker, 'order_id' => $orderId,
        'http' => $http, 'business_status' => $body['status'] ?? null]);
    need($http === 200 && (int) ($body['status'] ?? 0) === 200,
        'business HTTP response not successful');
    $data = $body['data'] ?? null;
    need(is_array($data)
        && array_keys($data) === ['order', 'personal', 'paymentMethods', 'deductionPreview'],
        'top-level response contract differs');
    need((int) ($data['order']['orderId'] ?? 0) === $orderId
        && (int) ($data['order']['pay_status'] ?? 0) === 10,
        'order response identity/status mismatch');
    need(is_array($data['paymentMethods']) && is_array($data['deductionPreview']),
        'payment or preview JSON type differs');
    [$mealHttp, $mealBody] = httpGet('http://127.0.0.1:' . PORT
        . '/api/mealCashier/orderInfo?orderId=' . $orderId . '&client=H5', $token);
    $mealData = $mealBody['data'] ?? null;
    need($mealHttp === 200 && (int) ($mealBody['status'] ?? 0) === 200
        && is_array($mealData) && is_array($mealData['order'] ?? null)
        && (int) ($mealData['order']['orderId'] ?? 0) === $orderId,
        'shared MealCashier route regression failed');
    foreach (['total_price', 'order_price', 'pay_price',
        'discount_amount', 'management_fee'] as $moneyField) {
        need(is_string($mealData['order'][$moneyField] ?? null),
            'shared MealCashier money JSON type differs: ' . $moneyField);
    }
    $rowAfter = one($store, 'SELECT * FROM ydy_meal_order WHERE ID=?', [$orderId]);
    need((int) ($rowAfter['pay_status'] ?? 0) === 10
        && (int) ($rowAfter['order_status'] ?? 0) === 10
        && (int) ($rowAfter['account_settlement_id'] ?? -1) === 0
        && $rowAfter['order_no'] === $orderNo && $rowAfter['remark'] === $marker,
        'owned order had an unexpected status/identity write');
    need(moneyCents($data['order']['pay_price'] ?? null)
        === moneyCents($rowAfter['pay_price'] ?? null),
        'response amount differs from post-request original row');
    $allowedMarketingFields = ['order_price', 'pay_price', 'management_fee',
        'discount_amount', 'marketing_strategy_id', 'marketing_strategy_time_range_id',
        'time_range_mode', 'strategy_time_range_text', 'actual_strategy_type',
        'actual_strategy_value', 'update_time'];
    foreach ($rowBefore as $field => $value) {
        if ($value !== ($rowAfter[$field] ?? null)) {
            need(in_array($field, $allowedMarketingFields, true),
                'unexpected owned-order write field');
        }
    }
    need(snapshot($ai, $store, $orderId) === $before,
        'existing actor, account or ledger changed');
    need(countRows($store, 'SELECT COUNT(*) FROM ydy_initial_menu WHERE MEAL_ORDER_ID=?',
        [$orderId]) === 0
        && countRows($store, 'SELECT COUNT(*) FROM ydy_meal_order_pickup WHERE MEAL_ORDER_ID=?',
            [$orderId]) === 0, 'owned order gained child rows');
    $deleted = $store->prepare('DELETE FROM ydy_meal_order WHERE ID=? AND ORDER_NO=?'
        . ' AND REMARK=? AND USER_ID=? AND PAY_STATUS=10 AND ORDER_STATUS=10'
        . ' AND ACCOUNT_SETTLEMENT_ID=0');
    $deleted->execute([$orderId, $orderNo, $marker, STORE_USER_ID]);
    need($deleted->rowCount() === 1, 'exact owned order cleanup did not affect one row');
    need(countRows($store, 'SELECT COUNT(*) FROM ydy_meal_order WHERE ID=? OR ORDER_NO=?',
        [$orderId, $orderNo]) === 0, 'owned order remains after cleanup');
    need(snapshot($ai, $store) === $before, 'existing actor or ledger changed after cleanup');
    $journal(['phase' => 'owned_order_deleted', 'marker' => $marker,
        'order_id' => $orderId]);
    [$zhctHttp, $zhctBody] = httpGet('http://127.0.0.1:' . PORT
        . '/api/User/zhctInfo', $token, 'POST');
    $zhctBalance = $zhctBody['data']['balance'] ?? null;
    $storeBalance = one($store, 'SELECT BALANCE FROM ydy_user WHERE ID=? AND ACCOUNT=?',
        [STORE_USER_ID, ACTOR]);
    need($zhctHttp === 200 && (int)($zhctBody['status'] ?? 0) === 200
        && array_key_exists('balance', $zhctBody['data'] ?? [])
        && moneyCents($zhctBalance) === moneyCents($storeBalance['balance'] ?? null),
        'B User/zhctInfo original balance mismatch');
    [$assetsHttp, $assetsBody] = httpGet('http://127.0.0.1:' . PORT
        . '/api/User/assets', $token);
    $assets = $assetsBody['data']['assets'] ?? null;
    need($assetsHttp === 200 && (int)($assetsBody['status'] ?? 0) === 200
        && is_array($assets) && array_key_exists('balance', $assets)
        && array_key_exists('subsidy', $assets)
        && array_key_exists('total_balance', $assets), 'B User/assets read failed');
    fwrite(STDERR, 'assets_diag=' . json_encode(['http' => $assetsHttp,
        'balance_type' => gettype($assets['balance']),
        'cash_cents' => moneyCents($assets['balance'])]) . PHP_EOL);
    [$cashoutHttp, $cashoutBody] = httpGet('http://127.0.0.1:' . PORT
        . '/api/Cashout/read', $token, 'POST');
    $cashoutMoney = $cashoutBody['data']['money'] ?? null;
    need($cashoutHttp === 200 && (int)($cashoutBody['status'] ?? 0) === 200
        && array_key_exists('money', $cashoutBody['data'] ?? [])
        && moneyCents($cashoutMoney) === 0,
        'B Cashout/read zero-refundable branch differs');
    $rechargeMarker = 'CR' . $marker;
    $rechargeNow = time();
    need(countRows($ai, 'SELECT COUNT(*) FROM yoshop_recharge_order'
        . ' WHERE ORDER_NO=? OR NOTE=?', [$rechargeMarker, $rechargeMarker]) === 0,
        'B owned recharge marker collision');
    $rechargeInsert = $ai->prepare('INSERT INTO yoshop_recharge_order'
        . ' (ORDER_NO,USER_ID,PAY_PRICE,ACTUAL_MONEY,PAY_METHOD,PAY_STATUS,'
        . ' PAY_TIME,REFUNDABLE_MONEY,RECHARGE_DATE,NOTE,CREATE_BY,STORE_ID,'
        . ' PLATFORM,CREATE_TIME,UPDATE_TIME)'
        . " VALUES (?,?,0.80,0.80,'wechat',20,?,0.80,?,?,?,10001,'H5',?,?)");
    $rechargeInsert->execute([$rechargeMarker, AI_ID, $rechargeNow,
        date('Y-m-d'), $rechargeMarker, $rechargeMarker,
        $rechargeNow, $rechargeNow]);
    $rechargeId = (int)$ai->lastInsertId();
    need($rechargeId > 0, 'B owned recharge ID missing');
    $journal(['phase' => 'recharge_inserted', 'marker' => $marker,
        'recharge_id' => $rechargeId, 'recharge_marker' => $rechargeMarker]);
    $rechargeRow = one($ai, 'SELECT ORDER_ID,ORDER_NO,USER_ID,PAY_METHOD,PAY_STATUS,'
        . ' REFUNDABLE_MONEY,NOTE FROM yoshop_recharge_order WHERE ORDER_ID=?',
        [$rechargeId]);
    need((int)($rechargeRow['user_id'] ?? 0) === AI_ID
        && ($rechargeRow['order_no'] ?? '') === $rechargeMarker
        && ($rechargeRow['note'] ?? '') === $rechargeMarker
        && ($rechargeRow['pay_method'] ?? '') === 'wechat'
        && (int)($rechargeRow['pay_status'] ?? 0) === 20
        && moneyCents($rechargeRow['refundable_money'] ?? null) === 80,
        'B owned recharge original row mismatch');
    try {
        [$cashoutPositiveHttp, $cashoutPositiveBody] = httpGet(
            'http://127.0.0.1:' . PORT . '/api/Cashout/read', $token, 'POST');
        $cashoutPositiveMoney = $cashoutPositiveBody['data']['money'] ?? null;
        need($cashoutPositiveHttp === 200
            && (int)($cashoutPositiveBody['status'] ?? 0) === 200
            && moneyCents($cashoutPositiveMoney) === 80,
            'B Cashout/read positive amount differs from owned recharge');
    } finally {
        // read() has no business DML: exact cleanup is safe even if HTTP times out.
            need(countRows($ai, 'SELECT COUNT(*) FROM yoshop_cashout_order_detail'
                . ' WHERE RECHARGE_ORDER_ID=?', [$rechargeId]) === 0,
                'B owned recharge acquired cashout detail');
            $deleteRecharge = $ai->prepare('DELETE FROM yoshop_recharge_order'
                . ' WHERE ORDER_ID=? AND ORDER_NO=? AND NOTE=? AND USER_ID=?'
                . " AND PAY_METHOD='wechat' AND PAY_STATUS=20"
                . ' AND REFUNDABLE_MONEY=0.80 AND STORE_ID=10001');
            $deleteRecharge->execute([$rechargeId, $rechargeMarker,
                $rechargeMarker, AI_ID]);
            need($deleteRecharge->rowCount() === 1,
                'B exact owned recharge cleanup not one row');
            $journal(['phase' => 'recharge_deleted', 'marker' => $marker,
                'recharge_id' => $rechargeId]);
            need(snapshot($ai, $store) === $before,
                'B actor or ledger changed during cashout preview');
    }
    $staffMarker = $marker . 'STAFF';
    need(countRows($ai, 'SELECT COUNT(*) FROM yoshop_staff WHERE USER_ID=? AND IS_DELETE=0',
        [AI_ID]) === 0
        && countRows($ai, 'SELECT COUNT(*) FROM yoshop_staff WHERE NAME=?',
            [$staffMarker]) === 0, 'B AI staff fixture collision');
    $staffInsert = $ai->prepare('INSERT INTO yoshop_staff'
        . ' (USER_ID,NAME,STORE_ID,HEIGHT,WEIGHT,GENDER,BIRTHDAY,PHONE,IS_DELETE)'
        . " VALUES (?,?,10001,170,60,1,'1990-01-01','',0)");
    $staffInsert->execute([AI_ID, $staffMarker]);
    $aiStaffId = (int)$ai->lastInsertId();
    need($aiStaffId > 0, 'B AI staff fixture ID missing');
    $aiStaffRow = one($ai, 'SELECT ID,USER_ID,NAME,HEIGHT,WEIGHT FROM yoshop_staff WHERE ID=?',
        [$aiStaffId]);
    need((int)($aiStaffRow['user_id'] ?? 0) === AI_ID
        && ($aiStaffRow['name'] ?? '') === $staffMarker
        && (int)($aiStaffRow['height'] ?? 0) === 170
        && (int)($aiStaffRow['weight'] ?? 0) === 60
        && countRows($ai, 'SELECT COUNT(*) FROM yoshop_meal WHERE STAFF_ID=?',
            [$aiStaffId]) === 0, 'B AI staff fixture original readback mismatch');
    $journal(['phase' => 'ai_staff_inserted', 'marker' => $marker,
        'ai_staff_id' => $aiStaffId, 'staff_marker' => $staffMarker]);
    $dataReturned = false;
    try {
        [$dataHttp, $dataBody] = httpGet('http://127.0.0.1:' . PORT
            . '/api/User/data', $token, 'POST');
        $dataReturned = true;
        [$coursePostHttp, $coursePost] = httpGet('http://127.0.0.1:' . PORT
            . '/api/drill.course/all', $token, 'POST');
        [$courseGetHttp, $courseGet] = httpGet('http://127.0.0.1:' . PORT
            . '/api/drill.course/all', $token, 'GET');
        $courseList = $coursePost['data']['list'] ?? null;
        need($coursePostHttp === 200 && $courseGetHttp === 200
            && (int)($coursePost['status'] ?? 0) === 200
            && (int)($courseGet['status'] ?? 0) === 200
            && is_array($courseList) && count($courseList) > 0
            && $courseList === ($courseGet['data']['list'] ?? null),
            'B drill.course/all POST differs from proven GET route');
    } finally {
        if ($dataReturned) {
            foreach (['yoshop_meal', 'yoshop_habit', 'yoshop_habit_rpe',
                'yoshop_staff_weight'] as $table) {
                need(countRows($ai, 'SELECT COUNT(*) FROM ' . $table . ' WHERE STAFF_ID=?',
                    [$aiStaffId]) === 0, 'B AI staff acquired child: ' . $table);
            }
            $staffDelete = $ai->prepare('DELETE FROM yoshop_staff WHERE ID=? AND USER_ID=?'
                . ' AND NAME=? AND STORE_ID=10001 AND IS_DELETE=0');
            $staffDelete->execute([$aiStaffId, AI_ID, $staffMarker]);
            need($staffDelete->rowCount() === 1, 'B AI staff exact cleanup failed');
            $journal(['phase' => 'ai_staff_deleted', 'marker' => $marker,
                'ai_staff_id' => $aiStaffId]);
        }
    }
    $userData = $dataBody['data'] ?? null;
    fwrite(STDERR, 'user_data_diag=' . json_encode(['http' => $dataHttp,
        'status' => $dataBody['status'] ?? null,
        'message' => $dataBody['message'] ?? null,
        'keys' => is_array($userData) ? array_keys($userData) : [],
        'base_type' => gettype($userData['baseData'] ?? null),
        'dining_type' => gettype($userData['diningData'] ?? null)]) . PHP_EOL);
    need($dataHttp === 200 && (int)($dataBody['status'] ?? 0) === 200
        && is_array($userData) && is_string($userData['baseData'] ?? null)
        && is_array($userData['diningData'] ?? null), 'B User/data read failed');
    $typeMap = static function ($value) use (&$typeMap) {
        if (!is_array($value)) return gettype($value);
        $out = [];
        foreach ($value as $key => $item) $out[$key] = $typeMap($item);
        return $out;
    };
    echo json_encode(['status' => 'B_LIMITED_PASS', 'http' => $http,
        'order_key_type_sha' => hash('sha256', json_encode($typeMap($data['order']))),
        'full_data_type_sha' => hash('sha256', json_encode($typeMap($data))),
        'order_types' => $typeMap($data['order']),
        'data_types' => $typeMap($data),
        'response_keys' => array_keys($data), 'source_order_cleaned' => true,
        'shared_meal_cashier_http' => $mealHttp,
        'zhct_info' => ['http' => $zhctHttp, 'balance_type' => gettype($zhctBalance),
            'balance_cents' => moneyCents($zhctBalance)],
        'assets' => ['http' => $assetsHttp, 'balance_type' => gettype($assets['balance']),
            'cash_cents' => moneyCents($assets['balance']),
            'subsidy_type' => gettype($assets['subsidy']),
            'total_type' => gettype($assets['total_balance'])],
        'cashout_read' => ['http' => $cashoutHttp,
            'money_type' => gettype($cashoutMoney), 'money_cents' => 0],
        'cashout_read_positive' => ['http' => $cashoutPositiveHttp,
            'money_type' => gettype($cashoutPositiveMoney),
            'money_cents' => moneyCents($cashoutPositiveMoney),
            'owned_recharge_deleted' => true],
        'drill_course_all' => ['post_http' => $coursePostHttp,
            'get_http' => $courseGetHttp, 'count' => count($courseList),
            'same_lane_methods_equal' => true],
        'user_data' => ['http' => $dataHttp,
            'base_nonempty' => $userData['baseData'] !== '',
            'dining_count' => count($userData['diningData']),
            'content_sha' => hash('sha256', json_encode($userData, JSON_UNESCAPED_UNICODE)),
            'base_text' => $userData['baseData'],
            'first_dining' => $userData['diningData'][0] ?? null,
            'last_dining' => $userData['diningData'][89] ?? null],
        'existing_actor_unchanged' => true,
        'marketing_refresh_on_owned_row' => $rowBefore !== $rowAfter,
        'external_payment_invoked' => false], JSON_UNESCAPED_SLASHES), PHP_EOL;
} catch (Throwable $error) {
    // Fail closed: if HTTP state is uncertain, retain the exact row and private pointer.
    fwrite(STDERR, 'STOP ' . get_class($error) . ' phase=' . ($httpConclusive ? 'post_http' : 'pre_or_in_http')
        . ' marker_sha=' . hash('sha256', $marker) . ' journal=' . $journalPath . PHP_EOL);
    throw $error;
} finally {
    \think\facade\Cache::delete($token);
    $tokenDeleted = !\think\facade\Cache::has($token);
    $journal(['phase' => 'token_cleanup', 'marker' => $marker,
        'order_id' => $orderId, 'token_zero' => $tokenDeleted]);
    fwrite(STDERR, 'token_zero=' . ($tokenDeleted ? '1' : '0')
        . ' private_journal=' . $journalPath . PHP_EOL);
}
