with = ['file'/*, 'price', 'default', 'sub'*/]; // $this->with = []; parent::__construct(Product::class, $this->with, ['*'], null, null, ['title']); } public function tableProduct(Request $request) { $data = $request->all(); if (isset($data["select"]) && is_null($data["select"])){ $data["select"] = []; } if (is_null($this->query)){ $query_table = $this->model::with($this->with); //*******کداضافه شده // ->where('active', '<=', now()) // فیلتر محصولات فعال // ->get(); //********* }else{ $query_table = $this->query; // $query_table = $query_table->where('active', '<=', now()); } TableEngine::create($query_table, $data) ->initSearch($this->search) ->select($this->select) ->export($query_table); $this->enrichAdminTablePrices($query_table['rows'] ?? null); if (Schema::hasColumn((new Product)->getTable(), 'auto_open_at')) { $query_table['auto_open_at'] = Product::selectRaw( 'SEC_TO_TIME(AVG(TIME_TO_SEC(TIME(auto_open_at)))) as avg_time' )->value('avg_time'); } if (Schema::hasColumn((new Product)->getTable(), 'auto_close_at')) { $query_table['auto_close_at'] = Product::selectRaw( 'SEC_TO_TIME(AVG(TIME_TO_SEC(TIME(auto_close_at)))) as avg_time' )->value('avg_time'); } // $query_table['11'] = $avgDuration; // $query_table['11'] = Product::avg('auto_open_at'); // $query_table['112'] = Product::avg('auto_close_at'); return $query_table; } public function archiveTable(Request $request) { $data = $request->all(); if (isset($data['select']) && is_null($data['select'])) { $data['select'] = []; } $query_table = Product::onlyTrashed()->with($this->with); TableEngine::create($query_table, $data) ->initSearch($this->search) ->select($this->select) ->export($query_table); $this->enrichAdminTablePrices($query_table['rows'] ?? null); return $query_table; } public function archive($id) { $product = Product::onlyTrashed()->findOrFail($id); $product->restore(); $this->broadcastPricesAfterResponse(); return response()->json(true); } public function editProducts(Request $request) { if (isset($request->auto_open_at, $request->auto_close_at)) { Product::query()->update([ 'auto_open_at' => $request->auto_open_at, 'auto_close_at' => $request->auto_close_at, ]); return response()->json(['message' => 'Products updated successfully.']); } return response()->json(['error' => 'auto_open_at and auto_close_at must be set'], 400); } public function store(Request $request) { $data = $this->filterProductData($request->all()); $product = Product::create($data); $this->broadcastPricesAfterResponse(true); return $this->resultCreate($product->id); } public function update(Request $request, $id) { // return 1; $data = $this->filterProductData($request->all()); $clearingReadingMethod = array_key_exists('reading_method', $data) && ($data['reading_method'] === null || $data['reading_method'] === ''); if ($clearingReadingMethod) { $data['reading_method'] = null; } // قیمت دستی ⇒ تعدیل مرجع بی‌معناست و صفر می‌شود. // قیمت اپ در filterProductData عمداً price=0 می‌گذارد؛ نباید تعدیل را پاک کند. if (isset($data['price']) && empty($data['app_price'])) { $data['reading_method'] = null; if (Product::hasPriceOffsetColumn()) { $data['price_offset_percent'] = 0; } } if (isset($data["auto_close_at"]) and isset($data["auto_close_at"])){ if ($data["auto_close_at"] == $data["auto_open_at"]){ unset($data["auto_open_at"]); unset($data["auto_close_at"]); } } // return $data['price']; // if(!isset($data['default'])){ // throw new UnprocessableEntityHttpException('مقادیر اعتبارات را وارد کنید'); // } // $data['auto_open_at'] = Carbon::parse($data['auto_open_at'])->format('Y-m-d H:i:s'); // $data['auto_close_at'] = Carbon::parse($data['auto_close_at'])->format('Y-m-d H:i:s'); $product = Product::findOrFail($id); $oldOffsetPercent = (float) ($product->price_offset_percent ?? 0); unset($data['trade_by_quantity']); if ( Product::hasPriceOffsetColumn() && $product->reading_method && array_key_exists('price_offset_percent', $data) && !$clearingReadingMethod ) { ProductPriceService::reapplyOffsetPercent( $product, (float) $data['price_offset_percent'], $oldOffsetPercent ); unset($data['price'], $data['price_profit']); } $product->update($data); $this->broadcastPricesAfterResponse(true); return response()->json(true); } public function destroy($id) { $product = Product::findOrFail($id); $product->update([ 'active' => 0, 'show' => 0, 'open_buy' => 0, 'open_sell' => 0, ]); $product->delete(); $this->broadcastPricesAfterResponse(); return response()->json(true); } public function listProduct() { if (app(ConfigService::class)->isStore()) { // اپ مشتری: فقط DB — sync با HTTP فقط cron/admin (جلوگیری از pending طولانی list) if (! $this->isCustomerApiRequest()) { app(ProductPriceSyncService::class)->syncStorePrices(true); $this->auto_close_open(); } return response()->json($this->customerStoreProductListPayload()); } // آبشده خانگی — کران قیمت را آپدیت می‌کند؛ list فقط از DB می‌خواند $products = Product::with(['file'])->where('active', 1)->get(); if ($this->isCustomerApiRequest()) { $this->applyCustomerShow($products); } if (Product::hasPriceOffsetColumn() && $this->isCustomerApiRequest()) { $products->each(function (Product $product) { if ($product->reading_method) { $product->makeHidden(['price_offset_percent']); } }); } return response()->json($products); } public function ProductById($id) { if (! $this->isCustomerApiRequest()) { app(ProductPriceSyncService::class)->syncStorePrices(true); $this->auto_close_open(); } $product = Product::with(['file']) ->where('id', $id) ->first(); if ($product) { if ($this->isCustomerApiRequest()) { $this->applyCustomerShow(collect([$product])); } $product->similar = Product::with(['file']) ->where('group_type', $product->group_type) ->where('id', '!=', $id) ->get(); if ($this->isCustomerApiRequest()) { $this->applyCustomerShow($product->similar); } } else { return response()->json(false); } return response()->json($product); } public function getPrice() { if (app(ConfigService::class)->isStore()) { app(ProductPriceSyncService::class)->syncStorePrices(true); } else { app(ProductPriceSyncService::class)->syncAll(true); } dispatch(function () { app(PriceBroadcastService::class)->broadcastAfterPriceUpdate(); app(\App\Services\PriceChannelBroadcastService::class)->broadcastIfEligible(); })->afterResponse(); return response()->json(true); } public function auto_close_open() { $config = app(ConfigService::class); $products = Product::with([])->get(); foreach ($products as $product) { Product::where('id', $product->id)->update([ 'show' => $config->isProductWithinTradingHours($product), ]); } return response()->json(true); } public function getProduct($id) { $product = Product::with($this->with)->where('id', $id)->first(); return response()->json($product); } /** * فعال/غیرفعال محصول — show باید با active هم‌گام باشد (اپ کاربر از show استفاده می‌کند). */ public function active($id) { $product = Product::findOrFail($id); $active = !$product->active; $product->update([ 'active' => $active, 'show' => $active, ]); return response()->json(true); } public function hide($id) { $query = Product::findOrFail($id); $query->update([ 'show' => !$query->show ]); return response()->json(true); } public function openSell($id) { return $this->toggleBooleanStatus($id, 'open_sell'); } public function openBuy($id) { return $this->toggleBooleanStatus($id, 'open_buy'); } public function auto_buy($id) { return $this->toggleBooleanStatus($id, 'auto_buy'); } public function auto_sell($id) { return $this->toggleBooleanStatus($id, 'auto_sell'); } private function toggleBooleanStatus($id, $field) { $product = Product::findOrFail($id); $product->$field = !$product->$field; $product->save(); if (!$product->open_buy and !$product->open_sell) { $product->show = false; $product->active = false; } else { $product->show = true; $product->active = true; } $product->save(); return response()->json($product->$field); } public function auto_group($action, $method) { // Define valid actions and methods $validActions = ['sell' => 'auto_sell', 'buy' => 'auto_buy', 'all' => ['auto_buy', 'auto_sell']]; $validMethods = ['true' => 1, 'false' => 0]; // Validate action and method if (!isset($validActions[$action]) || !array_key_exists($method, $validMethods)) { return response()->json(['error' => 'Invalid action or method'], 400); } // Get fields to update and value to set $fields = $validActions[$action]; $value = $validMethods[$method]; // Update products Product::query()->update(array_fill_keys((array)$fields, $value)); return response()->json(true); } private const ACTIVATION_SNAPSHOT_KEY = 'products_activation_snapshot'; public function activationSnapshotStatus() { $row = Cnt::where('key', self::ACTIVATION_SNAPSHOT_KEY)->first(); $payload = $row?->jsonPayload() ?? []; $items = $payload['items'] ?? []; return response()->json([ 'has_snapshot' => ! empty($items), 'saved_at' => $payload['saved_at'] ?? null, 'count' => count($items), ]); } public function deactivateAllProducts() { $products = Product::query() ->where(function ($query) { $query->where('active', 1) ->orWhere('open_buy', 1) ->orWhere('open_sell', 1); }) ->get(['id', 'active', 'show', 'open_buy', 'open_sell']); $items = $products->map(static fn (Product $product) => [ 'id' => $product->id, 'active' => (bool) $product->active, 'show' => (bool) $product->show, 'open_buy' => (bool) $product->open_buy, 'open_sell' => (bool) $product->open_sell, ])->values()->all(); Cnt::saveJsonPayload( self::ACTIVATION_SNAPSHOT_KEY, [ 'saved_at' => now()->timezone('Asia/Tehran')->format('Y-m-d H:i:s'), 'items' => $items, ], 'ذخیره وضعیت محصولات' ); $affected = Product::query()->update([ 'active' => 0, 'show' => 0, 'open_buy' => 0, 'open_sell' => 0, ]); $this->broadcastPricesAfterResponse(); return response()->json([ 'message' => 'همه محصولات بسته شد.', 'snapshot_count' => count($items), 'affected' => $affected, 'has_snapshot' => ! empty($items), ]); } public function restoreActivationSnapshot() { $row = Cnt::where('key', self::ACTIVATION_SNAPSHOT_KEY)->first(); $items = $row?->jsonPayload()['items'] ?? []; if (empty($items)) { return response()->json(['error' => 'اسنAPSHOTی برای بازگردانی وجود ندارد.'], 422); } $restored = 0; foreach ($items as $item) { $updated = Product::where('id', $item['id'] ?? 0)->update([ 'active' => ! empty($item['active']) ? 1 : 0, 'show' => ! empty($item['show']) ? 1 : 0, 'open_buy' => ! empty($item['open_buy']) ? 1 : 0, 'open_sell' => ! empty($item['open_sell']) ? 1 : 0, ]); if ($updated) { $restored++; } } $this->auto_close_open(); $this->broadcastPricesAfterResponse(); return response()->json([ 'message' => 'وضعیت قبلی محصولات بازگردانی شد.', 'restored' => $restored, 'has_snapshot' => true, ]); } public function closeAllBuy() { Product::query()->update(['open_buy' => 0]); $inactive = Product::where('open_buy', 0)->where('open_sell', 0)->update([ 'active' => 0, 'show' => 0, ]); $this->broadcastPricesAfterResponse(); return response()->json([ 'message' => 'خرید همه محصولات بسته شد.', 'inactive' => $inactive, ]); } public function closeAllSell() { Product::query()->update(['open_sell' => 0]); $inactive = Product::where('open_buy', 0)->where('open_sell', 0)->update([ 'active' => 0, 'show' => 0, ]); $this->broadcastPricesAfterResponse(); return response()->json([ 'message' => 'فروش همه محصولات بسته شد.', 'inactive' => $inactive, ]); } public function syncProductShow() { $this->auto_close_open(); $this->broadcastPricesAfterResponse(); return response()->json(['message' => 'نمایش محصولات طبق ساعات به‌روز شد.']); } private function broadcastPricesAfterResponse(bool $force = false): void { dispatch(function () use ($force) { app(PriceBroadcastService::class)->broadcastAfterPriceUpdate(null, $force); })->afterResponse(); } public function updatePrice(Request $request){ } private function isCustomerApiRequest(): bool { $path = trim((string) request()->path(), '/'); return str_starts_with($path, 'customer/') || str_contains($path, '/customer/'); } /** * لیست محصولات فروشگاه برای اپ/SSE — فقط DB، بدون sync خارجی. * * @return array */ public function customerStoreProductListPayload(): array { $products = Product::with(['file'])->where('active', 1)->get(); $categoryService = app(CategoryService::class); $products = $categoryService->filterProductsByVisibleCategories($products); $products = $categoryService->sortProductsForStore($products); $this->applyCustomerShow($products); return $products->values()->all(); } private function applyCustomerShow($products): void { $products = $products instanceof \Illuminate\Support\Collection ? $products : collect($products); if ($products->isEmpty()) { return; } // فروشگاه و خانگی: همان enrich قیمت لحظه‌ای (آبشده/قیمت اپ/مرجع) — بدون UPDATE دیتابیس $displayPriceService = app(ProductDisplayPriceService::class); $displayPriceService->warmLivePriceCache(); $displayPriceService->enrichForApi($products); $products->each(function (Product $product) use ($displayPriceService) { $product->setAttribute('show', $displayPriceService->resolveCustomerShow($product)); }); } /** * جدول ادمین — قیمت لحظه‌ای روی مدل (بدون UPDATE دیتابیس)، مثل فرم ویرایش و اپ مشتری. * * @param iterable|null $rows */ private function enrichAdminTablePrices($rows): void { if ($rows === null) { return; } $products = collect($rows)->filter(fn ($row) => $row instanceof Product); if ($products->isEmpty()) { return; } $displayPriceService = app(ProductDisplayPriceService::class); $displayPriceService->warmLivePriceCache(); $displayPriceService->enrichForApi($products); } private function filterProductData(array $data): array { if (!Product::hasPriceOffsetColumn()) { unset($data['price_offset_percent']); } if (!Product::hasOrderLimitColumns()) { $requestedLimit = filter_var($data['order_limit_enabled'] ?? false, FILTER_VALIDATE_BOOLEAN) || $this->normalizeOrderLimitCash($data['min_order_cash'] ?? null) !== null || $this->normalizeOrderLimitCash($data['max_order_cash'] ?? null) !== null; if ($requestedLimit) { throw new UnprocessableEntityHttpException( 'ستون‌های محدودیت مبلغ سفارش در دیتابیس وجود ندارد. مایگریشن را اجرا کنید: php artisan migrate — یا اسکریپت database/sql/add_product_order_limit_columns.sql' ); } unset($data['order_limit_enabled'], $data['min_order_cash'], $data['max_order_cash']); } else { $min = $this->normalizeOrderLimitCash($data['min_order_cash'] ?? null); $max = $this->normalizeOrderLimitCash($data['max_order_cash'] ?? null); $orderLimitEnabled = filter_var($data['order_limit_enabled'] ?? false, FILTER_VALIDATE_BOOLEAN) || $min !== null || $max !== null; if (!$orderLimitEnabled) { $data['order_limit_enabled'] = false; $data['min_order_cash'] = null; $data['max_order_cash'] = null; } else { if ($min === null && $max === null) { throw new UnprocessableEntityHttpException( 'با فعال بودن محدودیت مبلغ سفارش، حداقل یکی از حداقل یا حداکثر مبلغ باید وارد شود' ); } if ($min !== null && $max !== null && $min > $max) { throw new UnprocessableEntityHttpException('حداقل مبلغ نمی‌تواند بیشتر از حداکثر باشد'); } $data['order_limit_enabled'] = true; $data['min_order_cash'] = $min; $data['max_order_cash'] = $max; } } if (!Product::hasShowInAssetsColumn()) { unset($data['show_in_assets']); } elseif (array_key_exists('show_in_assets', $data)) { $data['show_in_assets'] = !empty($data['show_in_assets']); } if (!Product::hasUseLastPriceIfZeroColumn()) { unset($data['use_last_price_if_zero']); } elseif (array_key_exists('use_last_price_if_zero', $data)) { $data['use_last_price_if_zero'] = !empty($data['use_last_price_if_zero']); } if (!Product::hasTradeByQuantityColumn()) { unset($data['trade_by_quantity']); } elseif (array_key_exists('trade_by_quantity', $data)) { $data['trade_by_quantity'] = !empty($data['trade_by_quantity']); } if (!Product::hasDirectPriceColumn()) { unset($data['direct_price']); } if (!Product::hasAppPriceColumn()) { unset($data['app_price']); } elseif (array_key_exists('app_price', $data)) { $data['app_price'] = filter_var($data['app_price'], FILTER_VALIDATE_BOOLEAN); // قیمت اپ: مرجع خارجی ندارد، متغیر است (هر گرم × وزن) و قیمت دستی نگه داشته نمی‌شود if ($data['app_price']) { $data['reading_method'] = null; if (Product::hasDirectPriceColumn()) { $data['direct_price'] = 0; } $data['price'] = 0; $data['price_profit'] = 0; } } if (!Product::hasInventoryColumn()) { unset($data['inventory']); } if (!Product::hasDescriptionColumn()) { unset($data['description']); } elseif (array_key_exists('description', $data)) { $desc = is_string($data['description']) ? trim($data['description']) : ''; $data['description'] = $desc === '' ? null : $desc; } if (!Product::hasOverrideTradingHoursColumn()) { unset($data['override_trading_hours']); } elseif (array_key_exists('override_trading_hours', $data)) { $data['override_trading_hours'] = !empty($data['override_trading_hours']); } if (!Product::hasPhysicalDeliveryColumns()) { unset( $data['has_physical_delivery'], $data['min_physical_delivery_quantity'], $data['min_physical_delivery_grams'] ); } else { // سازگاری با نسخهٔ قبلی که فیلد را min_physical_delivery_grams می‌فرستاد if (!array_key_exists('min_physical_delivery_quantity', $data) && array_key_exists('min_physical_delivery_grams', $data)) { $data['min_physical_delivery_quantity'] = $data['min_physical_delivery_grams']; } unset($data['min_physical_delivery_grams']); $hasDelivery = array_key_exists('has_physical_delivery', $data) ? filter_var($data['has_physical_delivery'], FILTER_VALIDATE_BOOLEAN) : false; $data['has_physical_delivery'] = $hasDelivery; if (!$hasDelivery) { $data['min_physical_delivery_quantity'] = null; } else { $rawMin = $data['min_physical_delivery_quantity'] ?? null; $min = $this->normalizeCashFeeValue($rawMin); if ($min <= 0) { throw new UnprocessableEntityHttpException( 'برای فعال کردن تحویل فیزیکی باید حداقل مقدار معتبر وارد شود' ); } $data['min_physical_delivery_quantity'] = $min; } } $data['cash_fee_buy'] = $this->normalizeCashFeeValue($data['cash_fee_buy'] ?? 0); $data['cash_fee_sell'] = $this->normalizeCashFeeValue($data['cash_fee_sell'] ?? 0); if (Product::hasCashFeePercentModeColumns()) { $data['cash_fee_buy_is_percent'] = filter_var( $data['cash_fee_buy_is_percent'] ?? false, FILTER_VALIDATE_BOOLEAN ); $data['cash_fee_sell_is_percent'] = filter_var( $data['cash_fee_sell_is_percent'] ?? false, FILTER_VALIDATE_BOOLEAN ); } else { unset($data['cash_fee_buy_is_percent'], $data['cash_fee_sell_is_percent']); } return $data; } private function normalizeCashFeeValue(mixed $value): float { if ($value === null || $value === '') { return 0.0; } if (is_string($value)) { $value = trim(str_replace([' ', '٬'], '', $value)); if (str_contains($value, ',') && ! str_contains($value, '.')) { $value = str_replace(',', '.', $value); } else { $value = str_replace(',', '', $value); } } return (float) $value; } private function normalizeOrderLimitCash(mixed $value): ?int { if ($value === null || $value === '') { return null; } if (is_int($value)) { return $value >= 0 ? $value : null; } if (is_float($value)) { return $value >= 0 ? (int) $value : null; } if (! is_string($value)) { return null; } $value = $this->normalizeAsciiDigits(trim(str_replace([' ', ',', '٬'], '', $value))); if ($value === '' || ! ctype_digit($value)) { return null; } return (int) $value; } private function normalizeAsciiDigits(string $value): string { $persian = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹']; $arabic = ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩']; $latin = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; return str_replace(array_merge($persian, $arabic), array_merge($latin, $latin), $value); } }