Files
boursecu_server/app/Http/Controllers/Single/SingleOrderController.php
T

1859 lines
80 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Controllers\Single;
use App\DB\Te\TableEngine;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Admin\TahesabController;
use App\Models\Customer;
use App\Models\Order;
use App\Models\Price;
use App\Models\Product;
use App\Models\User;
use App\Mail\YourMailableClass;
use Illuminate\Support\Facades\Mail;
use App\Models\Withdraw;
use Illuminate\Http\Request;
use Rooyesh\Wallet\interfaces\TransactionInterface;
use Rooyesh\Wallet\interfaces\WalletInterface;
use Rooyesh\Wallet\models\Transaction;
use Rooyesh\Wallet\models\Wallet;
use App\Models\TransactionsAdmin;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Illuminate\Support\Facades\Http;
use App\Models\Setting;
use App\Models\Cnt;
use App\Services\MontzarService;
use App\Services\ConfigService;
use App\Services\ProductPriceService;
use App\Services\ProductDisplayPriceService;
use App\Services\FormulaPriceService;
use App\Services\ReferralGiftService;
use App\Services\WalletBroadcastService;
class SingleOrderController extends Controller
{
public function store(Request $request)
{
$data = $request->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;
}
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_interface = new WalletInterface($data['customer_id'], $ownerType);
$wallet = $wallet_interface->wallet;
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);
}
}
$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);
$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);
$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);
}
/**
* کیف مؤثر محصول برای تراکنش سفارش.
* محصولات متصل به کیف اصلی (main_wallet_products) مستقیماً روی کیف اصلی می‌نشینند.
*/
private function productWalletInterface(int $productId): WalletInterface
{
return new WalletInterface(
auth('customer')->id(),
auth('customer')->user()->type,
null,
self::effectiveProductWalletId($productId)
);
}
/** رفتار قدیمیِ رُند کردن موجودی فقط برای کیف خود محصول؛ کیف اصلی هرگز رُند نمی‌شود */
private function normalizeProductWalletGold(WalletInterface $walletInterface): void
{
if ($walletInterface->wallet->product_id === null) {
return;
}
$walletInterface->wallet->gold = intval($walletInterface->wallet->gold);
$walletInterface->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<array{level?:int,present?:float}|array{0:int,1:float}> */
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);
}
}
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 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<array<string, mixed>>
* }
*/
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);
}
}