| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216 |
- <?php
- declare(strict_types=1);
- /** Water commission accrual regression. Every fixture is rolled back. */
- require dirname(__DIR__) . '/vendor/autoload.php';
- require __DIR__ . '/support/MiniApiContract.php';
- \Dotenv\Dotenv::createUnsafeMutable(dirname(__DIR__))->load();
- \support\App::loadAllConfig(['route']);
- date_default_timezone_set('Asia/Shanghai');
- use plugin\saiadmin\app\model\water\WaterAccount;
- use plugin\saiadmin\app\model\water\WaterAgent;
- use plugin\saiadmin\app\model\water\WaterCommission;
- use plugin\saiadmin\app\model\water\WaterEnterprise;
- use plugin\saiadmin\app\model\water\WaterOrder;
- use plugin\saiadmin\app\service\WaterAgentDashboard;
- use plugin\saiadmin\app\service\WaterAgentManagement;
- use plugin\saiadmin\app\service\WaterBusiness;
- use plugin\saiadmin\app\service\WaterFinanceDashboard;
- use plugin\saiadmin\app\service\agent\WaterCommissionPolicy;
- use plugin\saiadmin\exception\ApiException;
- use app\miniapi\auth\Principal;
- use app\miniapi\model\MiniGrant;
- use app\miniapi\model\MiniUser;
- use app\miniapi\service\Access;
- use app\miniapi\service\AgentQuery;
- $checks = 0;
- function check(bool $ok, string $label): void
- {
- global $checks;
- $checks++;
- if (!$ok) {
- throw new RuntimeException($label);
- }
- }
- $business = new WaterBusiness();
- $agents = new WaterAgentManagement();
- $tag = 'COM-' . bin2hex(random_bytes(4));
- $db = (new WaterCommission())->getConnection();
- $db->beginTransaction();
- try {
- $originalPolicy = WaterCommissionPolicy::current();
- $defaultPolicy = WaterCommissionPolicy::save([
- 'level1_basis_points' => 1000,
- 'level2_basis_points' => 500,
- 'level3_basis_points' => 200,
- 'revision' => $originalPolicy['revision'],
- ], 1);
- $ids = [];
- foreach (['root', 'child', 'leaf', 'unset', 'other'] as $index => $name) {
- $parent = match ($name) {
- 'root', 'other' => 0,
- 'child' => $ids['root'],
- 'leaf' => $ids['child'],
- default => null,
- };
- $ids[$name] = $agents->save([
- 'name' => $tag . '-' . $name,
- 'contact' => 'Test',
- 'phone' => 'COM' . $index . substr($tag, 4),
- 'region' => 'Test region',
- 'parent_id' => $parent,
- 'status' => 1,
- ], 1)['id'];
- }
- $sku = $business->save('sku', ['name' => $tag, 'liters' => 18.9], 1)['id'];
- $package = $business->save('package', [
- 'name' => $tag . '-package',
- 'sku_id' => $sku,
- 'buckets' => 5,
- 'price_cents' => 12345,
- ], 1)['id'];
- $enterpriseIds = [];
- foreach (['root', 'child', 'leaf', 'unset'] as $name) {
- $enterpriseIds[$name] = $business->save('enterprise', [
- 'name' => $tag . '-' . $name . '-company',
- 'contact' => 'Test',
- 'phone' => 'COM' . $name,
- 'address' => 'Test address',
- 'agent_id' => $ids[$name],
- ], 1)['id'];
- }
- foreach (['root' => [1, 1000, 1235], 'child' => [2, 500, 617], 'leaf' => [3, 200, 247]] as $name => [$level, $rate, $amount]) {
- $reference = $tag . '-' . $name;
- $order = $business->purchase(['enterprise_id' => $enterpriseIds[$name], 'package_id' => $package, 'reference' => $reference], 1);
- $commission = WaterCommission::query()->where('order_id', $order['id'])->sole();
- check((int) $commission->agent_id === (int) $ids[$name], $name . ' direct agent only');
- check((int) $commission->agent_level === $level && (int) $commission->rate_basis_points === $rate, $name . ' rate snapshot');
- check((int) $commission->base_amount_cents === 12345 && (int) $commission->amount_cents === $amount, $name . ' rounded amount');
- check($commission->status === 'pending' && $commission->settled_at === null, $name . ' not withdrawable');
- check(WaterCommission::query()->where('order_id', $order['id'])->count() === 1, $name . ' has no ancestor commission');
- $replay = $business->purchase(['enterprise_id' => $enterpriseIds[$name], 'package_id' => $package, 'reference' => $reference], 1);
- check(($replay['replayed'] ?? false) === true && WaterCommission::query()->where('order_id', $order['id'])->count() === 1, $name . ' idempotent');
- }
- WaterEnterprise::query()->whereKey($enterpriseIds['leaf'])->update(['agent_id' => $ids['other']]);
- $original = WaterCommission::query()->where('enterprise_id', $enterpriseIds['leaf'])->first();
- check((int) $original->agent_id === (int) $ids['leaf'] && (int) $original->agent_level === 3, 'old commission keeps attribution');
- $newOrder = $business->purchase(['enterprise_id' => $enterpriseIds['leaf'], 'package_id' => $package, 'reference' => $tag . '-transfer'], 1);
- $newCommission = WaterCommission::query()->where('order_id', $newOrder['id'])->sole();
- check((int) $newCommission->agent_id === (int) $ids['other'] && (int) $newCommission->agent_level === 1, 'new order uses new owner');
- $noLevel = $business->purchase(['enterprise_id' => $enterpriseIds['unset'], 'package_id' => $package, 'reference' => $tag . '-unset'], 1);
- check(WaterCommission::query()->where('order_id', $noLevel['id'])->count() === 0, 'unknown level does not accrue');
- $demo = $business->purchase(['enterprise_id' => $enterpriseIds['root'], 'package_id' => $package, 'reference' => 'TEST-' . $tag], 1);
- check(WaterCommission::query()->where('order_id', $demo['id'])->count() === 0, 'demo receipt does not accrue');
- WaterAgent::query()->whereKey($ids['root'])->update(['status' => 2]);
- $disabled = $business->purchase(['enterprise_id' => $enterpriseIds['root'], 'package_id' => $package, 'reference' => $tag . '-disabled'], 1);
- check(WaterCommission::query()->where('order_id', $disabled['id'])->count() === 0, 'disabled direct agent does not accrue');
- WaterAgent::query()->whereKey($ids['root'])->update(['status' => 1]);
- $accountBefore = (int) WaterAccount::query()->where('enterprise_id', $enterpriseIds['root'])->where('sku_id', $sku)->value('entitlement');
- WaterAgent::query()->whereKey($ids['root'])->update(['parent_id' => 999999999]);
- try {
- $business->purchase(['enterprise_id' => $enterpriseIds['root'], 'package_id' => $package, 'reference' => $tag . '-broken'], 1);
- throw new RuntimeException('broken hierarchy should fail');
- } catch (DomainException $exception) {
- check($exception instanceof DomainException, 'broken hierarchy rejected');
- }
- check(WaterOrder::query()->where('reference', $tag . '-broken')->count() === 0, 'failed accrual rolls back order');
- check((int) WaterAccount::query()->where('enterprise_id', $enterpriseIds['root'])->where('sku_id', $sku)->value('entitlement') === $accountBefore, 'failed accrual rolls back entitlement');
- WaterAgent::query()->whereKey($ids['root'])->update(['parent_id' => 0]);
- check(WaterCommissionPolicy::amount(1, 1) === 0 && WaterCommissionPolicy::amount(5, 1) === 1, 'half cent rounds up');
- check(WaterCommissionPolicy::amount(12345, 3) === 247, 'third level integer rounding');
- $finance = (new WaterFinanceDashboard())->dashboard([]);
- check($finance['capabilities']['commissions'] === true && $finance['capabilities']['withdrawals'] === false, 'finance shows accrual without withdrawal');
- check($finance['summary']['pending_commission_cents'] >= 3334, 'finance includes pending accrual');
- $dashboard = (new WaterAgentDashboard())->detail($ids['leaf']);
- check($dashboard['agent']['commission_cents'] === 247, 'admin agent shows own commission');
- $now = date('Y-m-d H:i:s');
- $userId = MiniUser::query()->insertGetId([
- 'appid' => 'commission-fixture-' . $tag,
- 'openid' => 'commission-fixture-' . $tag,
- 'nickname' => 'Commission test',
- 'status' => 1,
- 'created_at' => $now,
- 'updated_at' => $now,
- ]);
- MiniGrant::query()->create([
- 'user_id' => $userId,
- 'role' => 'agent',
- 'resource_id' => $ids['leaf'],
- 'status' => 1,
- 'created_by' => 1,
- 'created_at' => $now,
- 'updated_at' => $now,
- ]);
- $query = new AgentQuery(new Access(new Principal((string) $userId)));
- $miniCommissions = $query->commissions($ids['leaf']);
- check($miniCommissions['pending_cents'] === 247 && $miniCommissions['withdrawable_cents'] === 0, 'mini API shows pending but not withdrawable');
- check($miniCommissions['total'] === 1 && $miniCommissions['items'][0]['status'] === 'pending', 'mini API lists own direct commission');
- check((new MiniApiContract())->data('Commissions', $miniCommissions), 'mini API matches OpenAPI contract');
- check($query->commissions($ids['leaf'], ['source' => 'demo'])['items'] === [], 'missing demo fixture never falls through to real commission');
- $updatedPolicy = WaterCommissionPolicy::save([
- 'level1_basis_points' => 800,
- 'level2_basis_points' => 400,
- 'level3_basis_points' => 100,
- 'revision' => $defaultPolicy['revision'],
- ], 1);
- check($updatedPolicy['revision'] === $defaultPolicy['revision'] + 1 && WaterCommissionPolicy::rate(1) === 800, 'edited rates are persisted');
- check(str_contains(WaterCommissionPolicy::message(), '8%') && str_contains(WaterCommissionPolicy::message(), '4%'), 'commission explanation follows saved rates');
- $afterChange = $business->purchase(['enterprise_id' => $enterpriseIds['child'], 'package_id' => $package, 'reference' => $tag . '-new-rate'], 1);
- $changedCommission = WaterCommission::query()->where('order_id', $afterChange['id'])->sole();
- check((int) $changedCommission->rate_basis_points === 400 && (int) $changedCommission->amount_cents === 494, 'new order uses edited second-level rate');
- check((int) WaterCommission::query()->where('enterprise_id', $enterpriseIds['child'])->orderBy('id')->first()->rate_basis_points === 500, 'earlier order retains original rate');
- $validInput = [
- 'level1_basis_points' => 800,
- 'level2_basis_points' => 400,
- 'level3_basis_points' => 100,
- 'revision' => $updatedPolicy['revision'],
- ];
- foreach ([
- ['level1_basis_points' => 10001, 'revision' => $updatedPolicy['revision']],
- ['level1_basis_points' => 1000, 'revision' => $defaultPolicy['revision']],
- ['level1_basis_points' => 1000, 'revision' => $updatedPolicy['revision'], 'unexpected' => true],
- ] as $invalid) {
- try {
- WaterCommissionPolicy::save(array_replace($validInput, $invalid), 1);
- throw new RuntimeException('invalid policy should fail');
- } catch (ApiException $exception) {
- check(true, 'invalid or stale policy rejected');
- }
- }
- } finally {
- $db->rollBack();
- }
- check(!WaterCommission::query()->where('enterprise_name', 'like', $tag . '%')->exists(), 'commission fixtures rolled back');
- if (in_array('--http', $argv, true)) {
- $token = \Tinywan\Jwt\JwtToken::generateToken([
- 'id' => 1, 'username' => 'admin', 'plat' => 'saiadmin', 'type' => 'web',
- 'client' => $tag, 'access_exp' => 120, 'refresh_exp' => 120,
- ])['access_token'];
- $request = curl_init('http://127.0.0.1:8787/core/water/agents/commission-policy');
- curl_setopt_array($request, [
- CURLOPT_RETURNTRANSFER => true,
- CURLOPT_TIMEOUT => 15,
- CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $token],
- ]);
- $response = curl_exec($request);
- $status = curl_getinfo($request, CURLINFO_HTTP_CODE);
- curl_close($request);
- $body = json_decode((string) $response, true);
- check($status === 200 && ($body['code'] ?? 0) === 200, 'commission policy HTTP route');
- check(($body['data']['level1_basis_points'] ?? null) === WaterCommissionPolicy::rate(1), 'HTTP rates match database');
- }
- echo "PASS: {$checks} commission checks; all fixtures rolled back.\n";
|