Files
boursecu_server/app/Http/Controllers/Single/SingleOrderController.php
T
2026-07-21 13:25:18 +03:30

1624 lines
70 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\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;
$basePrice = app(ProductDisplayPriceService::class)->resolveOrderBasePrice($orderProduct);
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();
$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']);
$data['service_charge_percentage'] = isset($data['fee_free']) && $data['fee_free'] === true ? 0 : ($data['product']['percent_fee_buy']) * $data['number'];
$data['price_original'] = $this->resolveProductUnitBasePrice($data['product']);
$this->applyCashPriceDiff($config, $data['product'], 1, $data, (float) $data['price_original']);
$data['total_fee'] = $data['number'] * $data['fee'];
$data['total_price_original'] = $data['product']['amount'] * ($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) {
$data['service_charge_percentage'] = isset($data['fee_free']) && $data['fee_free'] === true ? 0 : ($data['product']['percent_fee_sell']) * $data['number'];
$data['price_original'] = $this->resolveProductUnitBasePrice($data['product']);
$this->applyCashPriceDiff($config, $data['product'], 2, $data, (float) $data['price_original']);
$data['total_fee'] = $data['number'] * $data['fee'];
$data['total_price_original'] = $data['product']['amount'] * ($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));
$data['service_charge_percentage'] = $data['product']['percent_fee_buy'];
} elseif ($data['direction'] == 2) {
$this->applyCashPriceDiff($config, $data['product'], 2, $data, (float) ($data['price_original'] ?? 0));
$data['service_charge_percentage'] = $data['product']['percent_fee_sell'];
}
// 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->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)
{
$data['price'] = $data['product']['price'] + $data['product']['price_profit'];
$data['price'] += $data['difference'];
$data['amount'] = $data['number'] * $data['product']['amount'];
$data['total_amount'] = $data['amount'] + ($data['product']['percent_fee_buy'] * $data['amount']);
$data['total_price'] = $data['total_amount'] * $data['price'];
$data['payment_amount'] = $data['total_price'];
$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['total_price'] < (int) $minFirstBuy) {
throw new UnprocessableEntityHttpException('حداقل سفارش اول ' . number_format((int) $minFirstBuy) . ' ریال میباشد.');
}
}
$data['service_charge'] = isset($data['fee_free']) && $data['fee_free'] === true ? 0 : ($data['product']['percent_fee_buy'] * $data['amount']);
$data['service_charge_price'] = $data['service_charge'] * $data['price_original'];
$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['total_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 processBuyTransactionProductByGold(&$data, $wallet, $wallet_interface)
{
$data['price'] += $data['difference'];
$data['amount'] = $data['number'] * $data['product']['amount'];
$data['total_amount'] = $data['amount'] + ($data['product']['percent_fee_buy'] * $data['amount']);
$data['total_price'] = $data['total_amount'] * $data['price'];
$data['payment_amount'] = $data['total_price'];
$this->enforceProductOrderLimits($data, (float) $data['total_price']);
$data['service_charge'] = isset($data['fee_free']) && $data['fee_free'] === true ? 0 : ($data['product']['percent_fee_buy'] * $data['amount']);
$data['total_fee'] = 0;
$data['service_charge_price'] = $data['service_charge'] * $data['price_original'];
$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['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
);
}
$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';
$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)
{
$data['price'] = $data['product']['price'] + $data['product']['price_profit'];
$data['price'] -= $data['difference'];
$data['amount'] = $data['number'] * $data['product']['amount'];
$data['total_amount'] = $data['amount'] - ($data['product']['percent_fee_sell'] * $data['amount']);
$data['total_price'] = $data['total_amount'] * $data['price'];
$data['payment_amount'] = $data['total_price'];
$grossTotal = (float) ($data['amount'] * $data['price']);
$this->enforceProductOrderLimits($data, $grossTotal);
$data['service_charge'] = isset($data['fee_free']) && $data['fee_free'] === true ? 0 : ($data['product']['percent_fee_sell'] * $data['amount']);
$data['service_charge_price'] = $data['service_charge'] * $data['price_original'];
$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'] = $data['number'] * $data['product']['amount'];
$data['total_amount'] = $data['amount'] - ($data['product']['percent_fee_sell'] * $data['amount']);
$data['total_price'] = $data['total_amount'] * $data['price'];
$data['payment_amount'] = $data['total_price'];
$grossTotal = (float) ($data['amount'] * $data['price']);
$this->enforceProductOrderLimits($data, $grossTotal);
$data['service_charge'] = isset($data['fee_free']) && $data['fee_free'] === true ? 0 : ($data['product']['percent_fee_sell'] * $data['amount']);
$data['total_fee'] = 0;
$data['service_charge_price'] = $data['service_charge'] * $data['price_original'];
$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);
$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);
$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)
{
$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()) {
if ($data['aCustomers'][$i + 1]->profit_amount_by_lvl) {
$levels = json_decode(str_replace(['{', '}'], ['[', ']'], $data['aCustomers'][$i + 1]->profit_amount_by_lvl), true);
foreach ($levels as $level) {
if ($level[0] == $i + 1) {
// [$level[1],$data['aCustomers'][$i+1]];
if ($this->Config('has_page_earn_money') == true) {
$gift_for_invite = ($data['order']['service_charge'] * $level[1]) / 100;
$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, $_inp = [
"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, // مقدار صحیح برای `balance_gold`
'withdraw_id' => $Withdraw->id,
]);
}
}
}
}
if ($i == 100) break;
$i++; // برای جلوگیری از حلقه بی‌نهایت
}
return $data;
}
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);
$message = "کاربر {$customer->name} با شماره موبایل {$customer->mobile} اقدام به $transactionType {$subjectTitle} به مقدار " .
"{$amount} گرم به مبلغ " . 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;
$customerMessage = "$companyName\n$transactionType {$subjectTitle}\nگرم: {$amount}\n" .
"مبلغ کل: " . number_format($totalPrice) . "\n\n" .
"موجودی {$balanceLabel}: {$goldBalance}\n" .
"مانده ریالی: " . number_format($cashBalance) . "\n" .
self::publicSiteUrl(request());
if (self::isSmsNotifEnabled('sms_notif_order_customer')) {
$this->send($customer->mobile, $customerMessage);
}
}
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 = $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 = $data['amount'];
$totalAmount = $data['total_price'];
$pricePerGram = $weight > 0 ? $totalAmount / $weight : 0;
$goldBalance = $mainWallet->gold ?? 0;
$remainingBalance = $mainWallet->cash ?? 0;
$message = "$companyName\n";
$message .= "خرید آبشده\n";
$message .= "گرم: $weight\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 (float) $data['number'] * (float) $product->amount;
}
}
return null;
}
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)) . ' ریال می‌باشد'
);
}
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) {
$unitPrice = (float) ($product->price + $product->price_profit);
$difference = (float) ($data['difference'] ?? 0);
$amount = (float) $data['number'] * (float) $product->amount;
$sellUnitPrice = max(0.0, $unitPrice - $difference);
return $amount * $sellUnitPrice;
}
}
if (isset($data['amount'])) {
$amount = (float) $data['amount'];
if ($amount > 0) {
$price = (float) ($data['price'] ?? 0);
$difference = (float) ($data['difference'] ?? 0);
$sellUnitPrice = max(0.0, $price - $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() . ' شما کمتر از میزان سفارش است';
}
/**
* اختلاف قیمت سفارش:
* - محصول با «قیمت اپ»: اختلاف پایه تنظیمات (sell_fee/buy_fee/fee_single) + تب اختلاف محصول
* - بدون قیمت اپ: فقط تب اختلاف محصول (cash_fee) — بدون ۱M تنظیمات اپ
*
* @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);
}
if ($base <= 0) {
$base = ProductPriceService::productBasePriceRial($product);
}
$lines = [];
$amount = 0.0;
$keys = [];
if ($usesAppPrice) {
$feeSingle = (float) $config->get('fee_single', 0);
if ($isBuy) {
$buyFee = (float) $config->get('buy_fee', $feeSingle);
$settingsAmt = $buyFee > 0 ? $buyFee : $feeSingle;
$settingsKey = $buyFee > 0 ? 'buy_fee' : 'fee_single';
$settingsTitle = $buyFee > 0
? 'اختلاف پایه اپ · اضافه قیمت خرید (buy_fee)'
: 'اختلاف پایه اپ · اختلاف پایه (fee_single)';
} else {
$sellFee = (float) $config->get('sell_fee', $feeSingle);
$settingsAmt = $sellFee > 0 ? $sellFee : $feeSingle;
$settingsKey = $sellFee > 0 ? 'sell_fee' : 'fee_single';
$settingsTitle = $sellFee > 0
? 'اختلاف پایه اپ · کسر قیمت فروش (sell_fee)'
: 'اختلاف پایه اپ · اختلاف پایه (fee_single)';
}
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,
];
}
$hasApp = collect($lines)->contains(fn ($l) => ($l['kind'] ?? '') === 'app_spread');
$hasProduct = collect($lines)->contains(fn ($l) => ($l['kind'] ?? '') === 'product_spread');
if ($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 اعمال می‌شود).
* محصول با پرچم «قیمت اپ» → قیمت زندهٔ هر گرم اپ (با تعدیل درصدی)؛
* بقیه → همان قیمت ذخیره‌شدهٔ محصول.
*/
private function resolveProductUnitBasePrice(Product $product): int
{
$display = app(ProductDisplayPriceService::class);
if ($display->usesAppPrice($product)) {
return (int) round($display->resolveOrderBasePrice($product));
}
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) {
$goldLabel = rtrim(rtrim(number_format($scGold, 4, '.', ''), '0'), '.') . ' گرم';
$detailParts = [];
if ($scPct > 0) {
$detailParts[] = rtrim(rtrim(number_format($scPct, 4, '.', ''), '0'), '.') . '%';
}
if ($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) . ' ریال';
}
$receiptLines[] = [
'kind' => 'service_charge',
'key' => 'service_charge',
'label' => 'کارمزد معامله',
'is_percent' => $scPct > 0,
'raw' => $scPct,
'gold' => $scGold,
'rial' => $scRial,
'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)
{
$bonus = Setting::where('key', 'referral_bonus_amount')->where('is_active', 1)->first();
if ($bonus === null) {
return false;
}
$bonus = json_decode($bonus->value, 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);
$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);
$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);
}
}