306 lines
12 KiB
PHP
306 lines
12 KiB
PHP
<?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;
|
||
}
|
||
}
|