usesMontzarPricing(); } public function callbackUrl(int $localOrderId): string { $origin = rtrim(app(ConfigService::class)->resolvePublicSiteUrl(), '/'); if ($origin === '') { $origin = rtrim((string) config('app.url'), '/'); $origin = (string) preg_replace('#/server/?$#', '', $origin); } return $origin . '/server/customer/colleague/order/callback/' . $localOrderId; } /** * @param array $body */ public function handleCallbackPayload(array $body, ?int $localOrderId = null): array { $order = $this->findPendingOrder($body, $localOrderId); if (!$order) { return ['ok' => false, 'message' => 'سفارش در انتظار یافت نشد']; } $sts = $this->providerStsFromBody($body); if ($sts === null) { return ['ok' => false, 'message' => 'وضعیت نامعتبر']; } $this->rememberProviderId($order, $body); return $this->applyProviderSts($order, $sts, $body); } public function refreshIfDue(Order $order, bool $force = false): Order { if ((int) $order->sts !== 4) { return $order; } if (!$force && !$this->isPollDue($order)) { return $order; } try { $this->pollOne($order); } catch (\Throwable $e) { Log::warning('Colleague pending poll failed', [ 'order_id' => $order->id, 'error' => $e->getMessage(), ]); $this->touchPollAt($order); } return Order::query()->find($order->id) ?? $order; } public function pollOne(Order $order): array { $providerId = (int) ($order->provider_id ?? 0); if ($providerId <= 0) { $this->touchPollAt($order); return ['ok' => true, 'state' => 'wait']; } $body = app(MontzarService::class)->fetchColleagueOrder($providerId); $this->touchPollAt($order); if (!is_array($body)) { return ['ok' => true, 'state' => 'wait']; } $sts = $this->providerStsFromBody($body); if ($sts === null) { return ['ok' => true, 'state' => 'wait']; } // جدول سفارش همکار: ۲ موفق / ۳ ناموفق / ۴ هنوز باز. // بعضی پاسخ‌های تکی/کالبک‌مانند: ۴ و ۶ = رد. // اگر sts_str هم صریحاً رد/ناموفق باشد، همان را قطعی می‌گیریم. if ($this->providerBodyLooksRejected($body, $sts)) { return $this->rejectPending($order, $body); } if ($sts === 2) { return $this->approvePending($order, $body); } return ['ok' => true, 'state' => 'wait', 'sts' => (int) $order->sts]; } /** * @param array $body */ public function applyProviderSts(Order $order, int $providerSts, array $body = []): array { if (in_array($providerSts, [4, 6], true)) { return $this->rejectPending($order, $body); } if ($providerSts === 2) { return $this->approvePending($order, $body); } return ['ok' => true, 'state' => 'wait', 'sts' => (int) $order->sts]; } public function pendingForCustomer(int $customerId): ?Order { if ($customerId <= 0) { return null; } return Order::query() ->where('customer_id', $customerId) ->where('sts', 4) ->orderByDesc('id') ->first(); } public function publicStatusPayload(?Order $order): array { if (!$order) { return [ 'pending' => false, 'order' => null, 'sts' => null, 'sts_str' => null, ]; } $sts = (int) $order->sts; return [ 'pending' => $sts === 4, 'order_id' => $order->id, 'sts' => $sts, 'sts_str' => Order::$STS[$sts] ?? null, 'order' => $order, ]; } public function broadcast(Order $order): void { $customerId = (int) ($order->customer_id ?? 0); if ($customerId <= 0) { return; } app(PriceStreamService::class)->publishForCustomer($customerId, self::SSE_EVENT, [ 'order_id' => $order->id, 'sts' => (int) $order->sts, 'sts_str' => Order::$STS[(int) $order->sts] ?? null, 'pending' => (int) $order->sts === 4, ]); } /** * @param array $body */ private function findPendingOrder(array $body, ?int $localOrderId): ?Order { if ($localOrderId && $localOrderId > 0) { $order = Order::query()->where('id', $localOrderId)->where('sts', 4)->first(); if ($order) { return $order; } } $providerId = $this->providerIdFromBody($body); if ($providerId > 0) { return Order::query() ->where('provider_id', $providerId) ->where('sts', 4) ->orderByDesc('id') ->first(); } return null; } /** * @param array $body */ private function rememberProviderId(Order $order, array $body): void { $providerId = $this->providerIdFromBody($body); if ($providerId <= 0 || !Schema::hasColumn('orders', 'provider_id')) { return; } if ((int) ($order->provider_id ?? 0) === $providerId) { return; } $order->provider_id = $providerId; $order->save(); } /** * @param array $body */ private function approvePending(Order $order, array $body): array { $this->rememberProviderId($order, $body); $done = false; DB::transaction(function () use ($order, &$done) { $locked = Order::query()->where('id', $order->id)->lockForUpdate()->first(); if (!$locked || (int) $locked->sts !== 4) { return; } app(SingleOrderController::class)->finalizeColleaguePendingOrder($locked); $done = true; }); $fresh = Order::find($order->id); if ($fresh) { $this->broadcast($fresh); } return [ 'ok' => true, 'state' => $done ? 'approved' : 'ignored', 'sts' => $fresh ? (int) $fresh->sts : 2, ]; } /** * @param array $body */ private function rejectPending(Order $order, array $body): array { DB::transaction(function () use ($order, $body) { $locked = Order::query()->where('id', $order->id)->lockForUpdate()->first(); if (!$locked || (int) $locked->sts !== 4) { return; } $message = trim((string) ($body['sts_str'] ?? $body['message'] ?? 'رد شده توسط همکار')); app(SingleOrderController::class)->rejectColleaguePendingOrder($locked, $message !== '' ? $message : 'رد شده توسط همکار'); }); $fresh = Order::find($order->id); if ($fresh) { $this->broadcast($fresh); } return [ 'ok' => true, 'state' => 'rejected', 'sts' => $fresh ? (int) $fresh->sts : 3, ]; } private function touchPollAt(Order $order): void { if (!Schema::hasColumn('orders', 'colleague_last_poll_at')) { return; } $order->colleague_last_poll_at = now(); $order->save(); } private function isPollDue(Order $order): bool { $cutoff = now()->subSeconds(self::POLL_SECONDS); $last = $order->colleague_last_poll_at ?? null; if ($last) { return $last <= $cutoff; } $created = $order->created_at; return !$created || $created <= $cutoff; } /** * @param array $body */ public function providerStsFromBody(array $body): ?int { // اول ردیف سفارش تو‌در‌تو / rows — نه خودِ envelope که گاهی sts کهنه می‌گذارد $rows = []; foreach ([$body['order'] ?? null, $body['data'] ?? null] as $row) { if (is_array($row)) { $rows[] = $row; } } if (isset($body['rows']) && is_array($body['rows'])) { foreach ($body['rows'] as $row) { if (is_array($row)) { $rows[] = $row; } } } foreach ($body as $item) { if (!is_array($item)) { continue; } $rows[] = $item; if (isset($item['order']) && is_array($item['order'])) { $rows[] = $item['order']; } if (isset($item['data']) && is_array($item['data'])) { $rows[] = $item['data']; } } $rows[] = $body; foreach ($rows as $row) { if (!is_array($row) || !array_key_exists('sts', $row) || $row['sts'] === null || $row['sts'] === '') { continue; } return (int) $row['sts']; } return null; } /** * @param array $body */ private function providerBodyLooksRejected(array $body, int $sts): bool { if (in_array($sts, [3, 6], true)) { return true; } $str = mb_strtolower(trim((string) ( $body['sts_str'] ?? $body['order']['sts_str'] ?? $body['data']['sts_str'] ?? $body['message'] ?? '' ))); if ($str !== '' && preg_match('/رد|ناموفق|reject|fail|cancel|لغو/u', $str)) { // فقط وقتی sts هنوز «باز/در انتظار» نیست که با متن موفق قاطی شود return $sts !== 2; } return false; } /** * @param array $body */ private function providerIdFromBody(array $body): int { foreach ([$body['order'] ?? null, $body['data'] ?? null, $body] as $row) { if (!is_array($row)) { continue; } foreach (['order_id', 'id', 'provider_order_id'] as $key) { $value = (int) ($row[$key] ?? 0); if ($value > 0) { return $value; } } } return 0; } }