access->enterprise($enterprise); return (new \plugin\saiadmin\app\service\promotion\PromotionQuery())->enterprise($enterprise, $input); } public function __construct(private readonly Access $access) { } public function addressDetail(int $enterprise): array { $row = $this->access->enterprise($enterprise, true); $address = WaterEnterpriseAddress::query()->where('enterprise_id', $enterprise) ->where('is_default', 1)->where('status', 1)->orderBy('id')->first(); return $address ? EnterpriseAddressPresenter::detail($address, $row) : array_replace(WaterPresenter::enterprise($row), ['phone' => $row->phone]); } public function addressDetailById(int $enterprise, int $address): array { $row = $this->access->enterprise($enterprise, true); $site = WaterEnterpriseAddress::query()->where('enterprise_id', $enterprise) ->where('status', 1)->find($address); if (!$site) { throw new ApiException(40401, '配送地址不存在或已停用', 404); } return EnterpriseAddressPresenter::detail($site, $row); } public function profile(?int $enterprise): array { $result = [ 'user_id' => $this->access->actor->userId, 'identities' => $this->access->identities(), 'balance_cents' => null, 'reward_cents' => null, 'reward_buckets' => null, 'capabilities' => [ 'balance_payment' => false, 'rewards' => false, 'withdrawals' => false, ], ]; if ($enterprise !== null) { $balance = BalanceService::cents($enterprise); $result += [ 'enterprise' => WaterPresenter::enterprise($this->access->enterprise($enterprise)), 'rights' => $this->rights($enterprise)['summary'], ]; $result['balance_cents'] = $balance; $result['capabilities']['balance_payment'] = true; $promotion = \plugin\saiadmin\app\service\promotion\PromotionPolicy::current(); $result['reward_buckets'] = $promotion['initialized'] ? \plugin\saiadmin\app\service\promotion\PromotionQuery::issuedBuckets($enterprise) : 0; $result['capabilities']['rewards'] = $promotion['enabled']; } return $result; } public function addresses(): array { $ids = []; foreach ($this->access->identities() as $row) { if (str_starts_with($row['role'], 'enterprise_')) { $ids[] = $row['resource_id']; } } $enterprises = WaterEnterprise::query()->whereIn('id', array_unique($ids))->orderBy('id')->get()->keyBy('id'); $sites = WaterEnterpriseAddress::query()->whereIn('enterprise_id', $enterprises->keys()->all()) ->where('status', 1)->orderByDesc('is_default')->orderBy('id')->get(); $items = []; foreach ($sites as $site) { $enterprise = $enterprises->get($site->enterprise_id); if ($enterprise) { $items[] = EnterpriseAddressPresenter::summary($site, $enterprise); } } foreach ($enterprises as $enterprise) { if (!$sites->contains(fn ($site) => (int) $site->enterprise_id === (int) $enterprise->id)) { $items[] = EnterpriseAddressPresenter::legacy($enterprise); } } return [ 'items' => $items, 'capabilities' => [ 'multiple_sites_per_enterprise' => true, 'self_create_enterprise' => false, ], 'service_hours' => null, 'coverage_verified' => false, ]; } public function home(int $enterprise): array { $enterpriseRow = $this->access->enterprise($enterprise); $content = (new WaterContentDashboard())->preview([ 'purchase_status' => WaterOrder::query()->where('enterprise_id', $enterprise)->exists() ? 'purchased' : 'new', 'is_enterprise_admin' => $this->access->isEnterpriseAdmin($enterprise) ? 1 : 0, 'region' => $enterpriseRow->region, ]); return [ 'enterprise' => WaterPresenter::enterprise($enterpriseRow), 'banners' => $content['banners'], 'packages' => (new Catalog())->listing(['page_size' => 6])['items'], 'rights' => $this->rights($enterprise), 'devices' => $this->devices($enterprise, ['page_size' => 10]), 'unread_messages' => null, 'capabilities' => [ 'coupon' => false, 'messages' => false, 'automatic_delivery' => false, ], ]; } public function rights(int $enterprise): array { $enterpriseRow = $this->access->enterprise($enterprise); $policy = new ReplenishmentPolicy(); $items = []; foreach (WaterAccount::query()->with('sku')->where('enterprise_id', $enterprise)->orderBy('id')->get() as $account) { $sku = $account->sku; $estimate = $policy->evaluate($account->toArray() + [ 'employee_count' => $enterpriseRow->employee_count, 'liters' => $sku?->liters ?? 0, ]); $items[] = [ 'id' => (int) $account->id, 'sku_id' => (int) $account->sku_id, 'sku_name' => $sku?->name, 'liters' => $sku ? (float) $sku->liters : null, 'remaining' => (int) $account->entitlement, 'frozen' => (int) $account->frozen, 'available' => $estimate['available_entitlement'], 'full_qty' => (int) $account->full_qty, 'empty_qty' => (int) $account->empty_qty, 'days_left' => $estimate['days_left'], 'severity' => $estimate['severity'], 'expires_at' => null, ]; } return [ 'items' => $items, 'summary' => [ 'remaining' => array_sum(array_column($items, 'remaining')), 'available' => array_sum(array_column($items, 'available')), 'frozen' => array_sum(array_column($items, 'frozen')), 'full_qty' => array_sum(array_column($items, 'full_qty')), 'purchased' => (int) WaterOrder::query()->where('enterprise_id', $enterprise)->sum('buckets'), 'delivered' => (int) WaterDelivery::query()->where('enterprise_id', $enterprise)->sum('delivered_qty'), ], 'policy' => $policy->settings(), 'accounting' => 'enterprise_sku', 'capabilities' => [ 'individual_package_cards' => false, 'expiry_deduction' => false, ], ]; } public function ledger(int $enterprise, array $input): array { $this->access->enterprise($enterprise); $page = new Pagination($input); $query = WaterLedger::query()->where('enterprise_id', $enterprise)->whereIn('kind', [ 'purchase', 'reserve', 'cancel', 'complete', 'partial_receive', 'delivery_return', 'promotion_reward', ]); $total = (clone $query)->count(); $rows = $query->orderByDesc('id')->offset($page->offset())->limit($page->pageSize)->get([ 'id', 'sku_id', 'kind', 'quantity', 'reference', 'created_at', ])->toArray(); return $page->result($rows, $total) + ['quantity_semantics' => '操作桶数,非每条记录均代表权益扣减']; } public function orders(int $enterprise, array $input): array { $this->access->enterprise($enterprise); $page = new Pagination($input); $kind = Fields::choice($input, 'kind', ['purchases', 'deliveries', 'checkouts'], 'deliveries'); if ($kind === 'deliveries') { $query = WaterDelivery::query()->with('sku')->where('enterprise_id', $enterprise); $status = Fields::choice($input, 'status', [ 'all', 'pending', 'transit', 'completed', 'cancelled', 'exception', ], 'all'); $states = [ 'pending' => ['pending', 'assigned'], 'transit' => ['transit'], 'completed' => ['completed'], 'cancelled' => ['cancelled'], 'exception' => ['returning', 'returned'], ]; if ($status !== 'all') { $query->whereIn('status', $states[$status]); } $total = (clone $query)->count(); $items = $query->orderByDesc('id')->offset($page->offset())->limit($page->pageSize)->get()->map(WaterPresenter::delivery(...))->all(); } elseif ($kind === 'purchases') { $query = WaterOrder::query()->where('enterprise_id', $enterprise); $total = (clone $query)->count(); $items = $query->orderByDesc('id')->offset($page->offset())->limit($page->pageSize) ->get([ 'id', 'package_name', 'sku_id', 'buckets', 'amount_cents', 'created_at', ])->toArray(); } else { $query = MiniCheckout::query()->where('enterprise_id', $enterprise); $total = (clone $query)->count(); $items = $query->orderByDesc('id')->offset($page->offset())->limit($page->pageSize)->get()->map(fn ($record) => CheckoutService::present($record))->all(); } return $page->result($items, $total) + ['kind' => $kind]; } public function tracking(int $enterprise, int $id): array { $delivery = $this->access->enterpriseTask($enterprise, $id); $history = (new WaterFulfillment())->history($id); $events = array_map(fn ($row) => [ 'action' => $row['action'], 'created_at' => $row['created_at'], ], $history); foreach (MiniTaskRecord::query()->where('delivery_id', $id)->whereIn('kind', ['depart', 'checkin', 'exception'])->orderBy('id')->get() as $record) { $events[] = [ 'action' => $record->kind, 'created_at' => $record->created_at, ]; } usort($events, fn ($account, $b) => strcmp($account['created_at'], $b['created_at'])); return WaterPresenter::delivery($delivery) + [ 'events' => $events, 'photos' => MiniPhoto::query()->where('delivery_id', $id)->orderBy('id')->get(['id', 'created_at'])->toArray(), 'eta' => null, 'distance_km' => null, 'capabilities' => ['live_gps' => false], ]; } public function devices(int $enterprise, array $input): array { $this->access->enterprise($enterprise); $page = new Pagination($input); $query = WaterDevice::query()->where('enterprise_id', $enterprise); $total = (clone $query)->count(); $items = []; foreach ($query->orderBy('id')->offset($page->offset())->limit($page->pageSize)->get() as $device) { $items[] = $this->device($device); } return $page->result($items, $total); } private function device($device): array { $state = trim($device->fault) !== '' ? 'fault' : ($device->last_seen === null ? 'unconnected' : ($device->last_seen < date('Y-m-d H:i:s', time() - 900) ? 'offline' : 'online')); return [ 'id' => (int) $device->id, 'name' => $device->name, 'sn' => $device->sn, 'sku_id' => (int) $device->sku_id, 'state' => $state, 'last_seen' => $device->last_seen, 'today_liters' => $device->telemetry_date === date('Y-m-d') && $device->today_liters !== null ? (float) $device->today_liters : null, 'fault' => $device->fault, ]; } public function deviceDetail(int $enterprise, int $id): array { $this->access->enterprise($enterprise); $device = WaterDevice::query()->where('enterprise_id', $enterprise)->find($id); if (!$device) { throw new ApiException(40401, '设备不存在', 404); } $known = WaterDeviceUsage::query()->where('device_id', $id)->whereBetween('day', [date('Y-m-d', strtotime('-6 days')), date('Y-m-d')])->pluck('liters', 'day'); $trend = []; for ($i = 6;$i >= 0;$i--) { $day = date('Y-m-d', strtotime('-' . $i . ' days')); $trend[] = [ 'day' => $day, 'liters' => isset($known[$day]) ? (float) $known[$day] : null, ]; } return $this->device($device) + [ 'trend' => $trend, 'rights' => $this->rights($enterprise), 'automatic_delivery' => false, ]; } }