all(); $data['payment_product_id'] = $this->resolveGoldPaymentProductId($data); if (preg_match('/^gold:\d+$/', (string) ($data['method'] ?? ''))) { $data['method'] = 'gold'; } $data['is_katod'] = ($data['method'] ?? null) === 'katod'; if ($data['is_katod']) { $data['method'] = 'gold'; } $config = app(ConfigService::class); // فیلتر ساعت معامله per-product (سراسری یا override محصول) پایین‌تر در assertProductTradingOpen اعمال می‌شود. // throw new UnprocessableEntityHttpException('به دلیل نوسانات سفارشات بسته میباشد.'); $transactionType = $data['direction'] == 1 ? "خرید" : "فروش"; $customer = Customer::find(auth('customer')->id() ?? ($data['customer_id'] ?? null)); $data['first_buy'] = auth('customer')->id() && $customer && $customer->first_buy_order !== 1; if (auth('customer')->id()) { if (!$customer || !$customer->trade_active) { throw new UnprocessableEntityHttpException('امکان سفارش برای شما غیرفعال است.'); } self::assertCustomerIdentityVerified($customer); $data['customer_id'] = auth('customer')->id(); $orderProductId = (int) ($data['abshode_id'] ?? $data['product_id'] ?? $config->mainProductId() ?? 0); $orderProduct = $orderProductId > 0 ? Product::find($orderProductId) : null; $orderDirection = isset($data['direction']) ? (int) $data['direction'] : null; // پایه وسطِ نرخ خرید/فروش همکار است — سفارش با همان سمتی حساب شود که با همکار معامله می‌کنیم $basePrice = app(ProductDisplayPriceService::class) ->resolveOrderTradePrice($orderProduct, $orderDirection); if ($basePrice <= 0) { if ($config->isOutsideTradingHours()) { throw new UnprocessableEntityHttpException($config->tradingClosedMessage()); } throw new UnprocessableEntityHttpException('عدم ارائه سرویس.'); } $latestPrice = Price::latest()->first(); $data['all_price'] = $latestPrice; $data['price'] = $basePrice; $data['price_id'] = $latestPrice?->id; $data['difference'] = $config->orderPriceDiffRial($orderDirection); $data['service_charge_percentage'] = $config->orderServiceChargePercentage(); $fee_free = Setting::where('key', 'fee_free')->where('is_active', true)->value('value'); if ($fee_free && $this->feeFree($data['customer_id'], $fee_free)) { $data['service_charge_percentage'] = 0; // $data['difference'] = 0; $data['fee_free'] = true; } $data['pay_at'] = (new \DateTime())->format('Y-m-d H:i:s'); } else if (auth('user')->id()) {//admin $data['service_charge_percentage'] = 0; } $this->ensureOrderUnitPriceIsPerGram($data); if($data['price'] === 0){ throw new UnprocessableEntityHttpException(' سفارشات بسته میباشد.'); } $data['sts'] = 2; $data['type'] = 2; $data['payment_sts'] = 4; $data['price_original'] = $data['price']; $data['payment_remind'] = 0; $data['fee'] = $data['difference']; $ownerType = (int) ($customer?->type ?? 2); [$wallet, $wallet_interface] = $this->resolveOrderWallet((int) $data['customer_id'], $customer, $ownerType); if (!$wallet) { throw new UnprocessableEntityHttpException('کیف پول یافت نشد.'); } $response = []; // Check if the user wants to transact with a product or non-product (gold) if (isset($data['product_id']) and $data['product'] = Product::find($data['product_id'])) { $config->assertProductTradingOpen($data['product']); // Check if the user wants to buy if ($data['direction'] == 1) { $config->assertMainGoldZeroBeforeOtherProductBuy($wallet, $data['product']); // کارمزد فقط از fee_service_charge_percentage (بالا ست شده) — percent_fee_* مربوط به کارمزد نیست if (!empty($data['fee_free'])) { $data['service_charge_percentage'] = 0; } $data['price_original'] = $this->resolveProductUnitBasePrice($data['product'], 1); $this->applyCashPriceDiff($config, $data['product'], 1, $data, (float) $data['price_original']); $data['total_fee'] = $data['number'] * $data['fee']; $data['total_price_original'] = $this->resolveProductOrderPricingAmount($data['product'], 1.0) * ($data['price_original'] ?? 0); // Check if the method is cash if ($data['method'] == "cash") { $response['buy'] = $this->processBuyTransactionProduct($data, $wallet, $wallet_interface); } // Check if the method is gold else if ($data['method'] == "gold") { $response['buy'] = $this->processBuyTransactionProductByGold($data, $wallet, $wallet_interface); } else { throw new UnprocessableEntityHttpException('روش پرداخت نامعتبر است.'); } } // Check if the user wants to sell else if ($data['direction'] == 2) { if (!empty($data['fee_free'])) { $data['service_charge_percentage'] = 0; } $data['price_original'] = $this->resolveProductUnitBasePrice($data['product'], 2); $this->applyCashPriceDiff($config, $data['product'], 2, $data, (float) $data['price_original']); $data['total_fee'] = $data['number'] * $data['fee']; $data['total_price_original'] = $this->resolveProductOrderPricingAmount($data['product'], 1.0) * ($data['price_original'] ?? 0); $this->assertMinimumSellCash($data); // Check if the method is cash if ($data['method'] == "cash") { $response['sell'] = $this->processSellTransactionProduct($data, $wallet, $wallet_interface); } // Check if the method is gold else if ($data['method'] == "gold") { $response['sell'] = $this->processSellTransactionProductByGold($data, $wallet, $wallet_interface); } else { throw new UnprocessableEntityHttpException('روش پرداخت نامعتبر است.'); } } } else { // Calculate total price and fee for non-product (gold) transaction $data['total_price_original'] = $data['amount'] * ($data['price_original'] ?? 0); $mainProductId = app(\App\Services\ConfigService::class)->mainProductId(); $productId = $data['abshode_id'] ?? $data['product_id'] ?? $mainProductId; $data['product'] = $productId ? Product::find($productId) : null; if (!$data['product']) { throw new UnprocessableEntityHttpException('محصول آبشده یافت نشد.'); } $config->assertProductTradingOpen($data['product']); if ($data['direction'] == 1) { $this->applyCashPriceDiff($config, $data['product'], 1, $data, (float) ($data['price_original'] ?? 0)); // کارمزد همان fee_service_charge_percentage — override با percent_fee_* ممنوع if (!empty($data['fee_free'])) { $data['service_charge_percentage'] = 0; } } elseif ($data['direction'] == 2) { $this->applyCashPriceDiff($config, $data['product'], 2, $data, (float) ($data['price_original'] ?? 0)); if (!empty($data['fee_free'])) { $data['service_charge_percentage'] = 0; } } // fee اولیه از orderPriceDiffRial ممکن است با اختلاف واقعی محصول فرق کند $data['total_fee'] = $data['amount'] * $data['difference']; $data['product_id'] = $data['product']->id; // Check if the user wants to buy if ($data['direction'] == 1) { if (!$data['product']['open_buy']) { throw new UnprocessableEntityHttpException('خرید این محصول در این لحظه مقدور نمیباشد'); } $response['buy'] = $this->processBuyTransaction($data, $wallet, $wallet_interface); } // Check if the user wants to sell else if ($data['direction'] == 2) { if (!$data['product']['open_sell']) { throw new UnprocessableEntityHttpException('خرید این محصول در این لحظه مقدور نمیباشد'); } $this->assertMinimumSellCash($data); $response['sell'] = $this->processSellTransaction($data, $wallet, $wallet_interface); } } // دفترکل داخلی همیشه ثبت شود — مستقل از اتصال به حسابداری بیرونی if (isset($data['order'])) { try { app(\App\Services\AccountingLedgerService::class)->recordTrade($data['order']); } catch (\Throwable $e) { report($e); } } if ($config->isTahesabAccounting()) { try { $data['tahesab'] = app(TahesabController::class)->storeTrade($data); } catch (\Exception $e) { report($e); } } if ($config->isZarinAccounting()) { try { $data['zarin'] = app(\App\Http\Controllers\Admin\ZarinController::class)->storeTrade($data); } catch (\Exception $e) { report($e); } } if ($config->isPargarAccounting()) { try { $paymentProductId = (int) ($data['payment_product_id'] ?? 0); $paymentKind = app(\App\Services\PargarSyncService::class) ->inferPaymentKindFromContext([ 'payment_product_id' => $paymentProductId, ]); // خرید/فروش عادی از کیف ریالی (بدون payment_product_id) = نقدی if ($paymentProductId <= 0) { $paymentKind = \App\Services\PargarSyncService::PAYMENT_CASH; } $paymentGold = null; if ($paymentKind === \App\Services\PargarSyncService::PAYMENT_MAIN_GOLD) { $paymentGold = (float) ($data['total_amount'] ?? $data['amount'] ?? 0); } app(\App\Services\PargarSyncService::class)->recordTrade( $data['order'] ?? null, false, [ 'payment_kind' => $paymentKind, 'payment_product_id' => $paymentProductId > 0 ? $paymentProductId : null, 'product_id' => (int) ($data['product_id'] ?? 0) ?: null, 'number' => isset($data['number']) ? (float) $data['number'] : null, 'payment_gold' => $paymentGold, ] ); } catch (\Throwable $e) { report($e); } } if (isset($data['order'])) { try { app(\App\Services\ColleagueAccountingService::class)->recordCoveredTrade($data); } catch (\Throwable $e) { report($e); } } $response['reward'] = $this->give_order_reward($data, $wallet, $wallet_interface); if ((int) $data['direction'] === 1 && $customer) { $referrer = Customer::where('referral_id', $customer->referral_to_id)->first(); if (!empty($referrer) && $referrer->first_buy_order == 1 && $customer->first_buy_order !== 1) { $customer->first_buy_order = true; $customer->save(); $response['bonus_gift'] = $this->bonusGift($customer, $referrer); } } if ($customer && isset($data['order'])) { try { app(ReferralGiftService::class)->tryGiveOnFirstOrder($customer); } catch (\Throwable $e) { report($e); } } if (isset($data['order'])) { try { $response['Notifications'] = $this->sendTransactionNotifications($data, $transactionType); } catch (\Throwable $e) { report($e); } } if ($customer) { app(WalletBroadcastService::class)->broadcastForCustomer( (int) $customer->id, $ownerType ); } return response()->json([$response, $data]); } private function processBuyTransaction(&$data, $wallet, $wallet_interface) { $response = []; $config = app(ConfigService::class); $targetCash = $this->resolveTargetCash($data); // منطق واحد و مستقل از fee_deduct_from_gold: // • کاربر مبلغ ریالی زده (rialLocked) → دقیقاً همان مبلغ پرداخت می‌شود و کارمزد از طلا کم می‌شود. // • کاربر وزن زده (gramLocked) → دقیقاً همان وزن دریافت می‌شود و کارمزد به ریال قابل پرداخت اضافه می‌شود. $rialLocked = $targetCash !== null; $feeRate = isset($data['fee_free']) && $data['fee_free'] === true ? 0.0 : (float) ($data['service_charge_percentage'] ?? 0); $this->ensureOrderUnitPriceIsPerGram($data); $data['price'] += $data['difference']; if ($rialLocked) { // مبلغ ریالی دقیق — کارمزد از طلای دریافتی کسر می‌شود $unitPrice = (float) $data['price']; $grossGold = $unitPrice > 0 ? $targetCash / $unitPrice : 0.0; $data['service_charge'] = $this->normalizeGoldAmount($feeRate * $grossGold); $data['amount'] = $this->normalizeGoldAmount($grossGold - $data['service_charge']); $data['service_charge_price'] = $data['service_charge'] * $data['price_original']; $data['total_price'] = $data['amount'] * $data['price']; $data['payment_amount'] = $targetCash; } else { // وزن دقیق — کارمزد به مبلغ ریالی پرداختی اضافه می‌شود $data['amount'] = $this->normalizeGoldAmount((float) ($data['amount'] ?? 0)); $data['service_charge'] = $this->normalizeGoldAmount($feeRate * $data['amount']); $data['service_charge_price'] = $data['service_charge'] * $data['price_original']; $data['total_price'] = $data['amount'] * $data['price']; $data['payment_amount'] = $data['total_price'] + $data['service_charge_price']; } // سمت کسر کارمزد برای گزارش تفکیک‌شده در داشبورد $data['fee_asset'] = $rialLocked ? 'gold' : 'rial'; $this->enforceProductOrderLimits($data); if (!empty($data['first_buy']) && ($minFirstBuy = Setting::where('is_active', 1)->where('key', 'min_first_buy')->value('value'))) { if ($data['payment_amount'] < (int) $minFirstBuy) { throw new UnprocessableEntityHttpException('حداقل سفارش اول ' . number_format((int) $minFirstBuy) . ' ریال میباشد.'); } } if ($wallet->cash < $data['payment_amount']) { throw new UnprocessableEntityHttpException('موجودی شما کمتر از میزان سفارش است'); } $this->assertProviderOrderAccepted($data); $this->syncOrderProfitFields($data); $data['order'] = $order = Order::create($data); $abshode = $this->resolveAbshodeProductContext($data); $productId = (int) ($data['product_id'] ?? $abshode['id'] ?? 0); $usesProductWallet = $productId > 0 && !$config->isMainWalletProduct($productId); if ($usesProductWallet) { new TransactionInterface($wallet_interface, [ "cash" => $data['payment_amount'], "product_id" => $productId, 'expire_at' => now(), 'active' => true, 'order_id' => $order->id, 'start_at' => now(), 'title' => 'برداشت وجه نقد', ], 12, false, false); $wallet_interface_product = $this->productWalletInterface($productId); $this->normalizeProductWalletGold($wallet_interface_product); new TransactionInterface($wallet_interface_product, [ "gold" => $data['amount'], "product_id" => $productId, 'expire_at' => now(), 'active' => true, 'order_id' => $order->id, 'start_at' => now(), 'title' => $this->meltedOrProductBuyTitle($data), ], 1, true, false); } else { $buyTransaction = [ "cash" => $data['payment_amount'], "gold" => $data['amount'], 'expire_at' => now(), 'active' => true, 'order_id' => $order->id, 'start_at' => now(), 'title' => $this->meltedOrProductBuyTitle($data), ]; if ($productId > 0) { $buyTransaction['product_id'] = $productId; $buyTransaction['note'] = 'product_id:' . $productId; } new TransactionInterface($wallet_interface, $buyTransaction, 1, true, false); } $this->finalizeProviderOrder($data); $response['1'] = 2; return $response; } private function processBuyTransactionProduct(&$data, $wallet, $wallet_interface) { // همان قیمت پایهٔ سفارش (price_original / قیمت اپ) — نه ستون کهنهٔ products.price $data['price'] = $this->resolveProductOrderUnitPrice($data) + (float) ($data['difference'] ?? 0); $data['amount'] = $this->resolveProductOrderPricingAmount($data['product'], (float) $data['number']); // محصول تعدادی: کارمزد فقط از ریال (fee_service_charge_percentage) $feeRate = isset($data['fee_free']) && $data['fee_free'] === true ? 0.0 : (float) ($data['service_charge_percentage'] ?? 0); $data['service_charge'] = $this->normalizeGoldAmount($feeRate * $data['amount']); $data['service_charge_price'] = $data['service_charge'] * $data['price_original']; $data['total_amount'] = $data['amount']; $data['total_price'] = $data['amount'] * $data['price']; $data['payment_amount'] = $data['total_price'] + $data['service_charge_price']; $data['fee_asset'] = 'rial'; $this->enforceProductOrderLimits($data, (float) $data['total_price']); if (!empty($data['first_buy']) && ($minFirstBuy = Setting::where('is_active', 1)->where('key', 'min_first_buy')->value('value'))) { if ($data['payment_amount'] < (int) $minFirstBuy) { throw new UnprocessableEntityHttpException('حداقل سفارش اول ' . number_format((int) $minFirstBuy) . ' ریال میباشد.'); } } $data['wallet'] = $wallet; if ($wallet->cash < $data['payment_amount']) { throw new UnprocessableEntityHttpException('موجودی شما کمتر از میزان سفارش است'); } if (!$data['product']['open_buy']) { throw new UnprocessableEntityHttpException('خرید این محصول در این لحظه مقدور نمیباشد'); } $this->syncOrderProfitFields($data); $data['order'] = $order = Order::create($data); $data['transaction'][] = new TransactionInterface($wallet_interface, $_inp = [ "number" => $data['number'], "cash" => $data['payment_amount'], "product_id" => $data["product_id"], 'expire_at' => now(), 'active' => true, 'order_id' => $order->id, 'title' => "برداشت وجه نقد" ], 12, false, false); $wallet_interface_product = $this->productWalletInterface((int) $data["product_id"]); $this->normalizeProductWalletGold($wallet_interface_product); new TransactionInterface($wallet_interface_product, [ "gold" => $data['number'], "product_id" => $data["product_id"], 'expire_at' => now(), 'active' => true, 'order_id' => $order->id, 'start_at' => now(), 'title' => $this->meltedOrProductBuyTitle($data), ], 1, true, false); } private function processBuyTransactionProductByGold(&$data, $wallet, $wallet_interface) { $data['price'] += $data['difference']; $data['amount'] = $this->resolveProductOrderPricingAmount($data['product'], (float) $data['number']); // محصول تعدادی: کارمزد از ریال (fee_service_charge_percentage) $feeRate = isset($data['fee_free']) && $data['fee_free'] === true ? 0.0 : (float) ($data['service_charge_percentage'] ?? 0); $data['service_charge'] = $this->normalizeGoldAmount($feeRate * $data['amount']); $data['service_charge_price'] = $data['service_charge'] * $data['price_original']; $data['total_amount'] = $data['amount']; $data['total_price'] = $data['amount'] * $data['price']; $data['payment_amount'] = $data['total_price']; $data['fee_asset'] = 'rial'; $data['total_fee'] = 0; $this->enforceProductOrderLimits($data, (float) $data['total_price']); $data['wallet'] = $wallet; $paymentProductId = (int) ($data['payment_product_id'] ?? 0); $paymentWalletInterface = $wallet_interface; $paymentProduct = null; $paymentWallet = $wallet; if ($paymentProductId > 0) { $paymentProduct = Product::find($paymentProductId); if ( !$paymentProduct || (int) ($paymentProduct->metal_id ?? 0) !== (int) ($data['product']['metal_id'] ?? 0) ) { throw new UnprocessableEntityHttpException('روش پرداخت نامعتبر است.'); } $paymentWalletInterface = $this->productWalletInterface($paymentProductId); $paymentWallet = $paymentWalletInterface->wallet; if (!$paymentWallet) { throw new UnprocessableEntityHttpException('کیف پول یافت نشد.'); } } if ((float) $paymentWallet->gold < $data['total_amount']) { throw new UnprocessableEntityHttpException($this->insufficientGoldBalanceMessage($data)); } if ($data['service_charge_price'] > 0 && $wallet->cash < $data['service_charge_price']) { throw new UnprocessableEntityHttpException('موجودی ریالی شما برای کارمزد کافی نیست'); } if (!$data['product']['open_buy']) { throw new UnprocessableEntityHttpException('خرید این محصول در این لحظه مقدور نمیباشد'); } $this->syncOrderProfitFields($data); $data['order'] = $order = Order::create($data); $paymentSellTitle = $paymentProduct ? self::productSellTitle($paymentProduct->title ?? '') : self::mainMetalSellTitle(); $paymentTransaction = [ "number" => $data['number'], "gold" => $data['total_amount'], 'expire_at' => now(), 'active' => true, 'order_id' => $order->id, 'title' => $paymentSellTitle, ]; if ($paymentProductId > 0) { $paymentTransaction['product_id'] = $paymentProductId; $this->normalizeProductWalletGold($paymentWalletInterface); $data['transaction'][] = new TransactionInterface( $paymentWalletInterface, $paymentTransaction, 1, false, false ); } else { $data['transaction'][] = new TransactionInterface( $paymentWalletInterface, $paymentTransaction, 12, false, true ); } if ($data['service_charge_price'] > 0) { $data['transaction'][] = new TransactionInterface($wallet_interface, [ "cash" => $data['service_charge_price'], "product_id" => $data["product_id"], 'expire_at' => now(), 'active' => true, 'order_id' => $order->id, 'title' => 'کارمزد معامله', ], 12, false, false); } $wallet_interface_product = $this->productWalletInterface((int) $data["product_id"]); $this->normalizeProductWalletGold($wallet_interface_product); new TransactionInterface($wallet_interface_product, [ "gold" => $data['number'], "product_id" => $data["product_id"], 'expire_at' => now(), 'active' => true, 'order_id' => $order->id, 'start_at' => now(), 'title' => $this->meltedOrProductBuyTitle($data), ], 1, true, false); } private function processSellTransaction(&$data, $wallet, $wallet_interface) { $response = []; $config = app(ConfigService::class); $targetCash = $this->resolveTargetCash($data); // منطق واحد و مستقل از fee_deduct_from_gold: // • کاربر مبلغ ریالی زده (rialLocked) → دقیقاً همان مبلغ واریز می‌شود و کارمزد از طلای کیف کسر می‌شود. // • کاربر وزن زده (gramLocked) → دقیقاً همان وزن فروخته می‌شود و کارمزد از مبلغ ریالی دریافتی کم می‌شود. $rialLocked = $targetCash !== null; $feeRate = isset($data['fee_free']) && $data['fee_free'] === true ? 0.0 : (float) ($data['service_charge_percentage'] ?? 0); $this->ensureOrderUnitPriceIsPerGram($data); $data['price'] -= $data['difference']; if ($rialLocked) { // مبلغ ریالی دقیق دریافتی — کارمزد از طلای کیف (کسر اضافه‌تر) تأمین می‌شود $unitPrice = (float) $data['price']; $data['amount'] = $unitPrice > 0 ? $this->normalizeGoldAmount($targetCash / $unitPrice) : 0.0; $data['service_charge'] = $this->normalizeGoldAmount($feeRate * $data['amount']); $data['service_charge_price'] = $data['service_charge'] * $data['price_original']; $data['total_price'] = $targetCash; $data['payment_amount'] = $targetCash; $goldDebit = $this->normalizeGoldAmount($data['amount'] + $data['service_charge']); } else { // وزن دقیق فروش — کارمزد از مبلغ ریالی دریافتی کم می‌شود $data['amount'] = $this->normalizeGoldAmount((float) ($data['amount'] ?? 0)); $data['service_charge'] = $this->normalizeGoldAmount($feeRate * $data['amount']); $data['service_charge_price'] = $data['service_charge'] * $data['price_original']; $data['total_price'] = $data['amount'] * $data['price'] - $data['service_charge_price']; $data['payment_amount'] = $data['total_price']; $goldDebit = $data['amount']; } // سمت کسر کارمزد برای گزارش تفکیک‌شده در داشبورد $data['fee_asset'] = $rialLocked ? 'gold' : 'rial'; if (!$rialLocked && (float) $data['total_price'] < 0) { throw new UnprocessableEntityHttpException( 'مبلغ دریافتی فروش پس از کارمزد منفی شد. درصد کارمزد سفارش را بررسی کنید (باید اعشاری باشد؛ مثلاً ۰٫۰۱ برای ۱٪).' ); } $grossTotal = $data['amount'] * $data['price']; $this->enforceProductOrderLimits($data, (float) $grossTotal); $abshode = $this->resolveAbshodeProductContext($data); $productId = (int) ($data['product_id'] ?? $abshode['id'] ?? 0); $usesProductWallet = $productId > 0 && !$config->isMainWalletProduct($productId); $goldWallet = $wallet; $wallet_interface_product = null; if ($usesProductWallet) { $wallet_interface_product = $this->productWalletInterface($productId); $goldWallet = $wallet_interface_product->wallet; if (!$goldWallet) { throw new UnprocessableEntityHttpException('کیف پول محصول یافت نشد.'); } } if ($goldWallet->gold < $goldDebit) { throw new UnprocessableEntityHttpException($this->insufficientGoldBalanceMessage($data)); } $this->assertProviderOrderAccepted($data); $this->syncOrderProfitFields($data); $data['order'] = $order = Order::create($data); if ($usesProductWallet) { $this->normalizeProductWalletGold($wallet_interface_product); new TransactionInterface($wallet_interface_product, [ "gold" => $goldDebit, "product_id" => $productId, 'expire_at' => now(), 'active' => true, 'order_id' => $order->id, 'start_at' => now(), 'title' => $this->meltedOrProductSellTitle($data), ], 1, false, false); new TransactionInterface($wallet_interface, [ "cash" => $data['total_price'], "product_id" => $productId, 'expire_at' => now(), 'active' => true, 'order_id' => $order->id, 'start_at' => now(), 'title' => 'واریز وجه نقد', ], 12, false, true); } else { $sellTransaction = [ "gold" => $goldDebit, "cash" => $data['total_price'], 'expire_at' => now(), 'active' => true, 'order_id' => $order->id, 'title' => $this->meltedOrProductSellTitle($data), ]; if ($productId > 0) { $sellTransaction['product_id'] = $productId; $sellTransaction['note'] = 'product_id:' . $productId; } new TransactionInterface($wallet_interface, $sellTransaction, 2, false, true); } $this->finalizeProviderOrder($data); } private function processSellTransactionProduct(&$data, $wallet, $wallet_interface) { // همان قیمت پایهٔ سفارش (price_original / قیمت اپ) — نه ستون کهنهٔ products.price $data['price'] = max(0.0, $this->resolveProductOrderUnitPrice($data) - (float) ($data['difference'] ?? 0)); $data['amount'] = $this->resolveProductOrderPricingAmount($data['product'], (float) $data['number']); // محصول تعدادی: کارمزد فقط از ریال $feeRate = isset($data['fee_free']) && $data['fee_free'] === true ? 0.0 : (float) ($data['service_charge_percentage'] ?? 0); $data['service_charge'] = $this->normalizeGoldAmount($feeRate * $data['amount']); $data['service_charge_price'] = $data['service_charge'] * $data['price_original']; $data['total_amount'] = $data['amount']; $data['total_price'] = $data['amount'] * $data['price'] - $data['service_charge_price']; $data['payment_amount'] = $data['total_price']; $data['fee_asset'] = 'rial'; if ($data['total_price'] < 0) { throw new UnprocessableEntityHttpException( 'مبلغ دریافتی فروش پس از کارمزد منفی شد. درصد کارمزد سفارش را بررسی کنید (باید اعشاری باشد؛ مثلاً ۰٫۰۱ برای ۱٪).' ); } $grossTotal = (float) ($data['amount'] * $data['price']); $this->enforceProductOrderLimits($data, $grossTotal); $wallet_interface_product = $this->productWalletInterface((int) $data["product_id"]); if ($wallet_interface_product->wallet->gold < $data['number']) { throw new UnprocessableEntityHttpException('موجودی شما کمتر از میزان سفارش است'); } if (!$data['product']['open_sell']) { throw new UnprocessableEntityHttpException('فروش این محصول در این لحظه مقدور نمیباشد'); } $this->syncOrderProfitFields($data); $data['order'] = $order = Order::create($data); $this->normalizeProductWalletGold($wallet_interface_product); new TransactionInterface($wallet_interface_product, [ "gold" => $data['number'], "product_id" => $data["product_id"], 'expire_at' => now(), 'active' => true, 'order_id' => $order->id, 'start_at' => now(), 'title' => $this->meltedOrProductSellTitle($data), ], 1, false, false); $data['transaction'][] = new TransactionInterface($wallet_interface, $_inp = [ "number" => $data['number'], "cash" => $data['total_price'], "product_id" => $data["product_id"], 'expire_at' => now(), 'active' => true, 'order_id' => $order->id, 'title' => "واریز وجه نقد" ], 12, false, true); } private function processSellTransactionProductByGold(&$data, $wallet, $wallet_interface) { $data['price'] -= $data['difference']; $data['amount'] = $this->resolveProductOrderPricingAmount($data['product'], (float) $data['number']); // محصول تعدادی: کارمزد فقط از ریال $feeRate = isset($data['fee_free']) && $data['fee_free'] === true ? 0.0 : (float) ($data['service_charge_percentage'] ?? 0); $data['service_charge'] = $this->normalizeGoldAmount($feeRate * $data['amount']); $data['service_charge_price'] = $data['service_charge'] * $data['price_original']; $data['total_amount'] = $data['amount']; $data['total_price'] = $data['amount'] * $data['price']; $data['payment_amount'] = $data['total_price']; $data['fee_asset'] = 'rial'; $data['total_fee'] = 0; $grossTotal = (float) ($data['amount'] * $data['price']); $this->enforceProductOrderLimits($data, $grossTotal); $wallet_interface_product = $this->productWalletInterface((int) $data["product_id"]); if ($wallet_interface_product->wallet->gold < $data['number']) { throw new UnprocessableEntityHttpException('موجودی شما کمتر از میزان سفارش است'); } if ($data['service_charge_price'] > 0 && $wallet->cash < $data['service_charge_price']) { throw new UnprocessableEntityHttpException('موجودی ریالی شما برای کارمزد کافی نیست'); } if (!$data['product']['open_sell']) { throw new UnprocessableEntityHttpException('فروش این محصول در این لحظه مقدور نمیباشد'); } $this->syncOrderProfitFields($data); $data['order'] = $order = Order::create($data); $data['transaction'][] = new TransactionInterface($wallet_interface, $_inp = [ "number" => $data['number'], "gold" => $data['total_amount'], "product_id" => $data["product_id"], 'expire_at' => now(), 'active' => true, 'order_id' => $order->id, 'title' => $this->meltedOrProductSellTitle($data), ], 12, true, false); if ($data['service_charge_price'] > 0) { $data['transaction'][] = new TransactionInterface($wallet_interface, [ "cash" => $data['service_charge_price'], "product_id" => $data["product_id"], 'expire_at' => now(), 'active' => true, 'order_id' => $order->id, 'title' => 'کارمزد معامله', ], 12, false, false); } $this->normalizeProductWalletGold($wallet_interface_product); new TransactionInterface($wallet_interface_product, [ "gold" => $data['number'], "product_id" => $data["product_id"], 'expire_at' => now(), 'active' => true, 'order_id' => $order->id, 'start_at' => now(), 'title' => $this->meltedOrProductSellTitle($data), ], 1, false, false); } /** * کیف سفارش: مثل goldali — اول legacyهای main_wallet_products روی کیف اصلی ادغام، * بعد موجودی مؤثر (جمع) روی $wallet برای چک خرید/فروش. * * @return array{0: Wallet|null, 1: WalletInterface} */ private function resolveOrderWallet(int $customerId, ?Customer $customer = null, ?int $ownerType = null): array { $walletOwnerType = $ownerType ?? (int) ($customer?->type ?? 2); $balanceService = app(WalletBalanceService::class); $balanceService->consolidateAllMainWalletProductsForOwner($customerId, $walletOwnerType); $walletInterface = new WalletInterface($customerId, $walletOwnerType); $wallet = Wallet::query() ->where('owner', $customerId) ->where('owner_type', $walletOwnerType) ->whereNull('fund_id') ->whereNull('product_id') ->first(); if ($wallet === null) { $wallet = $walletInterface->wallet; } else { $wallet->refresh(); } if ($wallet && $balanceService->mainWalletProductIds() !== []) { $wallet->cash = $balanceService->effectiveMainWalletCash($customerId, $walletOwnerType); $wallet->gold = $balanceService->effectiveMainWalletGold($customerId, $walletOwnerType); } return [$wallet, $walletInterface]; } /** * کیف مؤثر محصول برای تراکنش سفارش. * محصولات متصل به کیف اصلی (main_wallet_products) مستقیماً روی کیف اصلی می‌نشینند. */ private function productWalletInterface(int $productId): WalletInterface { $customer = auth('customer')->user(); $customerId = (int) ($customer?->id ?? 0); $walletOwnerType = (int) ($customer?->type ?? 2); if ($customerId > 0 && self::isMainWalletProduct($productId)) { app(WalletBalanceService::class)->consolidateOrphanedForOwner( $customerId, $walletOwnerType, $productId ); } return new WalletInterface( $customerId, $walletOwnerType, null, self::effectiveProductWalletId($productId) ); } /** * رُند به عدد صحیح فقط برای محصول تعدادی (سکه/شمش قطعه‌ای). * روی طلای وزنی/اعشاری و کیف اصلی هرگز اعمال نشود — همان باگ از دست رفتن ۰.۹۱۳ و ۰.۳۹۱. */ private function normalizeProductWalletGold(WalletInterface $walletInterface): void { $wallet = $walletInterface->wallet; if (!$wallet || $wallet->product_id === null) { return; } $product = Product::query()->find((int) $wallet->product_id); if (!$product || !self::isCountBasedProduct(self::productUnitPayload($product))) { return; } $wallet->gold = intval($wallet->gold); $wallet->save(); } private function give_order_reward(&$data, $wallet, $wallet_interface) { if (!app(\App\Services\ConfigService::class)->getBool('enable_customer_level_profit', false)) { return $data; } $serviceCharge = (float) ($data['order']['service_charge'] ?? 0); if ($serviceCharge <= 0) { return $data; } $data['aCustomers'] = []; $i = 0; $data['aCustomers'][$i] = Customer::findOrFail($data['customer_id']); while ($data['aCustomers'][$i + 1] = Customer::where('referral_id', $data['aCustomers'][$i]->referral_to_id)->first()) { $levels = $this->parseProfitAmountByLevel($data['aCustomers'][$i + 1]->profit_amount_by_lvl); foreach ($levels as $levelRow) { $levelNo = (int) ($levelRow['level'] ?? $levelRow[0] ?? 0); $percent = (float) ($levelRow['present'] ?? $levelRow[1] ?? 0); if ($levelNo !== ($i + 1) || $percent <= 0) { continue; } $gift_for_invite = ($serviceCharge * $percent) / 100; if ($gift_for_invite <= 0) { continue; } $profit_receiver = $data['aCustomers'][$i + 1]->id; $profit_sender = $data['aCustomers'][0]->id; $Withdraw = Withdraw::create([ 'user_id' => $profit_sender, 'customer_id' => $profit_receiver, 'cash' => 0, 'gold' => $gift_for_invite, 'sts' => 9, ]); $wallet_interface = new WalletInterface($profit_receiver, 2); new TransactionInterface($wallet_interface, [ 'cash' => 0, 'gold' => $gift_for_invite, 'expire_at' => now(), 'active' => true, 'withdraw_id' => $Withdraw->id, 'start_at' => now(), ], 9, true, false); $transaction = Transaction::where('customer_id', $profit_receiver)->latest()->first(); TransactionsAdmin::create([ 'customer_id' => $profit_sender, 'customer_to_id' => $profit_receiver, 'user_id' => $profit_sender, 'order_id' => $data['order']['id'], 'wallet' => null, 'transaction_id' => $transaction->id, 'cash' => 0, 'note' => ' سود بابت سفارش', 'type' => 1, 'gold' => $gift_for_invite, 'balance_cash' => 0, 'balance_gold' => $gift_for_invite, 'withdraw_id' => $Withdraw->id, ]); } if ($i == 100) { break; } $i++; } return $data; } /** @return list */ private function parseProfitAmountByLevel(mixed $raw): array { if (is_array($raw)) { return $raw; } if (!is_string($raw) || trim($raw) === '') { return []; } $decoded = json_decode($raw, true); if (is_array($decoded)) { return $decoded; } // فرمت قدیمی: رشته شبیه {1,5},{2,2} $legacy = json_decode(str_replace(['{', '}'], ['[', ']'], $raw), true); return is_array($legacy) ? $legacy : []; } private function sendTransactionNotifications(array $data, string $transactionType) { $customer = Customer::findOrFail($data['customer_id']); $companyName = $this->Config('name_fa'); $amount = $data['amount'] ?? ( isset($data['number'], $data['product']['amount']) ? $data['number'] * $data['product']['amount'] : 0 ); $totalPrice = $data['total_price'] ?? ($data['payment_amount'] ?? 0); $subjectTitle = $this->transactionSubjectTitle($data); $amountLabel = self::formatSmsGoldWeight($amount); $message = "کاربر {$customer->name} با شماره موبایل {$customer->mobile} اقدام به $transactionType {$subjectTitle} به مقدار " . "{$amountLabel} گرم به مبلغ " . number_format($totalPrice) . " ریال کرد. با تشکر، $companyName"; if (self::isSmsNotifEnabled('sms_notif_order_admin')) { User::where('send_sms', 1)->get()->each(function ($admin) use ($message) { $this->send($admin->mobile, $message); }); } $wallet = Wallet::where('owner', $data['customer_id']) ->whereNull('fund_id') ->whereNull('product_id') ->first(); $balanceLabel = self::metalTypeLabel(); $goldBalance = $wallet ? ($wallet->gold ?? 0) : 0; $cashBalance = $wallet ? ($wallet->cash ?? 0) : 0; $goldBalanceLabel = self::formatSmsGoldWeight($goldBalance); $customerMessage = "$companyName\n$transactionType {$subjectTitle}\nگرم: {$amountLabel}\n" . "مبلغ کل: " . number_format($totalPrice) . "\n\n" . "موجودی {$balanceLabel}: {$goldBalanceLabel}\n" . "مانده ریالی: " . number_format($cashBalance) . "\n" . self::publicSiteUrl(request()); if (self::isSmsNotifEnabled('sms_notif_order_customer')) { $this->send($customer->mobile, $customerMessage); } try { app(\App\Services\CustomerAppNotifyService::class)->notifyCustomer( (int) $customer->id, $transactionType . ' ' . $subjectTitle, "گرم: {$amount}\nمبلغ کل: " . number_format((float) $totalPrice) . " ریال", 'success' ); } catch (\Throwable $e) { report($e); } $event = (int) ($data['direction'] ?? 0) === 2 ? 'order.sell_success' : 'order.buy_success'; \App\Services\Automation\AutomationEvents::fire($event, (int) $customer->id); } private function assertProviderOrderAccepted(array &$data): void { if (!app(ConfigService::class)->usesMontzarPricing()) { return; } $this->assertNoRecentDuplicateLocalOrder($data); $data['_montzar_parsed'] = app(MontzarService::class)->submitOrder($data); } private function finalizeProviderOrder(array &$data): void { if (!app(ConfigService::class)->usesMontzarPricing()) { return; } $parsed = $data['_montzar_parsed'] ?? null; if (!is_array($parsed)) { return; } app(MontzarService::class)->applyParsedOrder($data, $parsed); unset($data['_montzar_parsed']); } private function assertNoRecentDuplicateLocalOrder(array $data): void { $customerId = (int) ($data['customer_id'] ?? 0); if ($customerId <= 0) { return; } $direction = (int) ($data['direction'] ?? 0); $amount = round((float) ($data['amount'] ?? 0), 6); $windowSeconds = 20; $duplicate = Order::where('customer_id', $customerId) ->where('direction', $direction) ->where('created_at', '>=', now()->subSeconds($windowSeconds)) ->whereRaw('ROUND(amount, 6) = ?', [$amount]) ->exists(); if ($duplicate) { throw new UnprocessableEntityHttpException( 'در حال حاضر سفارش مشابه ثبت شده است. لطفاً چند ثانیه دیگر تلاش کنید.' ); } } public function storeBackUp(Request $request) { $data = $request->all(); if (auth('customer')->id()) {// متد در پنل کاربر $data['customer_id'] = auth('customer')->id(); $data['price'] = Price::orderBy('id', 'desc')->first()->price; } elseif (auth('user')->user()) {// متد در پنل ادمین $data['customer_id'] = $request->customer_id; $data['price'] = $request->price; } $data['sts'] = 2; $data['type'] = 2; $data['payment_sts'] = 4; $data['price_original'] = $data['price']; $add_sell_price = config('add_sell_price.single'); $customer = Customer::findOrFail($data['customer_id']); $wallet_interface = new WalletInterface($data['customer_id'], (int) $customer->type); $wallet = $wallet_interface->wallet; $data['total_price_original'] = $data['amount'] * $data['price_original']; $data['payment_remind'] = 0; $data['fee'] = $add_sell_price; $data['total_fee'] = $data['amount'] * $data['fee']; if ($data['direction'] == 1) { $data['price'] += $add_sell_price; $data['total_price'] = $data['amount'] * $data['price']; $data['payment_amount'] = $data['total_price']; if ($wallet->cash < $data['total_price']) { throw new UnprocessableEntityHttpException('موجودی شما کمتر از میزان سفارش است'); } $this->syncOrderProfitFields($data); $order = Order::create($data);//buy new TransactionInterface($wallet_interface, $_inp = [ "cash" => $data['total_price'], "gold" => $data['amount'], 'expire_at' => now(), 'active' => true, 'order_id' => $order->id, 'start_at' => now(), ], 1, true, false); } else {//sell $data['price'] -= $add_sell_price; $data['total_price'] = $data['amount'] * $data['price']; $data['payment_amount'] = $data['total_price']; if ($wallet->gold < $data['amount']) { throw new UnprocessableEntityHttpException('موجودی شما کمتر از میزان سفارش است'); } $this->syncOrderProfitFields($data); $order = Order::create($data); new TransactionInterface($wallet_interface, $_inp = [ "gold" => $data['amount'], "cash" => $data['total_price'], 'expire_at' => now(), 'active' => true, 'order_id' => $order->id ], 2, false, true); } //----------------------------------------------------------------------------sms // Determine transaction type $transactionType = ($data['direction'] == 1) ? "خرید" : "فروش"; // User information $customer = Customer::where("id", $data['customer_id'])->firstOrFail(); $userMobile = $customer->mobile; // User's mobile number $userName = $customer->name; // User's name $chargedAmount = $data['total_price']; // Total price in Rial $chargedAmountGold = self::formatSmsGoldWeight($data['amount']); // Amount of gold // Company name $companyName = $this->Config('name_fa'); // Company name from configuration // Constructing the SMS message $message = "کاربر $userName با شماره موبایل $userMobile اقدام به $transactionType طلا به مقدار " . $chargedAmountGold . " گرم به مبلغ " . number_format($chargedAmount) . " ریال کرد. با تشکر، $companyName"; // Getting admin mobile numbers and sending the SMS User::where('active', 1)->get()->each(function ($admin) use ($message) { $this->Send($admin->mobile, $message); }); $mainWallet = Wallet::where('owner', $data['customer_id']) ->where('owner_type', $customer->type) ->main() ->first(); $companyurl = self::publicSiteUrl(request()); $weight = (float) $data['amount']; $weightLabel = self::formatSmsGoldWeight($weight); $totalAmount = $data['total_price']; $pricePerGram = $weight > 0 ? $totalAmount / $weight : 0; $goldBalance = self::formatSmsGoldWeight($mainWallet->gold ?? 0); $remainingBalance = $mainWallet->cash ?? 0; $message = "$companyName\n"; $message .= "خرید آبشده\n"; $message .= "گرم: $weightLabel\n"; $message .= "قیمت گرم: " . number_format($pricePerGram) . "\n"; $message .= "مبلغ کل: " . number_format($totalAmount) . "\n\n"; $message .= "موجودی طلایی: $goldBalance\n"; $message .= "مانده ریالی: " . number_format($remainingBalance) . "\n"; $message .= "$companyurl" . "\n"; $this->Send($customer->mobile, $message); return response()->json($order); } public function table(Request $request) { $records = Order::where('customer_id', auth('customer')->id()) ->where('type', 2); TableEngine::create($records, $request->all()) ->initSearch([]) ->select([ '*', Order::$SELECT_STS_STR ])->export($records); return response()->json($records); } private function feeFree($customer_id, $count) { return Order::where('customer_id', $customer_id) ->where('type', 2) ->where('sts', 2) ->where('payment_sts', 4) ->count() < (int)$count; } private function resolveOrderAmount(array $data): ?float { if (isset($data['amount'])) { return (float) $data['amount']; } if (!empty($data['product_id']) && isset($data['number'])) { $product = $data['product'] ?? Product::find($data['product_id']); if ($product) { return $this->resolveProductOrderPricingAmount($product, (float) $data['number']); } } return null; } /** * مقدار ضرب‌شونده در قیمت واحد برای مبلغ سفارش. * قیمت فرمولی = قیمت هر عدد (وزن داخل فرمول است) → فقط تعداد. * بقیه = تعداد × وزن واحد محصول. */ private function resolveProductOrderPricingAmount(Product|array|null $product, float $number): float { if ($product instanceof Product && FormulaPriceService::isEnabled($product)) { return $number; } if (is_array($product) && ! empty($product['price_formula_enabled'])) { return $number; } $unitAmount = $product instanceof Product ? (float) ($product->amount ?? 0) : (float) (is_array($product) ? ($product['amount'] ?? 0) : 0); return $number * $unitAmount; } private function assertMinimumSellCash(array $data): void { $minSell = Setting::where('key', 'minimum_sell')->where('is_active', 1)->first(); if (!$minSell) { return; } $minRial = (float) $minSell->value; if ($minRial <= 0) { return; } $sellCash = $this->resolveSellOrderCashRial($data); if ($sellCash === null || $sellCash >= $minRial) { return; } throw new UnprocessableEntityHttpException( 'حداقل مبلغ فروش ' . number_format((int) round($minRial)) . ' ریال می‌باشد' ); } /** * قیمت پایهٔ هر گرم/واحد برای سفارش محصول — اول price_original (زنده)، بعد DB. */ private function resolveProductOrderUnitPrice(array $data): float { $base = (float) ($data['price_original'] ?? 0); if ($base > 0) { return $base; } $product = $data['product'] ?? null; if ($product instanceof Product) { return (float) (($product->price ?? 0) + ($product->price_profit ?? 0)); } if (is_array($product)) { return (float) (($product['price'] ?? 0) + ($product['price_profit'] ?? 0)); } return (float) ($data['price'] ?? 0); } private function resolveSellOrderCashRial(array $data): ?float { $targetCash = $this->resolveTargetCash($data); if ($targetCash !== null) { return (float) $targetCash; } if (!empty($data['product_id']) && isset($data['number'])) { $product = $data['product'] ?? Product::find($data['product_id']); if ($product) { $difference = (float) ($data['difference'] ?? 0); $amount = $this->resolveProductOrderPricingAmount($product, (float) $data['number']); $sellUnitPrice = max(0.0, $this->resolveProductOrderUnitPrice($data) - $difference); return $amount * $sellUnitPrice; } } if (isset($data['amount'])) { $amount = (float) $data['amount']; if ($amount > 0) { $difference = (float) ($data['difference'] ?? 0); $sellUnitPrice = max(0.0, $this->resolveProductOrderUnitPrice($data) - $difference); return $amount * $sellUnitPrice; } } return null; } private function resolveTargetCash(array $data): ?int { if (!isset($data['target_cash'])) { return null; } $target = (int) round((float) $data['target_cash']); return $target > 0 ? $target : null; } private function normalizeGoldAmount(float $amount): float { return round($amount, 10); } /** مظنهٔ زندهٔ هر گرم (ریال) — برای تشخیص قیمت مثقال به‌جای گرم */ private function liveGramQuoteRial(): float { $base = (float) app(ProductDisplayPriceService::class)->resolveAppBasePrice(); if ($base > 0) { return $base; } $montzar = (float) app(ConfigService::class)->get('montzar_live_price', 0); if ($montzar > 0) { return $montzar; } return (float) (Price::latest()->first()?->price ?? 0); } /** * اگر قیمت واحد سفارش حدود قیمت مثقال باشد، به قیمت هر گرم برگردان. * جلوگیری از ۱۰ میلیون تومان → ۱۰۵ سوت وقتی مثقال در فیلد «هر گرم» نشسته. */ private function ensureOrderUnitPriceIsPerGram(array &$data): void { $live = $this->liveGramQuoteRial(); $config = app(ConfigService::class); $price = $config->gramUnitPriceIfMesghalQuoted((float) ($data['price'] ?? 0), $live); if ($price <= 0 || abs($price - (float) ($data['price'] ?? 0)) < 1) { return; } $data['price'] = $price; if (isset($data['price_original'])) { $data['price_original'] = $config->gramUnitPriceIfMesghalQuoted( (float) $data['price_original'], $live ); } } private function resolveGoldPaymentProductId(array $data): ?int { if (!empty($data['payment_product_id'])) { return (int) $data['payment_product_id']; } $method = (string) ($data['method'] ?? ''); if (preg_match('/^gold:(\d+)$/', $method, $matches)) { return (int) $matches[1]; } return null; } private function resolveAbshodeProductContext(array $data): array { $productId = (int) ($data['product_id'] ?? $data['abshode_id'] ?? 0); $product = null; if (!empty($data['product'])) { $candidate = $data['product']; if ($candidate instanceof Product) { $product = $candidate; } elseif (is_array($candidate) && !empty($candidate['id'])) { $product = Product::find($candidate['id']); } } if (!$product && $productId > 0) { $product = Product::find($productId); } if (!$product) { return ['id' => 0, 'title' => '']; } return [ 'id' => (int) $product->id, 'title' => trim((string) $product->title), ]; } private function productTitleFromData(array $data): ?string { $product = $data['product'] ?? null; $title = null; if ($product) { if (is_array($product)) { $title = $product['title'] ?? null; } else { $title = $product->title ?? null; } } $title = trim((string) $title); if ($title === '' && !empty($data['product_id'])) { $title = trim((string) Product::query()->whereKey((int) $data['product_id'])->value('title')); } return $title !== '' ? $title : null; } private function isProductOrder(array $data): bool { return !empty($data['product_id']) && $this->productTitleFromData($data) !== null; } private function transactionSubjectTitle(array $data): string { return $this->productTitleFromData($data) ?? self::meltedGoldLabel(); } private function meltedOrProductBuyTitle(array $data): string { $productTitle = $this->productTitleFromData($data); return $productTitle !== null ? self::productBuyTitle($productTitle) : self::meltedGoldBuyTitle(); } private function meltedOrProductSellTitle(array $data): string { $productTitle = $this->productTitleFromData($data); return $productTitle !== null ? self::productSellTitle($productTitle) : self::meltedGoldSellTitle(); } private function insufficientGoldBalanceMessage(array $data): string { return 'موجودی ' . self::metalTypeLabel() . ' شما کمتر از میزان سفارش است'; } /** * اختلاف قیمت سفارش: * - محصول با «قیمت اپ»: buy_fee / sell_fee + تب اختلاف محصول * - بدون قیمت اپ: فقط تب اختلاف محصول (cash_fee) * * @return array{ * amount: float, * source: string, * key: string, * label: string, * uses_app_price: bool, * base_price: float, * lines: list> * } */ private function resolveCashPriceDiffMeta( ConfigService $config, Product $product, int $direction, ?float $basePriceRial = null ): array { $display = app(ProductDisplayPriceService::class); $usesAppPrice = $display->usesAppPrice($product); $isBuy = $direction === 1; $side = $isBuy ? 'خرید' : 'فروش'; $base = (float) ($basePriceRial ?? 0); if ($base <= 0) { $base = (float) $this->resolveProductUnitBasePrice($product, $direction); } if ($base <= 0) { $base = ProductPriceService::productBasePriceRial($product); } $lines = []; $amount = 0.0; $keys = []; if ($usesAppPrice) { if ($isBuy) { $settingsAmt = $config->displayBuyPriceDiffRial(); $settingsKey = 'buy_fee'; $settingsTitle = 'اختلاف پایه اپ · اضافه قیمت خرید (buy_fee)'; } else { $settingsAmt = $config->displaySellPriceDiffRial(); $settingsKey = 'sell_fee'; $settingsTitle = 'اختلاف پایه اپ · کسر قیمت فروش (sell_fee)'; } if ($settingsAmt > 0) { $amount += $settingsAmt; $keys[] = $settingsKey; $lines[] = [ 'kind' => 'app_spread', 'key' => $settingsKey, 'label' => $settingsTitle, 'is_percent' => false, 'raw' => $settingsAmt, 'per_unit' => $settingsAmt, 'detail' => number_format($settingsAmt) . ' ریال / گرم', ]; } } $productAmt = ProductPriceService::resolveCashFeeRial($product, $direction, $base); $productKey = $isBuy ? 'cash_fee_buy' : 'cash_fee_sell'; $isPercent = $isBuy ? (bool) ($product->cash_fee_buy_is_percent ?? false) : (bool) ($product->cash_fee_sell_is_percent ?? false); $raw = (float) ($isBuy ? ($product->cash_fee_buy ?? 0) : ($product->cash_fee_sell ?? 0)); if ($raw != 0.0 || $productAmt > 0) { $amount += $productAmt; $keys[] = $productKey; $rawLabel = $isPercent ? (rtrim(rtrim(number_format($raw, 4, '.', ''), '0'), '.') . '%') : (number_format($raw) . ' ریال'); $detail = $isPercent ? ($rawLabel . ' از ' . number_format($base) . ' ≈ ' . number_format($productAmt) . ' ریال / گرم') : (number_format($productAmt) . ' ریال / گرم'); $lines[] = [ 'kind' => 'product_spread', 'key' => $productKey, 'label' => 'تب اختلاف محصول · هزینه ' . $side, 'is_percent' => $isPercent, 'raw' => $raw, 'percent_of' => $isPercent ? $base : null, 'per_unit' => $productAmt, 'detail' => $detail, ]; } $customerId = (int) (auth('customer')->id() ?? 0); if ($customerId <= 0) { $customerId = (int) (request()->input('customer_id') ?? 0); } $specialSvc = app(\App\Services\CustomerSpecialPricingService::class); $specialRule = $specialSvc->ruleForCustomerProduct($customerId, (int) ($product->id ?? 0)); if ($specialRule && ! ($specialRule['show_product'] ?? true)) { throw new UnprocessableEntityHttpException('این محصول برای شما در دسترس نیست.'); } $specialAmt = $specialSvc->profitForDirection($specialRule, $direction); if ($specialAmt !== 0) { $amount += $specialAmt; $keys[] = 'customer_special_pricing'; $lines[] = [ 'kind' => 'special_pricing', 'key' => 'customer_special_pricing', 'label' => 'قیمت ویژه مشتری · ' . ($specialRule['title'] ?? '') . ' · ' . $side, 'is_percent' => false, 'raw' => $specialAmt, 'per_unit' => $specialAmt, 'detail' => number_format($specialAmt) . ' ریال / گرم', 'package_id' => $specialRule['package_id'] ?? null, ]; } $hasApp = collect($lines)->contains(fn ($l) => ($l['kind'] ?? '') === 'app_spread'); $hasProduct = collect($lines)->contains(fn ($l) => ($l['kind'] ?? '') === 'product_spread'); $hasSpecial = collect($lines)->contains(fn ($l) => ($l['kind'] ?? '') === 'special_pricing'); if ($hasSpecial && ($hasApp || $hasProduct)) { $source = 'app+product+special'; $label = 'اختلاف پایه/محصول + قیمت ویژه مشتری = ' . number_format($amount) . ' ریال / گرم'; } elseif ($hasSpecial) { $source = 'special'; $label = ($lines[array_key_last($lines)]['label'] ?? 'قیمت ویژه مشتری') . ' = ' . number_format($amount) . ' ریال'; } elseif ($hasApp && $hasProduct) { $source = 'app+product'; $label = 'قیمت صفحه اصلی اپلیکیشن + تب اختلاف محصول = ' . number_format($amount) . ' ریال / گرم'; } elseif ($hasApp) { $source = 'settings'; $label = ($lines[0]['label'] ?? 'اختلاف پایه اپ') . ' = ' . number_format($amount) . ' ریال'; } elseif ($hasProduct) { $source = 'product'; $label = ($lines[0]['label'] ?? 'تب اختلاف محصول') . (isset($lines[0]['detail']) ? ' (' . $lines[0]['detail'] . ')' : ''); } else { $source = 'none'; $label = 'بدون اختلاف قیمت'; } return [ 'amount' => $amount, 'source' => $source, 'key' => implode('+', $keys) ?: 'none', 'label' => $label, 'uses_app_price' => $usesAppPrice, 'base_price' => $base, 'lines' => $lines, ]; } private function resolveCashPriceDiff( ConfigService $config, Product $product, int $direction, ?float $basePriceRial = null ): float { return $this->resolveCashPriceDiffMeta($config, $product, $direction, $basePriceRial)['amount']; } /** اختلاف + منبع + اقلام فاکتور را روی $data می‌نشاند. */ private function applyCashPriceDiff( ConfigService $config, Product $product, int $direction, array &$data, ?float $basePriceRial = null ): void { $meta = $this->resolveCashPriceDiffMeta($config, $product, $direction, $basePriceRial); $data['difference'] = $meta['amount']; $data['fee'] = $meta['amount']; $data['fee_source'] = $meta['source']; $data['fee_source_key'] = $meta['key']; $data['fee_source_label'] = $meta['label']; $data['_fee_spread_lines'] = $meta['lines']; $data['_fee_uses_app_price'] = $meta['uses_app_price']; $data['_fee_base_price'] = $meta['base_price']; } /** * قیمت پایهٔ هر واحد محصول (بدون ضرب در وزن — وزن در total_price_original اعمال می‌شود). * - قیمت اپ → مظنهٔ زندهٔ اپ * - reading_method (مثل شمش zaarbox) → همان قیمت enrichشدهٔ لیست/UI، نه ستون کهنه/صفر DB * - بقیه → قیمت ذخیره‌شدهٔ محصول */ private function resolveProductUnitBasePrice(Product $product, ?int $direction = null): int { $display = app(ProductDisplayPriceService::class); // قیمت اپ یا فید مرجع — همان منبع نمایش؛ DB خام برای reading_method اغلب ۰/کهنه است if ($display->usesAppPrice($product) || $product->reading_method) { return (int) round($display->resolveOrderTradePrice($product, $direction)); } return (int) (($product->price ?? 0) + ($product->price_profit ?? 0)); } /** * product_id سفارش را برای ذخیره روی جدول orders تثبیت کن. */ private function ensureOrderProductId(array &$data): void { $productId = (int) ($data['product_id'] ?? $data['abshode_id'] ?? 0); if ($productId <= 0) { $productId = (int) (app(ConfigService::class)->mainProductId() ?? 0); } if ($productId > 0) { $data['product_id'] = $productId; } } /** * سود هر گرم / سود کل را از قیمت نهایی و قیمت بدون سود همگام کن * و فاکتور اقلام (مثل فاکتور رستوران) را کامل کن. */ private function syncOrderProfitFields(array &$data): void { $this->ensureOrderProductId($data); $price = (float) ($data['price'] ?? 0); $priceOriginal = (float) ($data['price_original'] ?? 0); $amount = (float) ($data['amount'] ?? 0); $data['fee'] = abs($price - $priceOriginal); $data['total_price_original'] = $amount * $priceOriginal; $data['total_fee'] = $amount * $data['fee']; $spreadLines = is_array($data['_fee_spread_lines'] ?? null) ? $data['_fee_spread_lines'] : []; $receiptLines = []; $spreadTotal = 0.0; foreach ($spreadLines as $line) { $perUnit = (float) ($line['per_unit'] ?? 0); $total = (int) round($perUnit * $amount); $spreadTotal += $total; $amountLabel = rtrim(rtrim(number_format($amount, 4, '.', ''), '0'), '.'); $receiptLines[] = array_merge($line, [ 'amount_units' => $amount, 'total' => $total, 'line_text' => ($line['detail'] ?? number_format($perUnit) . ' ریال / گرم') . ($amount > 0 ? (' × ' . $amountLabel . ' گرم = ' . number_format($total) . ' ریال') : ''), ]); } // اگر اقلام ذخیره نشده بود، حداقل یک خط از اختلاف واقعی بساز if ($receiptLines === [] && $data['fee'] > 0) { $feeTotal = (int) round((float) $data['total_fee']); $receiptLines[] = [ 'kind' => 'spread', 'key' => $data['fee_source_key'] ?? 'fee', 'label' => $data['fee_source_label'] ?? 'اختلاف قیمت', 'is_percent' => false, 'per_unit' => $data['fee'], 'amount_units' => $amount, 'total' => $feeTotal, 'detail' => number_format($data['fee']) . ' ریال / گرم', 'line_text' => number_format($data['fee']) . ' ریال / گرم × ' . rtrim(rtrim(number_format($amount, 4, '.', ''), '0'), '.') . ' گرم = ' . number_format($feeTotal) . ' ریال', ]; $spreadTotal = $feeTotal; } $scGold = (float) ($data['service_charge'] ?? 0); $scRial = (int) round((float) ($data['service_charge_price'] ?? 0)); $scPct = (float) ($data['service_charge_percentage'] ?? 0); if ($scGold > 0 || $scRial > 0) { $feeAsset = (string) ($data['fee_asset'] ?? 'rial'); $fromMetal = $feeAsset === 'gold'; $assetLabel = $fromMetal ? 'از فلز/مقدار' : 'از ریال'; $goldLabel = rtrim(rtrim(number_format($scGold, 4, '.', ''), '0'), '.') . ' گرم'; $detailParts = []; if ($scPct > 0) { $detailParts[] = rtrim(rtrim(number_format($scPct, 4, '.', ''), '0'), '.') . '%'; } if ($fromMetal && $scGold > 0 && $priceOriginal > 0) { $detailParts[] = $goldLabel . ' × ' . number_format($priceOriginal) . ' = ' . number_format($scRial) . ' ریال'; } elseif ($scRial > 0 && $priceOriginal > 0) { $goldEquiv = $scRial / $priceOriginal; $detailParts[] = number_format($scRial) . ' ریال ≈ ' . rtrim(rtrim(number_format($goldEquiv, 4, '.', ''), '0'), '.') . ' گرم'; } else { $detailParts[] = number_format($scRial) . ' ریال'; } $detailParts[] = $assetLabel; $receiptLines[] = [ 'kind' => 'service_charge', 'key' => 'service_charge', 'label' => 'کارمزد معامله · ' . $assetLabel, 'is_percent' => $scPct > 0, 'raw' => $scPct, 'gold' => $scGold, 'rial' => $scRial, 'fee_asset' => $feeAsset, 'per_unit' => $amount > 0 ? ($scRial / $amount) : 0, 'amount_units' => $amount, 'total' => $scRial, 'detail' => implode(' · ', $detailParts), 'line_text' => implode(' · ', $detailParts), ]; } $baseTotal = (int) round($amount * $priceOriginal); $customerTotal = (int) round((float) ($data['total_price'] ?? ($amount * $price))); $spreadTotal = (int) round($spreadTotal); $grandTotal = $spreadTotal + $scRial; $data['fee_breakdown'] = [ 'uses_app_price' => (bool) ($data['_fee_uses_app_price'] ?? false), 'amount_units' => $amount, 'price_original' => $priceOriginal, 'price_customer' => $price, 'base_total' => $baseTotal, 'customer_total' => $customerTotal, 'spread_per_unit' => (float) $data['fee'], 'spread_total' => $spreadTotal, 'service_charge_gold' => $scGold, 'service_charge_rial' => $scRial, 'grand_total' => $grandTotal, 'lines' => $receiptLines, ]; unset( $data['_fee_spread_lines'], $data['_fee_uses_app_price'], $data['_fee_base_price'] ); } private function bonusGift($customer, $referrer) { $bonusRaw = app(\App\Services\ConfigService::class)->getString('referral_bonus_amount'); if ($bonusRaw === '') { return false; } $bonus = json_decode($bonusRaw, true); $customer_bonus = $bonus[0]['customer'] ?? 0; $referrer_bonus = $bonus[0]['referrer'] ?? 0; if ($customer_bonus > 0) { $wallet_interface = new WalletInterface($customer->id, $customer->type); $Withdraw = Withdraw::create([ 'customer_id' => $customer->id, 'cash' => $customer_bonus, 'note' => 'پاداش اولین سفارش', 'sts' => 4, ]); $transaction = new TransactionInterface($wallet_interface, $_inp = [ 'gold' => 0, 'cash' => $customer_bonus, 'expire_at' => now(), 'note' => 'پاداش اولین سفارش', 'withdraw_id' => $Withdraw->id, 'active' => true, ], 3, true, true, 0); try { app(\App\Services\PargarSyncService::class)->queueWalletDocument( $customer, 'cash', (float) $customer_bonus, [ 'withdraw_id' => $Withdraw->id, 'note' => 'پاداش اولین سفارش', 'source' => 'bonus_first_order', ] ); } catch (\Throwable $e) { report($e); } $message = 'کیف پول شما به اندازه ' . number_format($customer_bonus) . ' ریال در تاریخ ' . verta()->format('Y/n/j H:i:s') . " شارژ شد بابت\n" . 'پاداش اولین سفارش' . ' ' . $this->Config('name_fa'); $this->Send($customer->mobile, $message); TransactionsAdmin::create([ 'customer_id' => $customer->id, 'customer_to_id' => $customer->referral->id ?? null, 'user_id' => $customer->id, 'wallet' => null, 'transaction_id' => $transaction->id ?? null, 'cash' => $customer_bonus, 'note' => 'پاداش خرید اول', 'type' => 1, 'gold' => 0, 'balance_cash' => 0, 'withdraw_id' => $Withdraw->id, ]); } if ($referrer_bonus > 0) { $wallet_interface = new WalletInterface($referrer->id, $referrer->type); $Withdraw = Withdraw::create([ 'customer_id' => $referrer->id, 'cash' => $referrer_bonus, 'note' => 'پاداش اولین سفارش زیرمجموعه', 'sts' => 4, ]); $referrerTransaction = new TransactionInterface($wallet_interface, $_inp = [ 'gold' => 0, 'cash' => $referrer_bonus, 'expire_at' => now(), 'note' => 'پاداش اولین سفارش زیرمجموعه', 'withdraw_id' => $Withdraw->id, 'active' => true, ], 3, true, true, 0); try { app(\App\Services\PargarSyncService::class)->queueWalletDocument( $referrer, 'cash', (float) $referrer_bonus, [ 'withdraw_id' => $Withdraw->id, 'note' => 'پاداش اولین سفارش زیرمجموعه', 'source' => 'bonus_referrer', ] ); } catch (\Throwable $e) { report($e); } $message = 'کیف پول شما به اندازه ' . number_format($referrer_bonus) . ' ریال در تاریخ ' . verta()->format('Y/n/j H:i:s') . " شارژ شد بابت\n" . 'بابت پاداش اولین سفارش زیرمجموعه' . ' ' . $this->Config('name_fa'); $this->Send($referrer->mobile, $message); TransactionsAdmin::create([ 'customer_id' => $customer->id, 'customer_to_id' => $referrer->id, 'user_id' => $customer->id, 'wallet' => null, 'transaction_id' => $referrerTransaction->id ?? null, 'cash' => $referrer_bonus, 'note' => 'پاداش خرید اول زیرمجموعه', 'type' => 1, 'gold' => 0, 'balance_cash' => 0, 'withdraw_id' => $Withdraw->id, ]); } return false; } private function enforceProductOrderLimits(array &$data, ?float $totalPriceRial = null): void { $product = $data['product'] ?? null; if (!$product instanceof Product) { $productId = (int) ($data['abshode_id'] ?? $data['product_id'] ?? 0); if ($productId) { $product = Product::find($productId); if ($product) { $data['product'] = $product; } } } elseif (is_array($product)) { $product = Product::find($product['id'] ?? $data['product_id'] ?? 0); if ($product) { $data['product'] = $product; } } if ($product instanceof Product) { $product->validateOrderCashLimit($this->resolveOrderLimitCashRial($data, $totalPriceRial)); } } /** * مبلغ مؤثر برای min/max محصول — با target_cash همان مبلغی که کاربر وارد کرده * (نه total_price موقت قبل از رُند گرم که ممکن است چند ریال کمتر شود). */ private function resolveOrderLimitCashRial(array $data, ?float $fallbackTotal = null): float { $targetCash = $this->resolveTargetCash($data); if ($targetCash !== null) { return (float) $targetCash; } if ($fallbackTotal !== null) { return $fallbackTotal; } return (float) ($data['payment_amount'] ?? $data['total_price'] ?? 0); } }