isConditionalOrderEnabled()) { return; } $this->expirePendingPastDue(); $orders = ConditionalOrder::query() ->with(['customer', 'product']) ->where('status', ConditionalOrder::STATUS_PENDING) ->where('is_active', true) ->whereHas('customer', function ($q) { $q->where('conditional_order_active', 1); }) ->get(); if ($orders->isEmpty()) { return; } $display = app(ProductDisplayPriceService::class); $liquidPrice = (int) ($payload['price'] ?? $payload['buy'] ?? 0); if ($liquidPrice <= 0) { $liquidPrice = (int) ($config->getInt('montzar_live_price', 0)); } $productsById = Product::query() ->whereIn('id', $orders->pluck('product_id')->unique()->filter()->all()) ->get() ->keyBy('id'); if ($productsById->isNotEmpty()) { $display->enrichForApi($productsById); } foreach ($orders as $order) { try { $this->tryExecute($order, $productsById, $liquidPrice, $config); } catch (\Throwable $e) { Log::warning('Conditional order evaluate failed', [ 'id' => $order->id, 'message' => $e->getMessage(), ]); } } } private function tryExecute( ConditionalOrder $order, $productsById, int $liquidPrice, ConfigService $config ): void { $product = $productsById->get($order->product_id) ?? $order->product; if (! $product) { return; } $display = app(ProductDisplayPriceService::class); $currentPrice = $this->resolveComparePrice($product, $liquidPrice, $display); if ($currentPrice <= 0 || ! $order->conditionMet($currentPrice)) { return; } try { $config->assertProductTradingOpen($product); } catch (\Throwable $e) { // خارج ساعت — pending بماند return; } $this->executeOrder($order, $product, $currentPrice); } private function resolveComparePrice(Product $product, int $liquidPrice, ProductDisplayPriceService $display): int { if ($display->usesAppPrice($product)) { $base = $liquidPrice > 0 ? $liquidPrice : $display->resolveOrderBasePrice($product); return max(0, (int) $base); } $price = (int) ($product->price ?? 0); if ($price > 0) { return $price; } return max(0, (int) $display->resolveOrderBasePrice($product)); } private function executeOrder(ConditionalOrder $order, Product $product, int $currentPrice): void { $customer = $order->customer ?? Customer::find($order->customer_id); if (! $customer || ! $customer->conditional_order_active) { return; } $payload = [ 'customer_id' => (int) $customer->id, 'direction' => (int) $order->direction, 'method' => 'cash', ]; if ($this->isMeltedLikeProduct($product)) { $payload['abshode_id'] = (int) $product->id; $payload['amount'] = (float) ($order->amount ?? 0); if ($payload['amount'] <= 0) { $this->markFailed($order, $currentPrice, 'مقدار گرم نامعتبر است'); return; } } else { $payload['product_id'] = (int) $product->id; $payload['number'] = (float) ($order->number ?? $order->amount ?? 0); if ($payload['number'] <= 0) { $this->markFailed($order, $currentPrice, 'تعداد نامعتبر است'); return; } } $guard = Auth::guard('customer'); $previous = $guard->user(); $guard->setUser($customer); try { $request = Request::create('/customer/single/order', 'POST', $payload); $response = app(SingleOrderController::class)->store($request); $body = method_exists($response, 'getData') ? (array) $response->getData(true) : (is_array($response) ? $response : []); $orderData = (isset($body[1]) && is_array($body[1])) ? $body[1] : $body; $createdOrderId = (int) ( data_get($orderData, 'order.id') ?: data_get($orderData, 'id') ?: 0 ); $order->status = ConditionalOrder::STATUS_EXECUTED; $order->is_active = false; $order->order_id = $createdOrderId > 0 ? $createdOrderId : null; $order->triggered_price = $currentPrice; $order->executed_at = now(); $order->fail_reason = null; $order->save(); } catch (\Throwable $e) { $message = $e instanceof HttpExceptionInterface ? ($e->getMessage() ?: 'خطا در ثبت سفارش') : ($e->getMessage() ?: 'خطا در ثبت سفارش'); $this->markFailed($order, $currentPrice, $message); } finally { if ($previous) { $guard->setUser($previous); } else { $guard->forgetUser(); } } } private function markFailed(ConditionalOrder $order, int $currentPrice, string $reason): void { $order->status = ConditionalOrder::STATUS_FAILED; $order->is_active = false; $order->triggered_price = $currentPrice; $order->executed_at = now(); $order->fail_reason = mb_substr($reason, 0, 250); $order->save(); } /** آبشده / معامله وزنی → amount؛ محصول ثابت → number */ private function isMeltedLikeProduct(Product $product): bool { $group = trim((string) ($product->group_type ?? '')); if (in_array($group, ['آبشده', 'کاتد مس'], true)) { return true; } if ((bool) ($product->trade_by_quantity ?? false)) { return true; } $mainId = app(ConfigService::class)->mainProductId(); return $mainId > 0 && (int) $product->id === (int) $mainId; } public function expirePendingPastDue(): int { $now = now()->timezone(config('app.timezone'))->format('Y-m-d H:i:s'); return ConditionalOrder::query() ->where('status', ConditionalOrder::STATUS_PENDING) ->whereNotNull('expires_at') ->where('expires_at', '<', $now) ->update([ 'status' => ConditionalOrder::STATUS_EXPIRED, 'is_active' => false, 'fail_reason' => 'منقضی شده', ]); } public function assertCustomerCanUse(Customer $customer): void { $config = app(ConfigService::class); if (! $config->isConditionalOrderEnabled()) { throw new \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException( 'قابلیت سفارش شرطی غیرفعال است.' ); } if (! $customer->conditional_order_active) { throw new \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException( 'سفارش شرطی برای شما مجاز نیست.' ); } } public function assertWithinLimit(Customer $customer): void { $limit = $customer->conditional_order_limit; if ($limit === null || (int) $limit <= 0) { return; } $pending = ConditionalOrder::query() ->where('customer_id', $customer->id) ->where('status', ConditionalOrder::STATUS_PENDING) ->count(); if ($pending >= (int) $limit) { throw new \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException( 'سقف تعداد سفارش شرطی شما پر شده است.' ); } } public function chargeRegistrationFee(Customer $customer): int { $fee = app(ConfigService::class)->conditionalOrderFee(); if ($fee <= 0) { return 0; } $walletInterface = new \Rooyesh\Wallet\interfaces\WalletInterface( (int) $customer->id, (int) $customer->type ); $wallet = $walletInterface->wallet; if (! $wallet || (float) $wallet->cash < $fee) { throw new \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException( 'موجودی ریالی برای کارمزد سفارش شرطی کافی نیست.' ); } new \Rooyesh\Wallet\interfaces\TransactionInterface($walletInterface, [ 'cash' => $fee, 'expire_at' => now(), 'note' => 'کارمزد ثبت سفارش شرطی', 'title' => 'کارمزد سفارش شرطی', 'active' => true, ], 4, true, false); return $fee; } }