EnterpriseQuery.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. <?php
  2. declare(strict_types=1);
  3. namespace app\miniapi\service;
  4. use app\miniapi\http\Fields;
  5. use app\miniapi\http\Pagination;
  6. use app\miniapi\exception\ApiException;
  7. use app\miniapi\model\MiniCheckout;
  8. use app\miniapi\model\MiniTaskRecord;
  9. use app\miniapi\model\MiniPhoto;
  10. use plugin\saiadmin\app\model\water\WaterEnterprise;
  11. use plugin\saiadmin\app\model\water\WaterEnterpriseAddress;
  12. use plugin\saiadmin\app\model\water\WaterAccount;
  13. use plugin\saiadmin\app\model\water\WaterOrder;
  14. use plugin\saiadmin\app\model\water\WaterDelivery;
  15. use plugin\saiadmin\app\model\water\WaterDevice;
  16. use plugin\saiadmin\app\model\water\WaterDeviceUsage;
  17. use plugin\saiadmin\app\model\water\WaterLedger;
  18. use plugin\saiadmin\app\service\WaterContentDashboard;
  19. use plugin\saiadmin\app\service\WaterFulfillment;
  20. use plugin\saiadmin\app\service\alerts\ReplenishmentPolicy;
  21. final class EnterpriseQuery
  22. {
  23. public function rewards(int $enterprise, array $input = []): array
  24. {
  25. $this->access->enterprise($enterprise);
  26. return (new \plugin\saiadmin\app\service\promotion\PromotionQuery())->enterprise($enterprise, $input);
  27. }
  28. public function __construct(private readonly Access $access)
  29. {
  30. }
  31. public function addressDetail(int $enterprise): array
  32. {
  33. $row = $this->access->enterprise($enterprise, true);
  34. $address = WaterEnterpriseAddress::query()->where('enterprise_id', $enterprise)
  35. ->where('is_default', 1)->where('status', 1)->orderBy('id')->first();
  36. return $address
  37. ? EnterpriseAddressPresenter::detail($address, $row)
  38. : array_replace(WaterPresenter::enterprise($row), ['phone' => $row->phone]);
  39. }
  40. public function addressDetailById(int $enterprise, int $address): array
  41. {
  42. $row = $this->access->enterprise($enterprise, true);
  43. $site = WaterEnterpriseAddress::query()->where('enterprise_id', $enterprise)
  44. ->where('status', 1)->find($address);
  45. if (!$site) {
  46. throw new ApiException(40401, '配送地址不存在或已停用', 404);
  47. }
  48. return EnterpriseAddressPresenter::detail($site, $row);
  49. }
  50. public function profile(?int $enterprise): array
  51. {
  52. $result = [
  53. 'user_id' => $this->access->actor->userId,
  54. 'identities' => $this->access->identities(),
  55. 'balance_cents' => null,
  56. 'reward_cents' => null,
  57. 'reward_buckets' => null,
  58. 'capabilities' => [
  59. 'balance_payment' => false,
  60. 'rewards' => false,
  61. 'withdrawals' => false,
  62. ],
  63. ];
  64. if ($enterprise !== null) {
  65. $balance = BalanceService::cents($enterprise);
  66. $result += [
  67. 'enterprise' => WaterPresenter::enterprise($this->access->enterprise($enterprise)),
  68. 'rights' => $this->rights($enterprise)['summary'],
  69. ];
  70. $result['balance_cents'] = $balance;
  71. $result['capabilities']['balance_payment'] = true;
  72. $promotion = \plugin\saiadmin\app\service\promotion\PromotionPolicy::current();
  73. $result['reward_buckets'] = $promotion['initialized']
  74. ? \plugin\saiadmin\app\service\promotion\PromotionQuery::issuedBuckets($enterprise) : 0;
  75. $result['capabilities']['rewards'] = $promotion['enabled'];
  76. }
  77. return $result;
  78. }
  79. public function addresses(): array
  80. {
  81. $ids = [];
  82. foreach ($this->access->identities() as $row) {
  83. if (str_starts_with($row['role'], 'enterprise_')) {
  84. $ids[] = $row['resource_id'];
  85. }
  86. }
  87. $enterprises = WaterEnterprise::query()->whereIn('id', array_unique($ids))->orderBy('id')->get()->keyBy('id');
  88. $sites = WaterEnterpriseAddress::query()->whereIn('enterprise_id', $enterprises->keys()->all())
  89. ->where('status', 1)->orderByDesc('is_default')->orderBy('id')->get();
  90. $items = [];
  91. foreach ($sites as $site) {
  92. $enterprise = $enterprises->get($site->enterprise_id);
  93. if ($enterprise) {
  94. $items[] = EnterpriseAddressPresenter::summary($site, $enterprise);
  95. }
  96. }
  97. foreach ($enterprises as $enterprise) {
  98. if (!$sites->contains(fn ($site) => (int) $site->enterprise_id === (int) $enterprise->id)) {
  99. $items[] = EnterpriseAddressPresenter::legacy($enterprise);
  100. }
  101. }
  102. return [
  103. 'items' => $items,
  104. 'capabilities' => [
  105. 'multiple_sites_per_enterprise' => true,
  106. 'self_create_enterprise' => false,
  107. ],
  108. 'service_hours' => null,
  109. 'coverage_verified' => false,
  110. ];
  111. }
  112. public function home(int $enterprise): array
  113. {
  114. $enterpriseRow = $this->access->enterprise($enterprise);
  115. $content = (new WaterContentDashboard())->preview([
  116. 'purchase_status' => WaterOrder::query()->where('enterprise_id', $enterprise)->exists() ? 'purchased' : 'new',
  117. 'is_enterprise_admin' => $this->access->isEnterpriseAdmin($enterprise) ? 1 : 0,
  118. 'region' => $enterpriseRow->region,
  119. ]);
  120. return [
  121. 'enterprise' => WaterPresenter::enterprise($enterpriseRow),
  122. 'banners' => $content['banners'],
  123. 'packages' => (new Catalog())->listing(['page_size' => 6])['items'],
  124. 'rights' => $this->rights($enterprise),
  125. 'devices' => $this->devices($enterprise, ['page_size' => 10]),
  126. 'unread_messages' => null,
  127. 'capabilities' => [
  128. 'coupon' => false,
  129. 'messages' => false,
  130. 'automatic_delivery' => false,
  131. ],
  132. ];
  133. }
  134. public function rights(int $enterprise): array
  135. {
  136. $enterpriseRow = $this->access->enterprise($enterprise);
  137. $policy = new ReplenishmentPolicy();
  138. $items = [];
  139. foreach (WaterAccount::query()->with('sku')->where('enterprise_id', $enterprise)->orderBy('id')->get() as $account) {
  140. $sku = $account->sku;
  141. $estimate = $policy->evaluate($account->toArray() + [
  142. 'employee_count' => $enterpriseRow->employee_count,
  143. 'liters' => $sku?->liters ?? 0,
  144. ]);
  145. $items[] = [
  146. 'id' => (int) $account->id,
  147. 'sku_id' => (int) $account->sku_id,
  148. 'sku_name' => $sku?->name,
  149. 'liters' => $sku ? (float) $sku->liters : null,
  150. 'remaining' => (int) $account->entitlement,
  151. 'frozen' => (int) $account->frozen,
  152. 'available' => $estimate['available_entitlement'],
  153. 'full_qty' => (int) $account->full_qty,
  154. 'empty_qty' => (int) $account->empty_qty,
  155. 'days_left' => $estimate['days_left'],
  156. 'severity' => $estimate['severity'],
  157. 'expires_at' => null,
  158. ];
  159. }
  160. return [
  161. 'items' => $items,
  162. 'summary' => [
  163. 'remaining' => array_sum(array_column($items, 'remaining')),
  164. 'available' => array_sum(array_column($items, 'available')),
  165. 'frozen' => array_sum(array_column($items, 'frozen')),
  166. 'full_qty' => array_sum(array_column($items, 'full_qty')),
  167. 'purchased' => (int) WaterOrder::query()->where('enterprise_id', $enterprise)->sum('buckets'),
  168. 'delivered' => (int) WaterDelivery::query()->where('enterprise_id', $enterprise)->sum('delivered_qty'),
  169. ],
  170. 'policy' => $policy->settings(),
  171. 'accounting' => 'enterprise_sku',
  172. 'capabilities' => [
  173. 'individual_package_cards' => false,
  174. 'expiry_deduction' => false,
  175. ],
  176. ];
  177. }
  178. public function ledger(int $enterprise, array $input): array
  179. {
  180. $this->access->enterprise($enterprise);
  181. $page = new Pagination($input);
  182. $query = WaterLedger::query()->where('enterprise_id', $enterprise)->whereIn('kind', [
  183. 'purchase',
  184. 'reserve',
  185. 'cancel',
  186. 'complete',
  187. 'partial_receive',
  188. 'delivery_return',
  189. 'promotion_reward',
  190. ]);
  191. $total = (clone $query)->count();
  192. $rows = $query->orderByDesc('id')->offset($page->offset())->limit($page->pageSize)->get([
  193. 'id',
  194. 'sku_id',
  195. 'kind',
  196. 'quantity',
  197. 'reference',
  198. 'created_at',
  199. ])->toArray();
  200. return $page->result($rows, $total) + ['quantity_semantics' => '操作桶数,非每条记录均代表权益扣减'];
  201. }
  202. public function orders(int $enterprise, array $input): array
  203. {
  204. $this->access->enterprise($enterprise);
  205. $page = new Pagination($input);
  206. $kind = Fields::choice($input, 'kind', ['purchases', 'deliveries', 'checkouts'], 'deliveries');
  207. if ($kind === 'deliveries') {
  208. $query = WaterDelivery::query()->with('sku')->where('enterprise_id', $enterprise);
  209. $status = Fields::choice($input, 'status', [
  210. 'all',
  211. 'pending',
  212. 'transit',
  213. 'completed',
  214. 'cancelled',
  215. 'exception',
  216. ], 'all');
  217. $states = [
  218. 'pending' => ['pending', 'assigned'],
  219. 'transit' => ['transit'],
  220. 'completed' => ['completed'],
  221. 'cancelled' => ['cancelled'],
  222. 'exception' => ['returning', 'returned'],
  223. ];
  224. if ($status !== 'all') {
  225. $query->whereIn('status', $states[$status]);
  226. }
  227. $total = (clone $query)->count();
  228. $items = $query->orderByDesc('id')->offset($page->offset())->limit($page->pageSize)->get()->map(WaterPresenter::delivery(...))->all();
  229. } elseif ($kind === 'purchases') {
  230. $query = WaterOrder::query()->where('enterprise_id', $enterprise);
  231. $total = (clone $query)->count();
  232. $items = $query->orderByDesc('id')->offset($page->offset())->limit($page->pageSize)
  233. ->get([
  234. 'id',
  235. 'package_name',
  236. 'sku_id',
  237. 'buckets',
  238. 'amount_cents',
  239. 'created_at',
  240. ])->toArray();
  241. } else {
  242. $query = MiniCheckout::query()->where('enterprise_id', $enterprise);
  243. $total = (clone $query)->count();
  244. $items = $query->orderByDesc('id')->offset($page->offset())->limit($page->pageSize)->get()->map(fn ($record) => CheckoutService::present($record))->all();
  245. }
  246. return $page->result($items, $total) + ['kind' => $kind];
  247. }
  248. public function tracking(int $enterprise, int $id): array
  249. {
  250. $delivery = $this->access->enterpriseTask($enterprise, $id);
  251. $history = (new WaterFulfillment())->history($id);
  252. $events = array_map(fn ($row) => [
  253. 'action' => $row['action'],
  254. 'created_at' => $row['created_at'],
  255. ], $history);
  256. foreach (MiniTaskRecord::query()->where('delivery_id', $id)->whereIn('kind', ['depart', 'checkin', 'exception'])->orderBy('id')->get() as $record) {
  257. $events[] = [
  258. 'action' => $record->kind,
  259. 'created_at' => $record->created_at,
  260. ];
  261. }
  262. usort($events, fn ($account, $b) => strcmp($account['created_at'], $b['created_at']));
  263. return WaterPresenter::delivery($delivery) + [
  264. 'events' => $events,
  265. 'photos' => MiniPhoto::query()->where('delivery_id', $id)->orderBy('id')->get(['id', 'created_at'])->toArray(),
  266. 'eta' => null,
  267. 'distance_km' => null,
  268. 'capabilities' => ['live_gps' => false],
  269. ];
  270. }
  271. public function devices(int $enterprise, array $input): array
  272. {
  273. $this->access->enterprise($enterprise);
  274. $page = new Pagination($input);
  275. $query = WaterDevice::query()->where('enterprise_id', $enterprise);
  276. $total = (clone $query)->count();
  277. $items = [];
  278. foreach ($query->orderBy('id')->offset($page->offset())->limit($page->pageSize)->get() as $device) {
  279. $items[] = $this->device($device);
  280. }
  281. return $page->result($items, $total);
  282. }
  283. private function device($device): array
  284. {
  285. $state = trim($device->fault) !== '' ? 'fault' : ($device->last_seen === null ? 'unconnected' : ($device->last_seen < date('Y-m-d H:i:s', time() - 900) ? 'offline' : 'online'));
  286. return [
  287. 'id' => (int) $device->id,
  288. 'name' => $device->name,
  289. 'sn' => $device->sn,
  290. 'sku_id' => (int) $device->sku_id,
  291. 'state' => $state,
  292. 'last_seen' => $device->last_seen,
  293. 'today_liters' => $device->telemetry_date === date('Y-m-d') && $device->today_liters !== null ? (float) $device->today_liters : null,
  294. 'fault' => $device->fault,
  295. ];
  296. }
  297. public function deviceDetail(int $enterprise, int $id): array
  298. {
  299. $this->access->enterprise($enterprise);
  300. $device = WaterDevice::query()->where('enterprise_id', $enterprise)->find($id);
  301. if (!$device) {
  302. throw new ApiException(40401, '设备不存在', 404);
  303. }
  304. $known = WaterDeviceUsage::query()->where('device_id', $id)->whereBetween('day', [date('Y-m-d', strtotime('-6 days')), date('Y-m-d')])->pluck('liters', 'day');
  305. $trend = [];
  306. for ($i = 6;$i >= 0;$i--) {
  307. $day = date('Y-m-d', strtotime('-' . $i . ' days'));
  308. $trend[] = [
  309. 'day' => $day,
  310. 'liters' => isset($known[$day]) ? (float) $known[$day] : null,
  311. ];
  312. }
  313. return $this->device($device) + [
  314. 'trend' => $trend,
  315. 'rights' => $this->rights($enterprise),
  316. 'automatic_delivery' => false,
  317. ];
  318. }
  319. }