where('customer_id', $customer->id) ->where('status', 'pending') ->exists(); if ($pending) { throw new UnprocessableEntityHttpException('یک درخواست تبدیل در انتظار تأیید دارید.'); } $quote = $this->quote($customer, $source, $rings15, $rings25); return CopperConvertRequest::query()->create([ 'customer_id' => $customer->id, 'status' => 'pending', 'source' => $source, 'source_product_id' => $quote['source']->id, 'asset_unit' => $quote['unit'], 'consumed_amount' => $quote['consumed'], 'leftover_amount' => $quote['leftover'], 'source_unit_price' => $quote['source_unit_price'], 'wire15_product_id' => $quote['wire15']?->id, 'wire15_rings' => $rings15, 'wire15_unit_price' => $quote['price15'], 'wire25_product_id' => $quote['wire25']?->id, 'wire25_rings' => $rings25, 'wire25_unit_price' => $quote['price25'], ]); } public function listForAdmin(): array { return CopperConvertRequest::query() ->with('customer:id,fname,lname,name,mobile') ->orderByDesc('id') ->limit(200) ->get() ->map(function (CopperConvertRequest $row) { $customer = $row->customer; $name = trim((string) ($customer->name ?? '')); if ($name === '') { $name = trim(((string) ($customer->fname ?? '')).' '.((string) ($customer->lname ?? ''))); } return [ 'id' => $row->id, 'status' => $row->status, 'source' => $row->source, 'customer_name' => $name !== '' ? $name : 'مشتری', 'customer_mobile' => $customer->mobile ?? '', 'asset_unit' => $row->asset_unit, 'consumed_amount' => $row->consumed_amount, 'leftover_amount' => $row->leftover_amount, 'source_unit_price' => $row->source_unit_price, 'wire15_rings' => $row->wire15_rings, 'wire15_unit_price' => $row->wire15_unit_price, 'wire25_rings' => $row->wire25_rings, 'wire25_unit_price' => $row->wire25_unit_price, 'created_at' => optional($row->created_at)->toDateTimeString(), ]; }) ->all(); } public function approve(int $id, int $adminId): CopperConvertRequest { $row = DB::transaction(function () use ($id, $adminId) { $row = CopperConvertRequest::query()->lockForUpdate()->findOrFail($id); if ($row->status !== 'pending') { throw new UnprocessableEntityHttpException('این درخواست قبلاً تعیین‌تکلیف شده است.'); } $customer = Customer::query()->findOrFail($row->customer_id); $available = $this->availableQuantity($customer, (int) $row->source_product_id); if ($available + 0.0001 < (float) $row->consumed_amount) { throw new UnprocessableEntityHttpException('موجودی مشتری برای این تبدیل کافی نیست.'); } $note = 'درخواست تبدیل #'.$row->id; $sellId = $this->placeLeg($customer, (int) $row->source_product_id, 2, (float) $row->consumed_amount, (float) $row->source_unit_price, $note); $buy15 = (int) $row->wire15_rings > 0 ? $this->placeLeg($customer, (int) $row->wire15_product_id, 1, (float) $row->wire15_rings, (float) $row->wire15_unit_price, $note) : null; $buy25 = (int) $row->wire25_rings > 0 ? $this->placeLeg($customer, (int) $row->wire25_product_id, 1, (float) $row->wire25_rings, (float) $row->wire25_unit_price, $note) : null; $row->status = 'approved'; $row->admin_id = $adminId; $row->sell_order_id = $sellId; $row->buy15_order_id = $buy15; $row->buy25_order_id = $buy25; $row->save(); return $row; }); $this->notifyCustomer($row, true); return $row; } public function reject(int $id, int $adminId): CopperConvertRequest { $row = CopperConvertRequest::query()->findOrFail($id); if ($row->status !== 'pending') { throw new UnprocessableEntityHttpException('این درخواست قبلاً تعیین‌تکلیف شده است.'); } $row->status = 'rejected'; $row->admin_id = $adminId; $row->save(); $this->notifyCustomer($row, false); return $row; } private function notifyCustomer(CopperConvertRequest $row, bool $approved): void { try { $customer = Customer::query()->find($row->customer_id); $mobile = trim((string) ($customer?->mobile ?? '')); if ($mobile === '') { return; } $company = trim((string) Controller::Config('name_fa')); $source = $this->sourceLabel((string) $row->source); $lines = [$company !== '' ? $company : 'درخواست تبدیل']; if ($approved) { $lines[] = 'درخواست تبدیل #'.$row->id.' تأیید شد.'; $lines[] = 'دارایی '.$source.' به مقدار '.$row->consumed_amount.' '.$row->asset_unit.' فروخته شد.'; if ((int) $row->wire15_rings > 0) { $lines[] = 'حلقه ۱.۵: '.$row->wire15_rings; } if ((int) $row->wire25_rings > 0) { $lines[] = 'حلقه ۲.۵: '.$row->wire25_rings; } } else { $lines[] = 'درخواست تبدیل #'.$row->id.' رد شد.'; $lines[] = 'دارایی '.$source.' شما بدون تغییر ماند.'; } Controller::Send($mobile, implode("\n", $lines)); } catch (\Throwable $e) { report($e); } } private function sourceLabel(string $source): string { if ($source === 'katod') { return 'کاتد'; } if ($source === 'maftoul') { return 'مفتول'; } if ($source === 'shamsh') { return 'شمش'; } return $source; } private function placeLeg(Customer $customer, int $productId, int $direction, float $number, float $unitPrice, string $note): int { if ($productId < 1 || $number <= 0 || $unitPrice <= 0) { throw new UnprocessableEntityHttpException('اطلاعات سفارش تبدیل ناقص است.'); } auth('customer')->setUser($customer); try { $response = app(SingleOrderController::class)->store(Request::create('/admin/single/order', 'POST', [ 'customer_id' => $customer->id, 'product_id' => $productId, 'direction' => $direction, 'number' => $number, 'method' => 'cash', 'price' => $unitPrice, 'difference' => 0, 'quote_unit_price' => $unitPrice, 'description' => $note, ])); } finally { auth('customer')->forgetUser(); } $body = $response->getData(true); $orderId = (int) ($body[1]['order']['id'] ?? 0); if ($orderId < 1) { throw new UnprocessableEntityHttpException('ثبت سفارش تبدیل انجام نشد.'); } return $orderId; } private function quote(Customer $customer, string $source, int $rings15, int $rings25): array { $products = Product::query()->where('active', 1)->get(); $sourceProduct = $this->findSource($products, $source); if (!$sourceProduct) { throw new UnprocessableEntityHttpException('محصول این دارایی پیدا نشد.'); } $wire15 = $rings15 > 0 ? $this->findWire($products, '1.5') : null; $wire25 = $rings25 > 0 ? $this->findWire($products, '2.5') : null; if ($rings15 > 0 && !$wire15) { throw new UnprocessableEntityHttpException('محصول سیم ۱.۵ پیدا نشد.'); } if ($rings25 > 0 && !$wire25) { throw new UnprocessableEntityHttpException('محصول سیم ۲.۵ پیدا نشد.'); } $prices = app(ProductDisplayPriceService::class); $sourcePerKg = (float) $prices->resolveOrderTradePrice($sourceProduct, 1); $price15 = $wire15 ? (int) round((float) $prices->resolveOrderTradePrice($wire15, 1)) : 0; $price25 = $wire25 ? (int) round((float) $prices->resolveOrderTradePrice($wire25, 1)) : 0; if ($sourcePerKg <= 0 || ($rings15 > 0 && $price15 <= 0) || ($rings25 > 0 && $price25 <= 0)) { throw new UnprocessableEntityHttpException('قیمت تبدیل در دسترس نیست.'); } $unit = trim((string) ($sourceProduct->unit ?: 'کیلوگرم')); if ($unit === '') { $unit = 'کیلوگرم'; } $available = $this->availableQuantity($customer, (int) $sourceProduct->id); $availableValue = $this->toKg($available, $unit, $sourceProduct) * $sourcePerKg; $spent = ($rings15 * $price15) + ($rings25 * $price25); if ($spent > $availableValue + 1) { throw new UnprocessableEntityHttpException('تعداد حلقه از ارزش دارایی بیشتر است.'); } $consumedKg = $spent / $sourcePerKg; $consumed = round($this->fromKg($consumedKg, $unit, $sourceProduct), 4); if ($consumed <= 0) { throw new UnprocessableEntityHttpException('مقدار قابل تبدیل صفر است.'); } if ($consumed > $available + 0.0001) { throw new UnprocessableEntityHttpException('موجودی این دارایی کافی نیست.'); } $leftover = round(max(0, $available - $consumed), 4); $sourceUnitPrice = (int) ceil($spent / $consumed); return [ 'source' => $sourceProduct, 'wire15' => $wire15, 'wire25' => $wire25, 'unit' => $unit, 'consumed' => $consumed, 'leftover' => $leftover, 'source_unit_price' => max(1, $sourceUnitPrice), 'price15' => (int) round($price15), 'price25' => (int) round($price25), ]; } private function availableQuantity(Customer $customer, int $productId): float { return (float) app(AssetSummaryService::class)->productDisplayQuantity( (int) $customer->id, (int) ($customer->type ?? 2), $productId ); } private function findSource(Collection $products, string $source): ?Product { return $products->first(function (Product $product) use ($source) { $group = trim((string) $product->group_type); $title = (string) $product->title; if ($source === 'katod') { return $group === 'کاتد مس' || str_contains($title, 'کاتد'); } if ($source === 'maftoul') { return $group === 'مفتول مس' || str_contains($title, 'مفتول'); } if ($source === 'shamsh') { return str_contains($group, 'شمش') || str_contains($title, 'شمش'); } return false; }); } private function findWire(Collection $products, string $section): ?Product { return $products->first(function (Product $product) use ($section) { $title = (string) $product->title; $pattern = $section === '1.5' ? '/1\.5|۱\.۵/u' : '/2\.5|۲\.۵/u'; return preg_match($pattern, $title) === 1; }); } private function toKg(float $quantity, string $unit, Product $product): float { if ($quantity <= 0) { return 0; } if (str_contains($unit, 'کیلو')) { return $quantity; } if (str_contains($unit, 'گرم')) { return $quantity / 1000; } $perPiece = (float) ($product->amount ?? 0); if ($perPiece <= 0) { return $quantity; } $pieceUnit = trim((string) $product->unit); $grams = str_contains($pieceUnit, 'کیلو') ? $perPiece * 1000 : $perPiece; return ($quantity * $grams) / 1000; } private function fromKg(float $kg, string $unit, Product $product): float { if ($kg <= 0) { return 0; } if (str_contains($unit, 'کیلو')) { return $kg; } if (str_contains($unit, 'گرم')) { return $kg * 1000; } $perPiece = (float) ($product->amount ?? 0); if ($perPiece <= 0) { return $kg; } $pieceUnit = trim((string) $product->unit); $grams = str_contains($pieceUnit, 'کیلو') ? $perPiece * 1000 : $perPiece; if ($grams <= 0) { return $kg; } return ($kg * 1000) / $grams; } }