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";