331 lines
21 KiB
PHP
331 lines
21 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
/**
|
||
* Real ORM/transaction regression test, using a disposable local MySQL database only.
|
||
* Run with ZYT_UNLINK_TEST_MYSQL_PORT pointing at an isolated, empty-password root instance.
|
||
* Never loads the application's database configuration or touches business data.
|
||
*/
|
||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||
|
||
use app\adminapi\lists\tcm\PrescriptionOrderLists;
|
||
use app\adminapi\logic\tcm\PrescriptionOrderLogic;
|
||
use app\adminapi\validate\tcm\PrescriptionOrderValidate;
|
||
use think\Container;
|
||
use think\DbManager;
|
||
use think\facade\Db;
|
||
|
||
$port = (int) getenv('ZYT_UNLINK_TEST_MYSQL_PORT');
|
||
if ($port <= 0) {
|
||
fwrite(STDERR, "Set ZYT_UNLINK_TEST_MYSQL_PORT to an isolated local MySQL instance.\n");
|
||
exit(1);
|
||
}
|
||
$isWorker = ($argv[1] ?? '') === '--worker';
|
||
$database = $isWorker ? (string) getenv('ZYT_UNLINK_TEST_DATABASE') : 'prescription_unlink_test_' . bin2hex(random_bytes(6));
|
||
if (!preg_match('/^prescription_unlink_test_[a-f0-9]{12}$/', $database)) {
|
||
throw new RuntimeException('Only this test\'s disposable database names are allowed');
|
||
}
|
||
$pdo = new PDO("mysql:host=127.0.0.1;port={$port};charset=utf8mb4", 'root', '', [
|
||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||
]);
|
||
if (!$isWorker) {
|
||
$pdo->exec("CREATE DATABASE `{$database}` CHARACTER SET utf8mb4");
|
||
}
|
||
$pdo->exec("USE `{$database}`");
|
||
$testApp = new think\App(); // Do not initialize: production config/services must never be loaded.
|
||
$manager = new DbManager();
|
||
$manager->setConfig([
|
||
'default' => 'mysql', 'auto_timestamp' => true, 'datetime_format' => false,
|
||
'connections' => ['mysql' => [
|
||
'type' => 'mysql', 'hostname' => '127.0.0.1', 'hostport' => $port,
|
||
'database' => $database, 'username' => 'root', 'password' => '',
|
||
'charset' => 'utf8mb4', 'prefix' => 'zyt_', 'fields_strict' => true,
|
||
]],
|
||
]);
|
||
Container::getInstance()->instance('think\DbManager', $manager);
|
||
Container::getInstance()->instance('config', new think\Config());
|
||
$testLang = new think\Lang($testApp);
|
||
think\Validate::maker(static fn (think\Validate $validator) => $validator->setLang($testLang));
|
||
|
||
$checks = 0;
|
||
$expect = static function (bool $ok, string $message) use (&$checks): void {
|
||
if (!$ok) {
|
||
throw new RuntimeException($message . ' | ' . PrescriptionOrderLogic::getError());
|
||
}
|
||
$checks++;
|
||
};
|
||
$admin = ['root' => 1, 'admin_id' => 1, 'name' => '隔离测试管理员'];
|
||
if ($isWorker) {
|
||
echo "ready\n";
|
||
flush();
|
||
$operation = $argv[2];
|
||
$params = ['id' => (int) $argv[3], 'pay_order_id' => (int) $argv[4], 'order_type' => 3, 'pay_amount' => 300];
|
||
$result = PrescriptionOrderLogic::$operation($params, 1, $admin);
|
||
echo json_encode(['success' => is_array($result), 'error' => PrescriptionOrderLogic::getError(),
|
||
'paid' => $result['paid'] ?? null, 'linked_paid' => $result['linked_pay_paid_total'] ?? null]) . "\n";
|
||
exit(0);
|
||
}
|
||
|
||
try {
|
||
$pdo->exec('CREATE TABLE zyt_tcm_prescription_order (
|
||
id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(50), diagnosis_id INT DEFAULT 1,
|
||
prescription_id INT DEFAULT 1, creator_id INT DEFAULT 1, amount DECIMAL(10,2) NOT NULL,
|
||
agency_collect_amount DECIMAL(10,2) NULL, linked_pay_order_id INT NULL,
|
||
prescription_audit_status INT DEFAULT 1, payment_slip_audit_status INT DEFAULT 1,
|
||
payment_slip_audit_remark VARCHAR(500) DEFAULT "", fulfillment_status INT DEFAULT 6,
|
||
completion_request INT DEFAULT 0, completion_request_time INT DEFAULT 0,
|
||
completion_request_by INT DEFAULT 0, completion_request_by_name VARCHAR(100) DEFAULT "",
|
||
paid DECIMAL(10,2) DEFAULT 0, refund_amount DECIMAL(10,2) DEFAULT 0, internal_cost DECIMAL(10,2) DEFAULT 0,
|
||
remark_extra VARCHAR(500) DEFAULT "", create_time INT DEFAULT 0,
|
||
update_time INT DEFAULT 0, delete_time INT NULL
|
||
) ENGINE=InnoDB');
|
||
$pdo->exec('CREATE TABLE zyt_order (
|
||
id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(50), patient_id INT DEFAULT 1,
|
||
creator_id INT DEFAULT 0, order_type INT DEFAULT 3, amount DECIMAL(10,2), status INT,
|
||
is_exempt INT DEFAULT 0, remark VARCHAR(200) DEFAULT "", payment_method VARCHAR(50) DEFAULT "",
|
||
create_type VARCHAR(50) DEFAULT "", payment_time INT NULL,
|
||
create_time INT DEFAULT 0, update_time INT DEFAULT 0, delete_time INT NULL
|
||
) ENGINE=InnoDB');
|
||
$pdo->exec('CREATE TABLE zyt_tcm_prescription_order_pay_order (
|
||
id INT PRIMARY KEY AUTO_INCREMENT, prescription_order_id INT, pay_order_id INT,
|
||
create_time INT, UNIQUE KEY uk_po_pay (prescription_order_id,pay_order_id)
|
||
) ENGINE=InnoDB');
|
||
$pdo->exec('CREATE TABLE zyt_tcm_prescription_order_log (
|
||
id INT PRIMARY KEY AUTO_INCREMENT, prescription_order_id INT, admin_id INT,
|
||
admin_name VARCHAR(64), action VARCHAR(32), summary VARCHAR(500), create_time INT
|
||
) ENGINE=InnoDB');
|
||
$pdo->exec('CREATE TABLE zyt_admin (id INT PRIMARY KEY, name VARCHAR(50), delete_time INT NULL)');
|
||
$pdo->exec("INSERT INTO zyt_admin VALUES (1, '隔离测试管理员', NULL)");
|
||
$pdo->exec('CREATE TABLE zyt_admin_role (admin_id INT, role_id INT)');
|
||
$pdo->exec('CREATE TABLE zyt_system_role_menu (role_id INT, menu_id INT)');
|
||
$pdo->exec('CREATE TABLE zyt_system_menu (
|
||
id INT PRIMARY KEY AUTO_INCREMENT, pid INT, type VARCHAR(5), name VARCHAR(100), icon VARCHAR(50),
|
||
sort INT, perms VARCHAR(100), paths VARCHAR(100), component VARCHAR(100), selected VARCHAR(100),
|
||
params VARCHAR(100), is_cache INT, is_show INT, is_disable INT DEFAULT 0, create_time INT, update_time INT
|
||
)');
|
||
$pdo->exec('CREATE TABLE zyt_tcm_diagnosis (id INT PRIMARY KEY, assistant_id INT, delete_time INT NULL)');
|
||
$pdo->exec('INSERT INTO zyt_tcm_diagnosis VALUES (1,1,NULL)');
|
||
|
||
$fixture = static function (string $amount, array $payments, array $orderFields = []): array {
|
||
$id = (int) Db::name('tcm_prescription_order')->insertGetId(array_merge([
|
||
'order_no' => 'TEST-' . bin2hex(random_bytes(4)), 'amount' => $amount,
|
||
'create_time' => time(),
|
||
], $orderFields));
|
||
$payIds = [];
|
||
foreach ($payments as $payment) {
|
||
$payId = (int) Db::name('order')->insertGetId(array_merge([
|
||
'order_no' => 'PAY-' . bin2hex(random_bytes(4)), 'status' => 2,
|
||
'create_time' => time(),
|
||
], $payment));
|
||
Db::name('tcm_prescription_order_pay_order')->insert([
|
||
'prescription_order_id' => $id, 'pay_order_id' => $payId, 'create_time' => time(),
|
||
]);
|
||
$payIds[] = $payId;
|
||
}
|
||
Db::name('tcm_prescription_order')->where('id', $id)->update(['linked_pay_order_id' => $payIds[0] ?? null]);
|
||
return [$id, $payIds];
|
||
};
|
||
$remove = static fn (int $id, int $payId) => PrescriptionOrderLogic::unlinkPayOrder([
|
||
'id' => $id, 'pay_order_id' => $payId,
|
||
], 1, $admin);
|
||
$snapshot = static fn (int $id): array => [
|
||
Db::name('tcm_prescription_order')->where('id', $id)->find(),
|
||
Db::name('tcm_prescription_order_pay_order')->where('prescription_order_id', $id)->order('id')->select()->toArray(),
|
||
Db::name('tcm_prescription_order_log')->where('prescription_order_id', $id)->order('id')->select()->toArray(),
|
||
];
|
||
$amounts = static function (array $result, float $total, float $paid, float $collect, bool $checkStoredPaid = true) use ($expect): void {
|
||
$expect((float) $result['amount'] === $total, 'Unlink must leave the business total unchanged');
|
||
$expect((float) $result['linked_pay_paid_total'] === $paid, 'Paid total must use remaining active payments');
|
||
$expect((float) $result['agency_collect_amount'] === $collect, 'Collection snapshot must equal unchanged total minus remaining payments');
|
||
if ($checkStoredPaid) {
|
||
$expect((float) $result['paid'] === $paid, 'Response paid must match remaining effective receipts');
|
||
$expect((float) Db::name('tcm_prescription_order')->where('id', $result['id'])->value('paid') === $paid,
|
||
'The paid field must be persisted, not only changed in the response');
|
||
}
|
||
};
|
||
|
||
// Missing permission is denied even before the new menu has been installed.
|
||
[$id, $payIds] = $fixture('1000.00', [['amount' => '200.00'], ['amount' => '400.00', 'status' => 5]], ['completion_request' => 1]);
|
||
$before = $snapshot($id);
|
||
$expect(PrescriptionOrderLogic::unlinkPayOrder(['id' => $id, 'pay_order_id' => $payIds[0]], 1,
|
||
['root' => 0, 'admin_id' => 1, 'role_id' => [], 'name' => 'No permission']) === false, 'Viewing/owning an order must not imply unlink permission');
|
||
$expect($snapshot($id) === $before, 'Permission failure must not change data');
|
||
|
||
$originalPayments = Db::name('order')->whereIn('id', $payIds)->select()->toArray();
|
||
$out = $remove($id, $payIds[0]);
|
||
$expect(is_array($out), 'Paid payment removal must succeed');
|
||
$amounts($out, 1000.0, 400.0, 600.0);
|
||
$expect((int) $out['fulfillment_status'] === 6 && (int) $out['payment_slip_audit_status'] === 1,
|
||
'Signed order and approved audit must remain in the same statistics scope');
|
||
$expect((int) $out['completion_request'] === 1, 'Existing completion request must be preserved');
|
||
$expect($out['pay_order_ids'] === [$payIds[1]] && (int) $out['linked_pay_order_id'] === $payIds[1], 'Primary link must move to the first remaining payment');
|
||
$expect(Db::name('order')->whereIn('id', $payIds)->select()->toArray() === $originalPayments, 'Unlink must never delete/refund/edit original payments');
|
||
$log = (string) Db::name('tcm_prescription_order_log')->where('prescription_order_id', $id)->value('summary');
|
||
$expect(str_contains($log, '总金额 ¥1000.00 不变') && str_contains($log, '(paid)¥0.00 → ¥400.00'),
|
||
'Audit log must record unchanged total and the paid correction');
|
||
$before = $snapshot($id);
|
||
$expect($remove($id, $payIds[0]) === false && $snapshot($id) === $before, 'Repeated request must not deduct again');
|
||
$amounts($remove($id, $payIds[1]), 1000.0, 0.0, 1000.0);
|
||
$expect(Db::name('tcm_prescription_order')->where('id', $id)->value('linked_pay_order_id') === null, 'Removing last payment must clear legacy primary link');
|
||
|
||
[$id, $payIds] = $fixture('1950.00', [['amount' => '100.00', 'is_exempt' => 1], ['amount' => '1850.00']]);
|
||
$amounts($remove($id, $payIds[0]), 1950.0, 1850.0, 100.0);
|
||
$out = $remove($id, $payIds[1]);
|
||
$amounts($out, 1950.0, 0.0, 1950.0);
|
||
$expect($out['linked_pay_orders'] === [] && $out['pay_order_ids'] === [], 'All payments can be removed without stale rows');
|
||
|
||
[$id, $payIds] = $fixture('1.03', [['amount' => '0.29'], ['amount' => '0.14']]);
|
||
$amounts($remove($id, $payIds[0]), 1.03, 0.14, 0.89);
|
||
[$otherId, $otherPayIds] = $fixture('2.00', [['amount' => '1.00']]);
|
||
$before = $snapshot($id);
|
||
$expect($remove($id, $otherPayIds[0]) === false && $snapshot($id) === $before, 'Foreign payment ID must not affect either order');
|
||
$expect(count($snapshot($otherId)[1]) === 1, 'Foreign order must retain its payment');
|
||
|
||
foreach ([3, 4] as $status) {
|
||
[$id, $payIds] = $fixture('100.00', [['amount' => '100.00']], ['fulfillment_status' => $status, 'paid' => '100.00']);
|
||
$before = $snapshot($id);
|
||
$expect($remove($id, $payIds[0]) === false && $snapshot($id) === $before, 'Completed/cancelled orders must retain amount and paid snapshot');
|
||
}
|
||
foreach ([['amount' => '100.00', 'status' => 4], ['amount' => '100.00', 'status' => 1],
|
||
['amount' => '100.00', 'delete_time' => time()], ['amount' => '-1.00']] as $payment) {
|
||
[$id, $payIds] = $fixture('100.00', [$payment]);
|
||
$before = $snapshot($id);
|
||
$expect($remove($id, $payIds[0]) === false && $snapshot($id) === $before, 'Refunded/deleted/unpaid/invalid amount must not be removed');
|
||
}
|
||
[$id, $payIds] = $fixture('100.00', [['amount' => '100.00']], ['delete_time' => time()]);
|
||
$before = $snapshot($id);
|
||
$expect($remove($id, $payIds[0]) === false && $snapshot($id) === $before, 'Deleted order must not be changed');
|
||
|
||
// Overpayment and stale paid values do not prevent unlinking: total must not be reduced.
|
||
foreach (['0.00', '9999.00', '101.00'] as $stalePaid) {
|
||
[$id, $payIds] = $fixture('100.00', [['amount' => '101.00'], ['amount' => '20.00']], ['paid' => $stalePaid]);
|
||
$amounts($remove($id, $payIds[0]), 100.0, 20.0, 80.0);
|
||
}
|
||
[$id, $payIds] = $fixture('0.00', [['amount' => '10.00']]);
|
||
$amounts($remove($id, $payIds[0]), 0.0, 0.0, 0.0);
|
||
|
||
// Refund balances can be lower than receipt face values. Never resurrect refunded money.
|
||
[$id, $payIds] = $fixture('1500.00', [['amount' => '1000.00'], ['amount' => '500.00']],
|
||
['paid' => '1300.00', 'refund_amount' => '200.00']);
|
||
$out = $remove($id, $payIds[1]);
|
||
$amounts($out, 1500.0, 1000.0, 500.0, false);
|
||
$expect((float) $out['paid'] === 800.0 && (float) Db::name('tcm_prescription_order')->where('id', $id)->value('paid') === 800.0,
|
||
'Partial refunds must retain the net paid balance when removing a receipt');
|
||
$amounts($remove($id, $payIds[0]), 1500.0, 0.0, 1500.0);
|
||
[$id, $payIds] = $fixture('1000.00', [['amount' => '200.00', 'status' => 4], ['amount' => '500.00'], ['amount' => '300.00']],
|
||
['paid' => '800.00', 'refund_amount' => '200.00']);
|
||
$amounts($remove($id, $payIds[1]), 1000.0, 300.0, 700.0);
|
||
[$id, $payIds] = $fixture('1500.00', [['amount' => '1000.00'], ['amount' => '500.00']],
|
||
['fulfillment_status' => 10, 'paid' => '0.00', 'refund_amount' => '1500.00']);
|
||
$out = $remove($id, $payIds[1]);
|
||
$amounts($out, 1500.0, 1000.0, 500.0, false);
|
||
$expect((float) $out['paid'] === 0.0 && (int) $out['fulfillment_status'] === 10, 'Full refund must not acquire a paid balance again');
|
||
|
||
// Remaining refunds and soft-deleted payments must not reappear in paid sums.
|
||
[$id, $payIds] = $fixture('1000.00', [
|
||
['amount' => '200.00', 'status' => 4], ['amount' => '300.00'],
|
||
['amount' => '100.00', 'delete_time' => time()], ['amount' => '50.00', 'status' => 5],
|
||
]);
|
||
$amounts($remove($id, $payIds[1]), 1000.0, 50.0, 950.0);
|
||
$listsReflection = new ReflectionClass(PrescriptionOrderLists::class);
|
||
$sum = $listsReflection->getMethod('sumLinkedPayForPrescriptionOrderIds');
|
||
$expect($sum->invoke($listsReflection->newInstanceWithoutConstructor(), [$id]) === 50.0,
|
||
'List summary and detail must use the same remaining paid amount');
|
||
|
||
// A failed audit-log insert must roll back the removed link, paid and collection snapshot.
|
||
[$id, $payIds] = $fixture('1000.00', [['amount' => '200.00']], ['paid' => '200.00', 'agency_collect_amount' => '800.00']);
|
||
$before = $snapshot($id);
|
||
$pdo->exec("CREATE TRIGGER reject_unlink_log BEFORE INSERT ON zyt_tcm_prescription_order_log
|
||
FOR EACH ROW SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'forced audit log failure'");
|
||
$expect($remove($id, $payIds[0]) === false, 'Audit log failure must reject the operation');
|
||
$expect($snapshot($id) === $before, 'Transaction must roll back link, amount, snapshot and log');
|
||
$pdo->exec('DROP TRIGGER reject_unlink_log');
|
||
$amounts($remove($id, $payIds[0]), 1000.0, 0.0, 1000.0);
|
||
|
||
// Permission migration is idempotent, grants no roles, and enables explicitly granted access.
|
||
$pdo->exec("INSERT INTO zyt_system_menu (perms,is_disable) VALUES ('tcm.prescriptionOrder/lists',0)");
|
||
$migration = file_get_contents(dirname(__DIR__) . '/sql/1.9.20260831/add_prescription_order_unlink_pay_order_menu.sql');
|
||
foreach ([1, 2] as $_) {
|
||
$pdo->exec($migration);
|
||
}
|
||
$menuId = (int) Db::name('system_menu')->where('perms', 'tcm.prescriptionOrder/unlinkPayOrder')->value('id');
|
||
$expect(Db::name('system_menu')->where('perms', 'tcm.prescriptionOrder/unlinkPayOrder')->count() === 1, 'Menu migration must be repeatable');
|
||
$expect(Db::name('system_role_menu')->count() === 0, 'Migration must not grant permissions automatically');
|
||
Db::name('admin_role')->insert(['admin_id' => 2, 'role_id' => 2]);
|
||
Db::name('system_role_menu')->insert(['role_id' => 2, 'menu_id' => $menuId]);
|
||
[$id, $payIds] = $fixture('100.00', [['amount' => '50.00']]);
|
||
$out = PrescriptionOrderLogic::unlinkPayOrder(['id' => $id, 'pay_order_id' => $payIds[0]], 2,
|
||
['root' => 0, 'admin_id' => 2, 'role_id' => [2], 'name' => '获授权测试员']);
|
||
$expect(is_array($out) && !array_key_exists('internal_cost', $out) && !array_key_exists('remark_extra', $out), 'Explicit permission must work while preserving financial/remark masking');
|
||
|
||
// Existing add/link operations also participate in the same transaction/row lock.
|
||
[$id, $payIds] = $fixture('1000.00', [['amount' => '200.00']]);
|
||
$out = PrescriptionOrderLogic::addPayOrder(['id' => $id, 'order_type' => 3, 'pay_amount' => 300], 1, $admin);
|
||
$expect(is_array($out), 'Existing add payment flow must still succeed');
|
||
$amounts($out, 1000.0, 500.0, 500.0, false);
|
||
$amounts($remove($id, (int) end($out['pay_order_ids'])), 1000.0, 200.0, 800.0);
|
||
$freeId = (int) Db::name('order')->insertGetId(['order_no' => 'FREE-PAYMENT', 'patient_id' => 1, 'status' => 2, 'amount' => 50, 'create_time' => time()]);
|
||
$out = PrescriptionOrderLogic::linkPayOrder(['id' => $id, 'pay_order_id' => $freeId], 1, $admin);
|
||
$expect(is_array($out), 'Existing link payment flow must still succeed');
|
||
$amounts($out, 1000.0, 250.0, 750.0, false);
|
||
|
||
// Separate PHP/DB connections race against a held parent row lock. Both are ready
|
||
// before release, so this tests the database lock rather than sequential double-clicks.
|
||
$race = static function (int $id, array $operations) use ($database): array {
|
||
putenv('ZYT_UNLINK_TEST_DATABASE=' . $database);
|
||
Db::startTrans();
|
||
Db::name('tcm_prescription_order')->where('id', $id)->lock(true)->find();
|
||
$workers = [];
|
||
try {
|
||
foreach ($operations as [$operation, $payId]) {
|
||
$process = proc_open([PHP_BINARY, __FILE__, '--worker', $operation, (string) $id, (string) $payId],
|
||
[0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
|
||
if (!is_resource($process)) throw new RuntimeException('Unable to start concurrency worker');
|
||
fclose($pipes[0]);
|
||
$workers[] = [$process, $pipes];
|
||
if (trim((string) fgets($pipes[1])) !== 'ready') throw new RuntimeException('Concurrency worker did not initialize');
|
||
}
|
||
} finally {
|
||
Db::commit();
|
||
}
|
||
$results = [];
|
||
foreach ($workers as [$process, $pipes]) {
|
||
$output = stream_get_contents($pipes[1]);
|
||
$error = stream_get_contents($pipes[2]);
|
||
fclose($pipes[1]);
|
||
fclose($pipes[2]);
|
||
if (proc_close($process) !== 0) throw new RuntimeException('Concurrency worker failed: ' . $error . $output);
|
||
$results[] = json_decode(trim($output), true, 512, JSON_THROW_ON_ERROR);
|
||
}
|
||
return $results;
|
||
};
|
||
[$id, $payIds] = $fixture('1000.00', [['amount' => '200.00']]);
|
||
$results = $race($id, [['unlinkPayOrder', $payIds[0]], ['unlinkPayOrder', $payIds[0]]]);
|
||
$expect(count(array_filter($results, static fn (array $r): bool => $r['success'])) === 1, 'Concurrent duplicate removal must succeed exactly once');
|
||
$expect((float) Db::name('tcm_prescription_order')->where('id', $id)->value('amount') === 1000.0, 'Concurrent duplicate removal must never change total');
|
||
$expect((float) Db::name('tcm_prescription_order')->where('id', $id)->value('paid') === 0.0, 'Concurrent duplicate removal must persist the remaining paid balance');
|
||
$expect(Db::name('tcm_prescription_order_log')->where('prescription_order_id', $id)->count() === 1, 'Concurrent duplicate must write one audit log');
|
||
foreach (['addPayOrder', 'linkPayOrder'] as $operation) {
|
||
[$id, $payIds] = $fixture('1000.00', [['amount' => '200.00']]);
|
||
$freeId = (int) Db::name('order')->insertGetId(['order_no' => 'RACE-' . $id, 'patient_id' => 1, 'status' => 2, 'amount' => 300, 'create_time' => time()]);
|
||
$results = $race($id, [['unlinkPayOrder', $payIds[0]], [$operation, $freeId]]);
|
||
$expect($results[0]['success'] && $results[1]['success'], 'Adding/linking during removal must retain both changes');
|
||
$row = Db::name('tcm_prescription_order')->where('id', $id)->find();
|
||
$expect((float) $row['amount'] === 1000.0 && (float) $row['agency_collect_amount'] === 700.0, 'Concurrent add/remove must preserve the total and update the collection snapshot');
|
||
$expect((float) $results[0]['paid'] === (float) $results[0]['linked_paid'], 'Removal must persist paid using the receipts visible within its transaction');
|
||
$remaining = Db::name('tcm_prescription_order_pay_order')->where('prescription_order_id', $id)->column('pay_order_id');
|
||
$expect(count($remaining) === 1 && !in_array($payIds[0], array_map('intval', $remaining), true), 'Concurrent link replacement must not resurrect a removed payment');
|
||
}
|
||
|
||
foreach ([[], ['id' => 1], ['id' => 0, 'pay_order_id' => 1], ['id' => 1, 'pay_order_id' => -1],
|
||
['id' => 1, 'pay_order_id' => 1.5], ['id' => 1, 'pay_order_id' => [1]]] as $params) {
|
||
$expect(!(new PrescriptionOrderValidate())->scene('unlinkPayOrder')->check($params), 'Request must require two positive integer IDs');
|
||
}
|
||
$expect((new PrescriptionOrderValidate())->scene('unlinkPayOrder')->check(['id' => 1, 'pay_order_id' => 2]), 'Valid IDs must pass validation');
|
||
echo "PrescriptionOrderUnlinkPayOrderTest: {$checks} assertions passed\n";
|
||
} finally {
|
||
$manager->connect()->close();
|
||
$pdo->exec("DROP DATABASE `{$database}`");
|
||
}
|