change copperConvertPage

This commit is contained in:
2026-11-01 12:42:14 +03:30
parent 9528056036
commit 3f59af5a6d
10 changed files with 495 additions and 3 deletions
@@ -0,0 +1,305 @@
<?php
namespace App\Services;
use App\Http\Controllers\Single\SingleOrderController;
use App\Models\CopperConvertRequest;
use App\Models\Customer;
use App\Models\Product;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
class CopperConvertRequestService
{
public function submit(Customer $customer, string $source, int $rings15, int $rings25): CopperConvertRequest
{
if ($rings15 < 1 && $rings25 < 1) {
throw new UnprocessableEntityHttpException('حداقل یک حلقه انتخاب کنید.');
}
$pending = CopperConvertRequest::query()
->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
{
return 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;
});
}
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();
return $row;
}
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;
}
}