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); 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; } if (isset($data["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); 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(); })->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(): void { dispatch(function () { app(PriceBroadcastService::class)->broadcastAfterPriceUpdate(); })->afterResponse(); } public function updatePrice(Request $request){ } /** فروشگاه — نمایش در اپ کاربر: فعال + ساعات + قیمت ذخیره‌شده یا محصول وزنی */ private function storeProductShowForCustomer(Product $product): bool { if (! $product->active) { return false; } if (! app(ConfigService::class)->isProductWithinTradingHours($product)) { return false; } $storedPrice = (int) ($product->price ?? 0) + (int) ($product->price_profit ?? 0); if ($storedPrice > 0) { return true; } if ((int) ($product->direct_price ?? 0) === 0 && (float) ($product->amount ?? 0) > 0) { return true; } return false; } 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; } if (app(ConfigService::class)->isStore()) { $products->each(function (Product $product) { $product->setAttribute('show', $this->storeProductShowForCustomer($product)); }); return; } $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 || app(ConfigService::class)->isStore()) { 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()) { unset($data['order_limit_enabled'], $data['min_order_cash'], $data['max_order_cash']); } elseif (empty($data['order_limit_enabled'])) { $data['order_limit_enabled'] = false; $data['min_order_cash'] = null; $data['max_order_cash'] = null; } else { $min = $data['min_order_cash'] ?? null; $max = $data['max_order_cash'] ?? null; $hasMin = $min !== null && $min !== ''; $hasMax = $max !== null && $max !== ''; if (!$hasMin && !$hasMax) { throw new UnprocessableEntityHttpException( 'با فعال بودن محدودیت مبلغ سفارش، حداقل یکی از حداقل یا حداکثر مبلغ باید وارد شود' ); } if ($hasMin && $hasMax && (float) $min > (float) $max) { throw new UnprocessableEntityHttpException('حداقل مبلغ نمی‌تواند بیشتر از حداکثر باشد'); } $data['order_limit_enabled'] = true; $data['min_order_cash'] = $hasMin ? (int) $min : null; $data['max_order_cash'] = $hasMax ? (int) $max : null; } 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::hasInventoryColumn()) { unset($data['inventory']); } 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']); } $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 = str_replace([',', ' ', '٬'], '', $value); } return (float) $value; } }