| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536 |
- <?php
- /** Real Eloquent transactions, every fixture including uploaded photo bytes 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 app\miniapi\auth\Principal;
- use app\miniapi\auth\SessionVerifier;
- use app\miniapi\model\MiniUser;
- use app\miniapi\model\MiniSession;
- use app\miniapi\model\MiniGrant;
- use app\miniapi\model\MiniCheckout;
- use app\miniapi\model\MiniEnterpriseBalance;
- use app\miniapi\model\MiniCommand;
- use app\miniapi\model\MiniAudit;
- use app\miniapi\model\MiniTaskRecord;
- use app\miniapi\model\MiniPhoto;
- use app\miniapi\service\Access;
- use app\miniapi\service\AuthService;
- use app\miniapi\service\Catalog;
- use app\miniapi\service\EnterpriseQuery;
- use app\miniapi\service\EnterpriseCommands;
- use app\miniapi\service\CheckoutService;
- use app\miniapi\service\DriverService;
- use app\miniapi\service\AgentQuery;
- use app\miniapi\service\IdentityManagement;
- use app\miniapi\exception\ApiException;
- use app\miniapi\controller\CheckoutController;
- use app\miniapi\controller\PhotoController;
- use plugin\saiadmin\app\model\water\WaterEnterprise;
- use plugin\saiadmin\app\model\water\WaterSku;
- use plugin\saiadmin\app\model\water\WaterAccount;
- use plugin\saiadmin\app\model\water\WaterOrder;
- use plugin\saiadmin\app\model\water\WaterDelivery;
- use plugin\saiadmin\app\model\water\WaterContent;
- use plugin\saiadmin\app\service\WaterBusiness;
- use plugin\saiadmin\app\service\WaterDispatch;
- use support\Request;
- use app\miniapi\support\PhoneNumber;
- use app\miniapi\http\Fields;
- $checks = 0;
- $contract = new MiniApiContract();
- function check(bool $ok, string $label): void
- {
- global $checks;
- $checks++;
- if (!$ok) {
- throw new RuntimeException($label);
- }
- }
- function reject(callable $fn, int $code, string $label): void
- {
- try {
- $fn();
- } catch (ApiException $e) {
- check($e->getCode() === $code, $label . ' actual ' . $e->getCode());
- return;
- }throw new RuntimeException('Missing rejection: ' . $label);
- }
- function inputRequest(string $path, array $data, Principal $actor): Request
- {
- $body = json_encode($data);
- $r = new Request("POST $path HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: " . strlen($body) . "\r\n\r\n" . $body);
- $r->miniPrincipal = $actor;
- return $r;
- }
- foreach ([
- '138-1234-5678' => ['13812345678', '138****5678'],
- '+86 138 1234 5678' => ['+8613812345678', '+861******5678'],
- '(010) 1234-5678' => ['01012345678', '010****5678'],
- '1234567' => ['1234567', '12***67'],
- ] as $input => [$normalized, $masked]) {
- check(PhoneNumber::normalize((string) $input) === $normalized, 'phone normalization');
- check(PhoneNumber::mask((string) $input) === $masked, 'formatted phone masked');
- }
- foreach (['------', '++++++', '()()()', '(01012345678', '138****5678', '123456', '+0123456789', '0000000', '1234567890123456'] as $phone) {
- reject(fn () => Fields::phone(['phone' => $phone]), 42201, 'invalid phone rejected');
- check(PhoneNumber::mask($phone) === '****', 'invalid legacy phone never exposed');
- }
- check(PhoneNumber::mask('') === '', 'missing phone stays empty');
- $models = [
- MiniUser::class,
- MiniSession::class,
- MiniGrant::class,
- MiniCheckout::class,
- MiniEnterpriseBalance::class,
- MiniCommand::class,
- MiniAudit::class,
- MiniTaskRecord::class,
- MiniPhoto::class,
- WaterEnterprise::class,
- WaterSku::class,
- WaterAccount::class,
- WaterOrder::class,
- WaterDelivery::class,
- ];
- $before = [];
- foreach ($models as $m) {
- $before[$m] = $m::query()->count();
- }
- $db = (new MiniUser())->getConnection();
- $db->beginTransaction();
- $tag = 'MINI-TEST-' . bin2hex(random_bytes(6));
- try {
- $business = new WaterBusiness();
- $auth = new AuthService();
- $manager = new IdentityManagement();
- $session = $auth->issue([
- 'appid' => 'test-' . $tag,
- 'openid' => 'u1',
- ]);
- $actor = (new SessionVerifier())->verify($session['access_token']);
- $other = $auth->issue([
- 'appid' => 'test-' . $tag,
- 'openid' => 'u2',
- ]);
- $otherActor = (new SessionVerifier())->verify($other['access_token']);
- check($session['identities'] === [], 'new login has no inherited authority');
- $provider = new class ('test-' . $tag) implements \app\miniapi\auth\IdentityProvider {
- public function __construct(private readonly string $appid)
- {
- }
- public function exchange(string $code): array
- {
- if ($code !== 'verified-test-code') {
- throw new ApiException(40102, 'invalid code', 401);
- }
- return ['appid' => $this->appid, 'openid' => 'u1'];
- }
- };
- $loginService = new AuthService($provider);
- check($loginService->login(['code' => 'verified-test-code'])['user_id'] === $session['user_id'], 'login service depends on identity adapter contract');
- reject(fn () => $loginService->login(['code' => 'verified-test-code', 'user_id' => 'forged']), 42201, 'client cannot supply provider identity');
- check(MiniSession::query()->where('user_id', $actor->userId)->value('token_hash') !== $session['access_token'], 'token hash only');
- reject(fn () => (new SessionVerifier())->verify('admin.jwt.token'), 40101, 'admin JWT not accepted');
- $enterprise = $business->save('enterprise', [
- 'name' => $tag,
- 'contact' => '?????????',
- 'phone' => '13812345678',
- 'address' => '????????????',
- 'region' => '?????????',
- 'employee_count' => 30,
- ], 1)['id'];
- $enterprise2 = $business->save('enterprise', [
- 'name' => $tag . '-other',
- 'contact' => '???',
- 'phone' => '13912345678',
- 'address' => '????????????',
- ], 1)['id'];
- $sku = $business->save('sku', [
- 'name' => $tag,
- 'liters' => 18.9,
- ], 1)['id'];
- $package = $business->save('package', [
- 'name' => $tag,
- 'sku_id' => $sku,
- 'buckets' => 30,
- 'price_cents' => 69900,
- ], 1)['id'];
- $business->purchase([
- 'enterprise_id' => $enterprise,
- 'package_id' => $package,
- 'reference' => $tag . '-receipt',
- ], 1);
- $business->stock([
- 'sku_id' => $sku,
- 'mode' => 'inbound',
- 'quantity' => 60,
- 'reference' => $tag . '-stock',
- 'note' => 'test',
- ], 1);
- $manager->grant(1, [
- 'user_id' => (int) $actor->userId,
- 'role' => 'enterprise_admin',
- 'resource_id' => $enterprise,
- ]);
- $manager->grant(1, [
- 'user_id' => (int) $otherActor->userId,
- 'role' => 'enterprise_member',
- 'resource_id' => $enterprise,
- ]);
- reject(fn () => $manager->grant(2, [
- 'user_id' => (int) $actor->userId,
- 'role' => 'enterprise_admin',
- 'resource_id' => $enterprise2,
- ]), 40301, 'only admin can bind identity');
- $access = new Access($actor);
- $query = new EnterpriseQuery($access);
- $commands = new EnterpriseCommands($access);
- $checkout = new CheckoutService($access);
- check(count($access->identities()) === 1, 'identity returned after explicit grant');
- reject(fn () => $access->enterprise($enterprise2), 40301, 'cross enterprise read forbidden');
- reject(fn () => (new Access($otherActor))->enterprise($enterprise, true), 40301, 'member cannot write');
- check($query->addresses()['items'][0]['phone'] === '138****5678', 'phone masking');
- check(count($query->addresses()['items']) === 1, 'addresses scoped');
- $rights = $query->rights($enterprise);
- check($rights['summary']['available'] === 30, 'real purchased rights');
- check($rights['items'][0]['expires_at'] === null && !$rights['capabilities']['individual_package_cards'], 'no fictitious expiry or separate cards');
- $catalog = (new Catalog())->detail($package);
- check($catalog['price_cents'] === 69900, 'catalog integer cents');
- WaterSku::query()->where('id', $sku)->update(['status' => 2]);
- reject(fn () => (new Catalog())->detail($package), 40401, 'disabled SKU hides package');
- WaterSku::query()->where('id', $sku)->update(['status' => 1]);
- $item = ['items' => [[
- 'package_id' => $package,
- 'quantity' => 2,
- ]]];
- $quote = $checkout->quote($enterprise, $item);
- check($quote['amount_cents'] === 139800 && $quote['discount_cents'] === 0, 'server quote does not copy mock discount');
- reject(fn () => $checkout->quote($enterprise, ['items' => [[
- 'package_id' => $package,
- 'quantity' => 1,
- ], [
- 'package_id' => $package,
- 'quantity' => 2,
- ]]]), 42201, 'duplicate package rejected');
- reject(fn () => $checkout->quote($enterprise, ['items' => [[
- 'package_id' => $package,
- 'quantity' => 1,
- 'price_cents' => 1,
- ]]]), 42201, 'client price rejected');
- $create = $item + [
- 'request_key' => $tag . '-checkout',
- 'expected_amount_cents' => 139800,
- 'agreement_accepted' => true,
- ];
- reject(fn () => $checkout->create($enterprise, array_replace($create, ['expected_amount_cents' => 1])), 40904, 'changed quote rejected');
- $order = $checkout->create($enterprise, $create);
- check($order['status'] === 'pending', 'checkout stored pending');
- check($checkout->create($enterprise, $create)['id'] === $order['id'], 'checkout retry reuses order');
- reject(fn () => $checkout->create($enterprise, array_replace($create, ['expected_amount_cents' => 1])), 40901, 'different replay rejected');
- check($query->rights($enterprise)['summary']['available'] === 30, 'pending order never credits rights');
- check(WaterOrder::query()->where('enterprise_id', $enterprise)->count() === 1, 'pending order never appears in receipts');
- $controller = new CheckoutController();
- $quoteResponse = $controller->quote(inputRequest('/api/mini/v1/test', $item, $actor), (string) $enterprise);
- check(json_decode($quoteResponse->rawBody(), true)['data']['amount_cents'] === 139800, 'nested JSON items work through HTTP controller');
- MiniEnterpriseBalance::query()->create([
- 'enterprise_id' => $enterprise,
- 'balance_cents' => 200000,
- 'created_at' => date('Y-m-d H:i:s'),
- 'updated_at' => date('Y-m-d H:i:s'),
- ]);
- $payKey = $tag . '-pay';
- $payResponse = $controller->pay(inputRequest('/api/mini/v1/test', [
- 'request_key' => $payKey,
- 'payment_method' => 'balance',
- ], $actor), (string) $enterprise, (string) $order['id']);
- $payBody = json_decode($payResponse->rawBody(), true);
- check($payBody['data']['status'] === 'paid', 'balance payment marks checkout paid');
- check((int) MiniEnterpriseBalance::query()->where('enterprise_id', $enterprise)->value('balance_cents') === 60200, 'balance payment deducts cents');
- check($query->rights($enterprise)['summary']['available'] === 90, 'balance payment grants purchased rights');
- check(WaterOrder::query()->where('enterprise_id', $enterprise)->count() === 3, 'balance payment creates receipts');
- check($checkout->pay($enterprise, $order['id'], [
- 'request_key' => $payKey,
- 'payment_method' => 'balance',
- ])['replayed'] === true, 'balance payment retry replays safely');
- reject(fn () => $controller->pay(inputRequest('/api/mini/v1/test', [
- 'request_key' => $tag . '-wechat',
- 'payment_method' => 'wechat',
- ], $actor), (string) $enterprise, (string) $order['id']), 50303, 'wechat payment remains reserved');
- check($checkout->detail($enterprise, $order['id'])['address']['phone'] === '138****5678', 'checkout address masked');
- check($query->orders($enterprise, ['kind' => 'checkouts'])['total'] === 1, 'checkout list real');
- $cancelOrder = $checkout->create($enterprise, array_replace($create, ['request_key' => $tag . '-cancel-create']));
- check($checkout->cancel($enterprise, $cancelOrder['id'], ['request_key' => $tag . '-cancel-order'])['status'] === 'cancelled', 'checkout cancellation');
- $address = $query->addresses()['items'][0];
- check($query->addressDetail($enterprise)['phone'] === '13812345678', 'enterprise admin can edit actual contact');
- reject(fn () => (new EnterpriseQuery(new Access($otherActor)))->addressDetail($enterprise), 40301, 'member cannot read editable contact');
- $changed = $commands->address($enterprise, [
- 'request_key' => $tag . '-address',
- 'revision' => $address['revision'],
- 'address' => '???????????????',
- 'contact' => '???',
- 'phone' => '138-1234-5678',
- ]);
- check(WaterEnterprise::query()->find($enterprise)->phone === '13812345678', 'address stores normalized phone');
- check(WaterEnterprise::query()->find($enterprise)->address === '???????????????', 'address update works');
- reject(fn () => $commands->address($enterprise, [
- 'request_key' => $tag . '-stale-address',
- 'revision' => $address['revision'],
- 'address' => '???????????????',
- 'contact' => '???',
- 'phone' => '13812345678',
- ]), 40901, 'stale address revision rejected even within same second');
- $booking = [
- 'sku_id' => $sku,
- 'quantity' => 8,
- 'request_key' => $tag . '-book',
- ];
- $task = $commands->book($enterprise, $booking)['id'];
- check($commands->book($enterprise, $booking)['id'] === $task, 'booking retry no duplicate');
- check($query->rights($enterprise)['summary']['frozen'] === 8, 'booking freezes existing rights');
- reject(fn () => $commands->book($enterprise, array_replace($booking, ['request_key' => $tag . '-book-other'])), 40902, 'second active booking rejected');
- reject(fn () => $commands->address($enterprise, [
- 'request_key' => $tag . '-address-active',
- 'revision' => $changed['revision'],
- 'address' => '??????',
- 'contact' => '???',
- 'phone' => '13812345678',
- ]), 40902, 'active task prevents moving address');
- check($commands->cancel($enterprise, $task, ['request_key' => $tag . '-cancel-task'])['status'] === 'cancelled', 'cancel pending task');
- check($query->rights($enterprise)['summary']['frozen'] === 0, 'cancel releases rights');
- $task = $commands->book($enterprise, array_replace($booking, ['request_key' => $tag . '-book-2']))['id'];
- $dispatch = new WaterDispatch();
- $driver = $dispatch->saveDriver([
- 'name' => $tag,
- 'phone' => $tag,
- 'region' => '?????????',
- 'vehicle' => 'test',
- 'capacity' => 100,
- ], 1)['id'];
- $dispatch->shift([
- 'driver_id' => $driver,
- 'date' => date('Y-m-d'),
- 'state' => 'on',
- ], 1);
- $dispatch->commit([
- 'date' => date('Y-m-d'),
- 'request_key' => $tag . '-route',
- 'routes' => [[
- 'driver_id' => $driver,
- 'task_ids' => [$task],
- ]],
- ], 1);
- $manager->grant(1, [
- 'user_id' => (int) $actor->userId,
- 'role' => 'driver',
- 'resource_id' => $driver,
- ]);
- $driverService = new DriverService($access);
- check($driverService->tasks($driver, [])['total'] === 1, 'driver sees only assigned task');
- check($driverService->detail($driver, $task)['contact_phone'] === '13812345678', 'assigned driver can call customer');
- reject(fn () => (new DriverService(new Access($otherActor)))->detail($driver, $task), 40301, 'unbound driver forbidden');
- reject(fn () => $commands->cancel($enterprise, $task, ['request_key' => $tag . '-cancel-assigned']), 40902, 'customer cannot cancel assigned task');
- check($driverService->command($driver, $task, 'start', ['request_key' => $tag . '-depart'])['status'] === 'transit', 'driver departure uses stock transaction');
- check($driverService->command($driver, $task, 'start', ['request_key' => $tag . '-depart'])['replayed'], 'departure replay');
- $driverService->command($driver, $task, 'checkin', [
- 'request_key' => $tag . '-checkin',
- 'latitude' => 22.52,
- 'longitude' => 113.92,
- ]);
- reject(fn () => $driverService->command($driver, $task, 'checkin', [
- 'request_key' => $tag . '-bad-location',
- 'latitude' => 100,
- 'longitude' => 113.92,
- ]), 42201, 'latitude range checked');
- $driverService->command($driver, $task, 'exception', [
- 'request_key' => $tag . '-exception',
- 'reason' => '??????????????????',
- ]);
- check(WaterDelivery::query()->find($task)->status === 'transit', 'exception report does not invent completion');
- $finish = [
- 'request_key' => $tag . '-complete',
- 'accepted_qty' => 8,
- 'empty_returned' => 0,
- 'receiver' => '???',
- 'confirmed' => true,
- 'photo_ids' => [],
- ];
- reject(fn () => $driverService->command($driver, $task, 'complete', $finish), 42201, 'completion requires photo');
- $img = imagecreatetruecolor(640, 100);
- ob_start();
- imagepng($img);
- $base64 = base64_encode(ob_get_clean());
- imagedestroy($img);
- $photoInput = [
- 'request_key' => $tag . '-photo',
- 'image_base64' => $base64,
- 'latitude' => 22.52,
- 'longitude' => 113.92,
- ];
- $photo = $driverService->photo($driver, $task, $photoInput);
- check($driverService->photo($driver, $task, $photoInput)['id'] === $photo['id'], 'photo upload idempotent');
- check(MiniPhoto::query()->find($photo['id'])->mime === 'image/jpeg', 'photos reencoded and watermarked');
- reject(fn () => $driverService->photo($driver, $task, array_replace($photoInput, [
- 'request_key' => $tag . '-bad-photo',
- 'image_base64' => base64_encode('<?php ?>'),
- ])), 42201, 'non image rejected');
- $draft = array_replace($finish, [
- 'request_key' => $tag . '-draft',
- 'photo_ids' => [$photo['id']],
- ]);
- check($driverService->command($driver, $task, 'draft', $draft)['saved'], 'draft saved without settlement');
- check($query->rights($enterprise)['summary']['remaining'] === 90, 'draft never consumes rights');
- $finish['photo_ids'] = [$photo['id']];
- check($driverService->command($driver, $task, 'complete', $finish)['status'] === 'completed', 'driver completes assigned task');
- check($driverService->command($driver, $task, 'complete', $finish)['replayed'], 'completion retry does not settle twice');
- check($driverService->detail($driver, $task)['contact_phone'] === null, 'completed task no longer exposes call number');
- $rights = $query->rights($enterprise);
- check($rights['summary']['remaining'] === 82 && $rights['summary']['frozen'] === 0, 'exactly eight rights consumed');
- check($rights['summary']['full_qty'] === 8, 'site inventory received');
- check($query->tracking($enterprise, $task)['photos'][0]['id'] == $photo['id'], 'customer sees own fulfillment evidence');
- reject(fn () => $query->tracking($enterprise2, $task), 40301, 'tracking cannot cross enterprise');
- $pr = new Request('GET /api/mini/v1/photos/' . $photo['id'] . '?scope=enterprise&resource_id=' . $enterprise . " HTTP/1.1\r\nHost: localhost\r\n\r\n");
- $pr->miniPrincipal = $actor;
- check((new PhotoController())->photo($pr, (string) $photo['id'])->getHeader('Content-Type') === 'image/jpeg', 'private photo authorized');
- $pr2 = new Request('GET /api/mini/v1/photos/' . $photo['id'] . '?scope=enterprise&resource_id=' . $enterprise2 . " HTTP/1.1\r\nHost: localhost\r\n\r\n");
- $pr2->miniPrincipal = $otherActor;
- reject(fn () => (new PhotoController())->photo($pr2, (string) $photo['id']), 40301, 'photo ownership enforced');
- $device = $business->save('device', [
- 'name' => $tag,
- 'sn' => $tag,
- 'enterprise_id' => $enterprise,
- 'sku_id' => $sku,
- ], 1)['id'];
- check($query->deviceDetail($enterprise, $device)['today_liters'] === null, 'unknown telemetry stays null');
- check(count($query->deviceDetail($enterprise, $device)['trend']) === 7, 'seven day trend');
- check(!$driverService->deviceCheck($driver, $task, $tag)['online'], 'device registration does not mean online');
- $now = date('Y-m-d H:i:s');
- WaterContent::query()->insert([
- 'title' => $tag,
- 'image_url' => 'https://example.test/a.png',
- 'target_url' => 'https://example.test/',
- 'audience' => 'purchased',
- 'region' => '?????????',
- 'sort' => 999999,
- 'status' => 1,
- 'created_at' => $now,
- 'updated_at' => $now,
- ]);
- $home = $query->home($enterprise);
- check(in_array($tag, array_column($home['banners'], 'title'), true), 'home derives actual paid audience');
- check($home['unread_messages'] === null, 'no fake unread message count');
- check($driverService->dashboard($driver)['summary']['completed_today'] === 1, 'driver dashboard completion');
- check(count($driverService->routes($driver)) === 1, 'assigned routes returned');
- $agent = $business->save('agent', [
- 'name' => $tag,
- 'contact' => '??????',
- 'phone' => $tag,
- 'region' => '?????????',
- 'address' => '??????',
- ], 1)['id'];
- WaterEnterprise::query()->where('id', $enterprise)->update(['agent_id' => $agent]);
- $manager->grant(1, [
- 'user_id' => (int) $actor->userId,
- 'role' => 'agent',
- 'resource_id' => $agent,
- ]);
- $aq = new AgentQuery($access);
- check($aq->customers($agent, [])['total'] === 1, 'agent customers scoped');
- check($aq->orders($agent, [])['total'] === 3, 'paid checkout receipts enter agent turnover');
- check($aq->dashboard($agent)['summary']['month_amount_cents'] === 209700, 'agent real receipt turnover');
- check($aq->commissions($agent)['withdrawable_cents'] === 0 && $aq->commissions($agent)['pending_cents'] === 0, 'unsettled commission account starts at zero');
- $customer = $aq->customers($agent, [])['items'][0];
- check($customer['amount_cents'] === 209700 && $customer['has_purchased'] && $customer['device_count'] === 1, 'customer aggregates preserve paid and device totals');
- check($customer['last_purchase_at'] !== null, 'customer last receipt timestamp');
- WaterEnterprise::query()->where('id', $enterprise2)->update(['agent_id' => $agent, 'phone' => '+86 138 1234 5678']);
- $futureReceipt = WaterOrder::query()->where('enterprise_id', $enterprise)->first()->replicate();
- $futureReceipt->fill([
- 'enterprise_id' => $enterprise2,
- 'reference' => $tag . '-future',
- 'created_at' => date('Y-m-d H:i:s', time() + 86400),
- ])->save();
- $db->enableQueryLog();
- try {
- $db->flushQueryLog();
- $aq->customers($agent, ['page_size' => 1]);
- $singleCount = count($db->getQueryLog());
- $db->flushQueryLog();
- $customers = $aq->customers($agent, ['page_size' => 100]);
- check(count($db->getQueryLog()) === $singleCount, 'customer query count does not grow per row');
- check(count($customers['items']) === 2, 'query-count regression covers multiple customers');
- } finally {
- $db->disableQueryLog();
- $db->flushQueryLog();
- }
- $unpaid = $customers['items'][0];
- check($unpaid['id'] === $enterprise2 && $unpaid['amount_cents'] === 0 && !$unpaid['has_purchased'], 'future receipt excluded from customer purchase totals');
- check($unpaid['last_purchase_at'] === null && $unpaid['device_count'] === 0, 'empty aggregates return null timestamp and zero count');
- check($unpaid['phone'] === '+861******5678', 'agent list masks legacy international phone');
- check($aq->customers($agent, ['filter' => 'unpaid'])['total'] === 1, 'unpaid filter agrees with aggregates');
- foreach ([
- 'Login' => $session,
- 'Config' => (new \app\miniapi\service\SystemService())->configuration(),
- 'Health' => (new \app\miniapi\service\SystemService())->status(),
- 'Profile' => $query->profile($enterprise),
- 'Addresses' => $query->addresses(),
- 'EditableAddress' => $query->addressDetail($enterprise),
- 'Home' => $home,
- 'Package' => $catalog,
- 'PackagePage' => (new Catalog())->listing(['keyword' => $tag]),
- 'Rights' => $rights,
- 'LedgerPage' => $query->ledger($enterprise, []),
- 'EnterpriseOrderPage' => $query->orders($enterprise, []),
- 'Tracking' => $query->tracking($enterprise, $task),
- 'DevicePage' => $query->devices($enterprise, []),
- 'DeviceDetail' => $query->deviceDetail($enterprise, $device),
- 'Rewards' => $query->rewards($enterprise),
- 'Quote' => $quote,
- 'Checkout' => $order,
- 'CheckoutDetail' => $checkout->detail($enterprise, $order['id']),
- 'AddressResult' => $changed,
- 'DriverDashboard' => $driverService->dashboard($driver),
- 'TaskPage' => $driverService->tasks($driver, []),
- 'TaskDetail' => $driverService->detail($driver, $task),
- 'Route' => $driverService->routes($driver)[0],
- 'DeviceCheck' => $driverService->deviceCheck($driver, $task, $tag),
- 'PhotoResult' => $photo,
- 'AgentDashboard' => $aq->dashboard($agent),
- 'CustomerPage' => $customers,
- 'AgentOrderPage' => $aq->orders($agent, []),
- 'Commissions' => $aq->commissions($agent),
- ] as $schema => $result) {
- check($contract->data($schema, $result), 'actual response matches OpenAPI: ' . $schema);
- }
- foreach (['purchases', 'checkouts'] as $kind) {
- check($contract->data('EnterpriseOrderPage', $query->orders($enterprise, ['kind' => $kind])), 'order variant matches OpenAPI: ' . $kind);
- }
- reject(fn () => (new AgentQuery(new Access($otherActor)))->dashboard($agent), 40301, 'agent identity enforced');
- $manager->grant(1, [
- 'user_id' => (int) $actor->userId,
- 'role' => 'enterprise_admin',
- 'resource_id' => $enterprise,
- 'status' => 0,
- ]);
- reject(fn () => $query->rights($enterprise), 40301, 'revocation effective immediately');
- reject(fn () => $checkout->create($enterprise, $create), 40301, 'revoked user cannot replay old checkout result');
- $auth->logout($session['access_token']);
- reject(fn () => (new SessionVerifier())->verify($session['access_token']), 40101, 'logout invalidates token');
- MiniSession::query()->where('user_id', $otherActor->userId)->update(['expires_at' => date('Y-m-d H:i:s', time() - 1)]);
- reject(fn () => (new SessionVerifier())->verify($other['access_token']), 40101, 'expired token rejected');
- check(MiniAudit::query()->where('actor_type', 'mini')->where('actor_id', $actor->userId)->count() > 0, 'mini actor audit separate from admin');
- check(WaterDelivery::query()->find($task)->created_by === 0, 'mini IDs not impersonating admin IDs');
- } finally {
- $db->rollBack();
- }
- foreach ($models as $m) {
- check($m::query()->count() === $before[$m], $m . ' fixtures rolled back');
- }
- echo "PASS: $checks mini water API checks; no persistent business changes.\n";
|