积分与余额账务系统硬切重构
This commit is contained in:
1 parent
ca12226b39
commit
5cae5b5c93
159 files changed
+7275
-4718
No files matched your search
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
use AKSRT\Ledger\AssetAmount;
|
||||
use AKSRT\Ledger\AssetType;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class AssetAmountTest extends TestCase {
|
||||
#[DataProvider('validBalanceProvider')]
|
||||
public function test_balance_parses_exact_decimal_strings(string $input, int $minor, string $decimal): void {
|
||||
$amount = AssetAmount::balance($input);
|
||||
self::assertSame(AssetType::BALANCE, $amount->asset());
|
||||
self::assertSame($minor, $amount->minor());
|
||||
self::assertSame($decimal, $amount->decimal());
|
||||
}
|
||||
|
||||
public static function validBalanceProvider(): array {
|
||||
return [
|
||||
'zero' => ['0', 0, '0.00'],
|
||||
'one decimal' => ['12.3', 1230, '12.30'],
|
||||
'two decimals' => ['12.34', 1234, '12.34'],
|
||||
'integer' => ['999', 99900, '999.00'],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('invalidBalanceProvider')]
|
||||
public function test_balance_rejects_non_canonical_input(string $input): void {
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
AssetAmount::balance($input);
|
||||
}
|
||||
|
||||
public static function invalidBalanceProvider(): array {
|
||||
return [
|
||||
'negative' => ['-1.00'],
|
||||
'float precision' => ['1.001'],
|
||||
'scientific notation' => ['1e2'],
|
||||
'leading zero' => ['01.00'],
|
||||
'leading decimal' => ['.50'],
|
||||
'trailing decimal' => ['1.'],
|
||||
'plus sign' => ['+1.00'],
|
||||
'comma' => ['1,00'],
|
||||
'empty' => [''],
|
||||
];
|
||||
}
|
||||
|
||||
public function test_balance_rejects_integer_overflow(): void {
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
AssetAmount::balance((string) PHP_INT_MAX . '.99');
|
||||
}
|
||||
|
||||
public function test_points_are_integer_minor_units(): void {
|
||||
self::assertSame(37, AssetAmount::points(37)->minor());
|
||||
self::assertSame('37', AssetAmount::points(37)->decimal());
|
||||
}
|
||||
|
||||
public function test_negative_points_are_rejected(): void {
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
AssetAmount::points(-1);
|
||||
}
|
||||
|
||||
public function test_balance_minor_helpers_do_not_use_floats(): void {
|
||||
self::assertSame(1234, AssetAmount::balance_minor('12.34'));
|
||||
self::assertSame('12.34', AssetAmount::decimal_from_minor(1234));
|
||||
}
|
||||
|
||||
public function test_percentage_rounds_half_up_in_minor_units(): void {
|
||||
self::assertSame(266, AssetAmount::percentage(333, 80));
|
||||
self::assertSame(1, AssetAmount::percentage(1, 50));
|
||||
self::assertSame(0, AssetAmount::percentage(1, 49));
|
||||
}
|
||||
|
||||
public function test_percentage_rejects_out_of_range_values(): void {
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
AssetAmount::percentage(100, 101);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
use AKSRT\Ledger\AssetType;
|
||||
use AKSRT\Ledger\LedgerCommand;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class LedgerCommandTest extends TestCase {
|
||||
public function test_transfer_splits_principal_fee_and_net_receipt(): void {
|
||||
$command = LedgerCommand::transfer(
|
||||
'client-transfer:1:550e8400-e29b-41d4-a716-446655440000',
|
||||
'transfer',
|
||||
10,
|
||||
20,
|
||||
AssetType::BALANCE,
|
||||
1000,
|
||||
125
|
||||
);
|
||||
|
||||
self::assertSame([-875, -125, 875], array_column($command->entries, 'delta_minor'));
|
||||
self::assertSame(['transfer_out', 'fee', 'transfer_in'], array_column($command->entries, 'entry_type'));
|
||||
self::assertSame(1000, $command->metadata['gross_minor']);
|
||||
self::assertSame(125, $command->metadata['fee_minor']);
|
||||
self::assertSame(875, $command->metadata['net_minor']);
|
||||
self::assertSame(-125, array_sum(array_column($command->entries, 'delta_minor')));
|
||||
}
|
||||
|
||||
public function test_transfer_rejects_net_below_one_minor_unit(): void {
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
LedgerCommand::transfer('transfer:key', 'transfer', 1, 2, AssetType::POINTS, 10, 10);
|
||||
}
|
||||
|
||||
public function test_transfer_rejects_negative_fee_before_arithmetic(): void {
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
LedgerCommand::transfer('transfer:key', 'transfer', 1, 2, AssetType::POINTS, PHP_INT_MAX, -1);
|
||||
}
|
||||
|
||||
public function test_validation_returns_400_for_bad_operation_key(): void {
|
||||
$command = new LedgerCommand('bad key', 'credit', [[
|
||||
'user_id' => 1,
|
||||
'asset' => 'points',
|
||||
'delta_minor' => 1,
|
||||
'entry_type' => 'credit',
|
||||
]]);
|
||||
$error = $command->validate();
|
||||
self::assertInstanceOf(WP_Error::class, $error);
|
||||
self::assertSame('aksrt_ledger_operation_key', $error->get_error_code());
|
||||
self::assertSame(400, $error->get_error_data()['status']);
|
||||
}
|
||||
|
||||
public function test_payload_hash_is_stable_for_metadata_key_order(): void {
|
||||
$entries = [['user_id' => 1, 'asset' => 'points', 'delta_minor' => 2, 'entry_type' => 'credit']];
|
||||
$first = new LedgerCommand('stable:key', 'credit', $entries, '', '', 0, ['b' => 2, 'a' => 1]);
|
||||
$second = new LedgerCommand('stable:key', 'credit', $entries, '', '', 0, ['a' => 1, 'b' => 2]);
|
||||
self::assertSame($first->payload_hash(), $second->payload_hash());
|
||||
}
|
||||
|
||||
public function test_payload_hash_changes_with_business_payload(): void {
|
||||
$first = new LedgerCommand('stable:key', 'credit', [[
|
||||
'user_id' => 1,
|
||||
'asset' => 'points',
|
||||
'delta_minor' => 2,
|
||||
'entry_type' => 'credit',
|
||||
]]);
|
||||
$second = new LedgerCommand('stable:key', 'credit', [[
|
||||
'user_id' => 1,
|
||||
'asset' => 'points',
|
||||
'delta_minor' => 3,
|
||||
'entry_type' => 'credit',
|
||||
]]);
|
||||
self::assertNotSame($first->payload_hash(), $second->payload_hash());
|
||||
}
|
||||
|
||||
public function test_validation_rejects_empty_entries_and_invalid_asset(): void {
|
||||
$empty = new LedgerCommand('empty:key', 'credit', []);
|
||||
self::assertSame('aksrt_ledger_entries', $empty->validate()->get_error_code());
|
||||
|
||||
$invalid = new LedgerCommand('asset:key', 'credit', [[
|
||||
'user_id' => 1,
|
||||
'asset' => 'cash',
|
||||
'delta_minor' => 1,
|
||||
'entry_type' => 'credit',
|
||||
]]);
|
||||
self::assertSame('aksrt_ledger_asset', $invalid->validate()->get_error_code());
|
||||
}
|
||||
|
||||
public function test_validation_requires_integer_non_zero_entry_values(): void {
|
||||
foreach (['1', 0, PHP_INT_MIN] as $delta) {
|
||||
$command = new LedgerCommand('entry:key', 'credit', [[
|
||||
'user_id' => 1,
|
||||
'asset' => 'points',
|
||||
'delta_minor' => $delta,
|
||||
'entry_type' => 'credit',
|
||||
]]);
|
||||
self::assertSame('aksrt_ledger_entry', $command->validate()->get_error_code());
|
||||
}
|
||||
}
|
||||
|
||||
public function test_validation_rejects_negative_actor_and_invalid_entry_type(): void {
|
||||
$actor = new LedgerCommand('actor:key', 'credit', [[
|
||||
'user_id' => 1,
|
||||
'asset' => 'points',
|
||||
'delta_minor' => 1,
|
||||
'entry_type' => 'credit',
|
||||
]], '', '', -1);
|
||||
self::assertSame('aksrt_ledger_actor', $actor->validate()->get_error_code());
|
||||
|
||||
$type = new LedgerCommand('type:key', 'credit', [[
|
||||
'user_id' => 1,
|
||||
'asset' => 'points',
|
||||
'delta_minor' => 1,
|
||||
'entry_type' => 'Bad Type',
|
||||
]]);
|
||||
self::assertSame('aksrt_ledger_entry', $type->validate()->get_error_code());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
use AKSRT\Ledger\AssetType;
|
||||
use AKSRT\Ledger\LedgerResult;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class LedgerResultTest extends TestCase {
|
||||
public function test_balance_reads_integer_minor_units(): void {
|
||||
$result = new LedgerResult(17, true, [], [
|
||||
4 => ['points' => 25, 'balance' => 1234],
|
||||
]);
|
||||
self::assertSame(25, $result->balance(4, AssetType::POINTS));
|
||||
self::assertSame(1234, $result->balance(4, AssetType::BALANCE));
|
||||
self::assertSame(0, $result->balance(99, AssetType::BALANCE));
|
||||
self::assertSame(17, $result->to_array()['transaction_id']);
|
||||
self::assertTrue($result->to_array()['replayed']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use AKSRT\Ledger\OperationKey;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class OperationKeyTest extends TestCase {
|
||||
public function test_request_key_requires_uuid_header(): void {
|
||||
$error = OperationKey::from_request(new WP_REST_Request(), 'transfer', 9);
|
||||
self::assertInstanceOf(WP_Error::class, $error);
|
||||
self::assertSame('aksrt_idempotency_key', $error->get_error_code());
|
||||
self::assertSame(400, $error->get_error_data()['status']);
|
||||
}
|
||||
|
||||
public function test_request_key_is_scoped_to_actor_and_operation(): void {
|
||||
$request = new WP_REST_Request(['Idempotency-Key' => '550E8400-E29B-41D4-A716-446655440000']);
|
||||
self::assertSame(
|
||||
'client-transfer:9:550e8400-e29b-41d4-a716-446655440000',
|
||||
OperationKey::from_request($request, 'transfer', 9)
|
||||
);
|
||||
}
|
||||
|
||||
public function test_deterministic_key_is_stable_and_length_bounded(): void {
|
||||
$key = OperationKey::deterministic('Revenue Settle', [42, str_repeat('A', 300)]);
|
||||
self::assertSame($key, OperationKey::deterministic('Revenue Settle', [42, str_repeat('A', 300)]));
|
||||
self::assertLessThanOrEqual(191, strlen($key));
|
||||
self::assertMatchesRegularExpression('/^[a-z0-9:._-]+$/', $key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
define('ABSPATH', __DIR__ . '/');
|
||||
|
||||
if (!function_exists('__')) {
|
||||
function __(string $text, string $domain = ''): string {
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
if (!function_exists('sanitize_key')) {
|
||||
function sanitize_key(string $key): string {
|
||||
return strtolower((string) preg_replace('/[^a-z0-9_\-]/', '', $key));
|
||||
}
|
||||
}
|
||||
if (!function_exists('wp_json_encode')) {
|
||||
function wp_json_encode($value, int $flags = 0) {
|
||||
return json_encode($value, $flags);
|
||||
}
|
||||
}
|
||||
if (!function_exists('is_wp_error')) {
|
||||
function is_wp_error($value): bool {
|
||||
return $value instanceof WP_Error;
|
||||
}
|
||||
}
|
||||
if (!function_exists('wp_is_uuid')) {
|
||||
function wp_is_uuid($uuid, $version = null): bool {
|
||||
$pattern = '/^[0-9a-f]{8}-[0-9a-f]{4}-([0-9a-f])[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i';
|
||||
if (!is_string($uuid) || !preg_match($pattern, $uuid, $matches)) {
|
||||
return false;
|
||||
}
|
||||
return $version === null || strtolower($matches[1]) === strtolower((string) $version);
|
||||
}
|
||||
}
|
||||
if (!class_exists('WP_Error')) {
|
||||
class WP_Error {
|
||||
public function __construct(private string $code, private string $message = '', private $data = null) {}
|
||||
public function get_error_code(): string { return $this->code; }
|
||||
public function get_error_message(): string { return $this->message; }
|
||||
public function get_error_data() { return $this->data; }
|
||||
}
|
||||
}
|
||||
if (!class_exists('WP_REST_Request')) {
|
||||
class WP_REST_Request {
|
||||
/** @param array<string,string> $headers */
|
||||
public function __construct(private array $headers = []) {}
|
||||
public function get_header(string $name): string {
|
||||
foreach ($this->headers as $key => $value) {
|
||||
if (strcasecmp($key, $name) === 0) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
require_once dirname(__DIR__, 2) . '/src/Ledger/AssetType.php';
|
||||
require_once dirname(__DIR__, 2) . '/src/Ledger/AssetAmount.php';
|
||||
require_once dirname(__DIR__, 2) . '/src/Ledger/LedgerCommand.php';
|
||||
require_once dirname(__DIR__, 2) . '/src/Ledger/LedgerResult.php';
|
||||
require_once dirname(__DIR__, 2) . '/src/Ledger/OperationKey.php';
|
||||
@@ -0,0 +1,170 @@
|
||||
const test = require('node:test')
|
||||
const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
|
||||
const root = path.resolve(__dirname, '..', '..')
|
||||
|
||||
function sourceFiles(dir) {
|
||||
const out = []
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
if (entry.name === 'build' || entry.name === 'vendor' || entry.name === 'node_modules') continue
|
||||
const full = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) out.push(...sourceFiles(full))
|
||||
else if (/\.(php|js|vue|cjs)$/.test(entry.name)) out.push(full)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
test('old asset implementations and compatibility aliases are absent', () => {
|
||||
assert.equal(fs.existsSync(path.join(root, 'src', 'Credits', 'Credits.php')), false)
|
||||
assert.equal(fs.existsSync(path.join(root, 'src', 'Credits', 'Balance.php')), false)
|
||||
assert.equal(fs.existsSync(path.join(root, 'src', 'Payment', 'Rebate.php')), false)
|
||||
assert.equal(fs.existsSync(path.join(root, 'src', 'Admin', 'Native', 'PayRebate.php')), false)
|
||||
assert.equal(fs.existsSync(path.join(root, 'assets', 'admin', 'src', 'pages', 'payment', 'PaymentRebate.vue')), false)
|
||||
assert.equal(fs.existsSync(path.join(root, 'assets', 'admin', 'src', 'pages', 'payment', 'components', 'RebateDetailDrawer.vue')), false)
|
||||
|
||||
const files = [
|
||||
...sourceFiles(path.join(root, 'src')),
|
||||
...sourceFiles(path.join(root, 'inc')),
|
||||
...sourceFiles(path.join(root, 'assets', 'js')),
|
||||
...sourceFiles(path.join(root, 'templates')),
|
||||
]
|
||||
const forbidden = [
|
||||
/aksrt_credits(?:\b|_)/,
|
||||
/['"]aksrt_balance['"]/,
|
||||
/\bcredit_log\b/,
|
||||
/\bbalance_log\b/,
|
||||
/aksrt_credits_(?:added|deducted)/,
|
||||
/aksrt_balance_(?:added|deducted)/,
|
||||
/credits_(?:added|deducted)/,
|
||||
/points_pass_exchange_/,
|
||||
/legacy_task_points/,
|
||||
/transfer_refund|recovery_failed/,
|
||||
/\bPayRebate\b/,
|
||||
/\bPaymentRebate\b/,
|
||||
/\bRebateDetailDrawer\b/,
|
||||
/\brebate_log\b/,
|
||||
/\/payment\/rebate(?:\b|\/)/,
|
||||
/\bpay_rebate\b/,
|
||||
/aksrtpay_rebate_page/,
|
||||
]
|
||||
const violations = []
|
||||
for (const file of files) {
|
||||
const text = fs.readFileSync(file, 'utf8')
|
||||
for (const pattern of forbidden) {
|
||||
if (pattern.test(text)) violations.push(`${path.relative(root, file)}: ${pattern}`)
|
||||
}
|
||||
}
|
||||
assert.deepEqual(violations, [])
|
||||
})
|
||||
|
||||
test('asset tables are InnoDB and operation keys are unique', () => {
|
||||
const schema = fs.readFileSync(path.join(root, 'src', 'Core', 'DBMigratorPart2.php'), 'utf8')
|
||||
for (const table of ['asset_account', 'asset_transaction', 'asset_entry']) {
|
||||
assert.match(schema, new RegExp(`CREATE TABLE \\{\\$prefix\\}aksrt_${table} \\([\\s\\S]*?\\) ENGINE=InnoDB`))
|
||||
}
|
||||
assert.match(schema, /UNIQUE KEY uniq_operation_key \(operation_key\)/)
|
||||
assert.match(schema, /UNIQUE KEY uniq_transaction_line \(transaction_id, line_no\)/)
|
||||
})
|
||||
|
||||
test('user asset submissions send idempotency keys', () => {
|
||||
const request = fs.readFileSync(path.join(root, 'assets', 'js', 'core', 'request.js'), 'utf8')
|
||||
assert.match(request, /createIdempotencyKey/)
|
||||
for (const file of ['credits.js', 'reward.js', 'vip-purchase-coordinator.js', 'cms-download.js']) {
|
||||
const source = fs.readFileSync(path.join(root, 'assets', 'js', file), 'utf8')
|
||||
assert.match(source, /Idempotency-Key/, `${file} must send Idempotency-Key`)
|
||||
}
|
||||
})
|
||||
|
||||
test('core asset paths never use floating-point money helpers', () => {
|
||||
const files = [
|
||||
'src/Ledger/AssetAmount.php',
|
||||
'src/Ledger/LedgerCommand.php',
|
||||
'src/Ledger/LedgerService.php',
|
||||
'src/Credits/BalanceService.php',
|
||||
'src/Credits/BalanceServiceCharge.php',
|
||||
'src/Credits/BalanceServiceRest.php',
|
||||
'src/Payment/BalanceGateway.php',
|
||||
'src/Payment/ChargeCard.php',
|
||||
'src/Payment/CommissionLedger.php',
|
||||
'src/Payment/Revenue.php',
|
||||
'src/Payment/Withdrawal.php',
|
||||
'src/Admin/UserController.php',
|
||||
'src/Content/PayRest.php',
|
||||
'src/Content/VipServicePurchase.php',
|
||||
]
|
||||
const forbidden = [/\(float\)/, /floatval\s*\(/, /round\s*\(/, /number_format\s*\(/]
|
||||
const violations = []
|
||||
for (const relative of files) {
|
||||
const text = fs.readFileSync(path.join(root, relative), 'utf8')
|
||||
for (const pattern of forbidden) {
|
||||
if (pattern.test(text)) violations.push(`${relative}: ${pattern}`)
|
||||
}
|
||||
}
|
||||
assert.deepEqual(violations, [])
|
||||
})
|
||||
|
||||
test('content preview is read-only while purchase requires an operation key', () => {
|
||||
const source = fs.readFileSync(path.join(root, 'src', 'Content', 'PayRest.php'), 'utf8')
|
||||
const previewStart = source.indexOf('public function rest_preview')
|
||||
const purchaseStart = source.indexOf('public function rest_purchase')
|
||||
const purchaseEnd = source.indexOf('public function rest_order_status', purchaseStart)
|
||||
assert.ok(previewStart >= 0 && purchaseStart > previewStart && purchaseEnd > purchaseStart)
|
||||
assert.doesNotMatch(source.slice(previewStart, purchaseStart), /OperationKey::from_request/)
|
||||
assert.match(source.slice(purchaseStart, purchaseEnd), /OperationKey::from_request\(\$request, 'content-purchase'/)
|
||||
})
|
||||
|
||||
test('native financial actions and REST adjustments carry UUID operation keys', () => {
|
||||
const income = fs.readFileSync(path.join(root, 'src', 'Admin', 'Native', 'PayIncome.php'), 'utf8')
|
||||
const withdrawals = fs.readFileSync(path.join(root, 'src', 'Admin', 'Native', 'PayWithdrawals.php'), 'utf8')
|
||||
const points = fs.readFileSync(path.join(root, 'src', 'User', 'CreditsAdmin.php'), 'utf8')
|
||||
const balance = fs.readFileSync(path.join(root, 'src', 'User', 'BalanceAdmin.php'), 'utf8')
|
||||
const restUsers = fs.readFileSync(path.join(root, 'src', 'Admin', 'UserController.php'), 'utf8')
|
||||
for (const source of [income, withdrawals, points, balance]) {
|
||||
assert.match(source, /wp_generate_uuid4\(\)/)
|
||||
assert.match(source, /wp_is_uuid\(/)
|
||||
}
|
||||
assert.match(points, /PHP_INT_MAX/)
|
||||
assert.match(points, /preg_match\('\/\^-\?/)
|
||||
assert.match(points, /strcmp\(\$unsigned_delta, \$max_integer\)/)
|
||||
assert.match(points, /current_user_can\('edit_user', \$user_id\)/)
|
||||
assert.match(balance, /current_user_can\('edit_user', \$user_id\)/)
|
||||
assert.match(restUsers, /OperationKey::from_request\(\$request, 'admin-adjustment'/)
|
||||
assert.match(restUsers, /current_user_can\('edit_user', \$user_id\)/)
|
||||
})
|
||||
|
||||
test('charge cards have no legacy input or metadata aliases', () => {
|
||||
const source = fs.readFileSync(path.join(root, 'src', 'Payment', 'ChargeCard.php'), 'utf8')
|
||||
assert.doesNotMatch(source, /\$params\[['"]code['"]\]/)
|
||||
assert.doesNotMatch(source, /\$data\[['"]code['"]\]/)
|
||||
assert.doesNotMatch(source, /\$raw\[['"]vip_days['"]\]/)
|
||||
assert.doesNotMatch(source, /\$raw\[['"]credits['"]\]/)
|
||||
assert.match(source, /\$params\['password'\]/)
|
||||
assert.match(source, /\$raw\['days'\]/)
|
||||
assert.match(source, /\$raw\['points'\]/)
|
||||
})
|
||||
|
||||
test('VIP card redemption is one locked transaction with bounded retry and replay checks', () => {
|
||||
const source = fs.readFileSync(path.join(root, 'src', 'Payment', 'ChargeCard.php'), 'utf8')
|
||||
const start = source.indexOf('private function redeem_vip_card')
|
||||
const end = source.indexOf('private function vip_card_replay_result', start)
|
||||
const method = source.slice(start, end)
|
||||
assert.ok(start >= 0 && end > start)
|
||||
assert.match(method, /for \(\$attempt = 1; \$attempt <= 3; \$attempt\+\+\)/)
|
||||
assert.match(method, /START TRANSACTION/)
|
||||
assert.match(method, /FOR UPDATE/)
|
||||
assert.match(method, /request_key = %s FOR UPDATE/)
|
||||
assert.match(method, /COMMIT/)
|
||||
assert.match(method, /ROLLBACK/)
|
||||
assert.match(method, /\[1205, 1213\]/)
|
||||
assert.match(method, /vip_card_replay_result/)
|
||||
})
|
||||
|
||||
test('admin ledger keeps cash minor values as decimal strings', () => {
|
||||
const controller = fs.readFileSync(path.join(root, 'src', 'Admin', 'UserController.php'), 'utf8')
|
||||
const page = fs.readFileSync(path.join(root, 'assets', 'admin', 'src', 'pages', 'CmsCredits.vue'), 'utf8')
|
||||
assert.match(controller, /asset_minor_for_response/)
|
||||
assert.match(controller, /decimal_from_minor/)
|
||||
assert.doesNotMatch(page, /\/\s*100(?:\D|$)/)
|
||||
})
|
||||
Reference in new issue
Block a user