Post Montzar-covered buy/sell to Zarin/Tahesab/Pargar using montzar_account_* settings, and harden public storage URL/MIME handling for uploads. Co-authored-by: Cursor <cursoragent@cursor.com>
1870 lines
80 KiB
PHP
1870 lines
80 KiB
PHP
<?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);
|
||
}
|
||
}
|
||
|
||
if (isset($data['order'])) {
|
||
try {
|
||
app(\App\Services\ColleagueAccountingService::class)->recordCoveredTrade($data);
|
||
} catch (\Throwable $e) {
|
||
report($e);
|
||
}
|
||
}
|
||
|
||
$response['reward'] = $this->give_order_reward($data, $wallet, $wallet_interface);
|
||
if ((int) $data['direction'] === 1 && $customer) {
|
||
$referrer = Customer::where('referral_id', $customer->referral_to_id)->first();
|
||
if (!empty($referrer) && $referrer->first_buy_order == 1 && $customer->first_buy_order !== 1) {
|
||
$customer->first_buy_order = true;
|
||
$customer->save();
|
||
$response['bonus_gift'] = $this->bonusGift($customer, $referrer);
|
||
}
|
||
}
|
||
if ($customer && isset($data['order'])) {
|
||
try {
|
||
app(ReferralGiftService::class)->tryGiveOnFirstOrder($customer);
|
||
} catch (\Throwable $e) {
|
||
report($e);
|
||
}
|
||
}
|
||
|
||
if (isset($data['order'])) {
|
||
try {
|
||
$response['Notifications'] = $this->sendTransactionNotifications($data, $transactionType);
|
||
} catch (\Throwable $e) {
|
||
report($e);
|
||
}
|
||
}
|
||
|
||
if ($customer) {
|
||
app(WalletBroadcastService::class)->broadcastForCustomer(
|
||
(int) $customer->id,
|
||
$ownerType
|
||
);
|
||
}
|
||
|
||
return response()->json([$response, $data]);
|
||
}
|
||
|
||
private function processBuyTransaction(&$data, $wallet, $wallet_interface)
|
||
{
|
||
|
||
$response = [];
|
||
$config = app(ConfigService::class);
|
||
$targetCash = $this->resolveTargetCash($data);
|
||
// منطق واحد و مستقل از fee_deduct_from_gold:
|
||
// • کاربر مبلغ ریالی زده (rialLocked) → دقیقاً همان مبلغ پرداخت میشود و کارمزد از طلا کم میشود.
|
||
// • کاربر وزن زده (gramLocked) → دقیقاً همان وزن دریافت میشود و کارمزد به ریال قابل پرداخت اضافه میشود.
|
||
$rialLocked = $targetCash !== null;
|
||
$feeRate = isset($data['fee_free']) && $data['fee_free'] === true
|
||
? 0.0
|
||
: (float) ($data['service_charge_percentage'] ?? 0);
|
||
$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);
|
||
}
|
||
$event = (int) ($data['direction'] ?? 0) === 2 ? 'order.sell_success' : 'order.buy_success';
|
||
\App\Services\Automation\AutomationEvents::fire($event, (int) $customer->id);
|
||
}
|
||
|
||
private function assertProviderOrderAccepted(array &$data): void
|
||
{
|
||
if (!app(ConfigService::class)->usesMontzarPricing()) {
|
||
return;
|
||
}
|
||
|
||
$this->assertNoRecentDuplicateLocalOrder($data);
|
||
|
||
$data['_montzar_parsed'] = app(MontzarService::class)->submitOrder($data);
|
||
}
|
||
|
||
private function finalizeProviderOrder(array &$data): void
|
||
{
|
||
if (!app(ConfigService::class)->usesMontzarPricing()) {
|
||
return;
|
||
}
|
||
|
||
$parsed = $data['_montzar_parsed'] ?? null;
|
||
if (!is_array($parsed)) {
|
||
return;
|
||
}
|
||
|
||
app(MontzarService::class)->applyParsedOrder($data, $parsed);
|
||
unset($data['_montzar_parsed']);
|
||
}
|
||
|
||
private function assertNoRecentDuplicateLocalOrder(array $data): void
|
||
{
|
||
$customerId = (int) ($data['customer_id'] ?? 0);
|
||
if ($customerId <= 0) {
|
||
return;
|
||
}
|
||
|
||
$direction = (int) ($data['direction'] ?? 0);
|
||
$amount = round((float) ($data['amount'] ?? 0), 6);
|
||
$windowSeconds = 20;
|
||
|
||
$duplicate = Order::where('customer_id', $customerId)
|
||
->where('direction', $direction)
|
||
->where('created_at', '>=', now()->subSeconds($windowSeconds))
|
||
->whereRaw('ROUND(amount, 6) = ?', [$amount])
|
||
->exists();
|
||
|
||
if ($duplicate) {
|
||
throw new UnprocessableEntityHttpException(
|
||
'در حال حاضر سفارش مشابه ثبت شده است. لطفاً چند ثانیه دیگر تلاش کنید.'
|
||
);
|
||
}
|
||
}
|
||
|
||
public function storeBackUp(Request $request)
|
||
{
|
||
$data = $request->all();
|
||
if (auth('customer')->id()) {// متد در پنل کاربر
|
||
$data['customer_id'] = auth('customer')->id();
|
||
$data['price'] = Price::orderBy('id', 'desc')->first()->price;
|
||
} elseif (auth('user')->user()) {// متد در پنل ادمین
|
||
$data['customer_id'] = $request->customer_id;
|
||
$data['price'] = $request->price;
|
||
}
|
||
$data['sts'] = 2;
|
||
$data['type'] = 2;
|
||
$data['payment_sts'] = 4;
|
||
$data['price_original'] = $data['price'];
|
||
$add_sell_price = config('add_sell_price.single');
|
||
$customer = Customer::findOrFail($data['customer_id']);
|
||
$wallet_interface = new WalletInterface($data['customer_id'], (int) $customer->type);
|
||
$wallet = $wallet_interface->wallet;
|
||
$data['total_price_original'] = $data['amount'] * $data['price_original'];
|
||
$data['payment_remind'] = 0;
|
||
$data['fee'] = $add_sell_price;
|
||
$data['total_fee'] = $data['amount'] * $data['fee'];
|
||
|
||
if ($data['direction'] == 1) {
|
||
$data['price'] += $add_sell_price;
|
||
$data['total_price'] = $data['amount'] * $data['price'];
|
||
$data['payment_amount'] = $data['total_price'];
|
||
|
||
if ($wallet->cash < $data['total_price']) {
|
||
throw new UnprocessableEntityHttpException('موجودی شما کمتر از میزان سفارش است');
|
||
}
|
||
$this->syncOrderProfitFields($data);
|
||
$order = Order::create($data);//buy
|
||
new TransactionInterface($wallet_interface, $_inp = [
|
||
"cash" => $data['total_price'],
|
||
"gold" => $data['amount'],
|
||
'expire_at' => now(),
|
||
'active' => true,
|
||
'order_id' => $order->id,
|
||
'start_at' => now(),
|
||
], 1, true, false);
|
||
} else {//sell
|
||
$data['price'] -= $add_sell_price;
|
||
$data['total_price'] = $data['amount'] * $data['price'];
|
||
$data['payment_amount'] = $data['total_price'];
|
||
if ($wallet->gold < $data['amount']) {
|
||
throw new UnprocessableEntityHttpException('موجودی شما کمتر از میزان سفارش است');
|
||
}
|
||
$this->syncOrderProfitFields($data);
|
||
$order = Order::create($data);
|
||
new TransactionInterface($wallet_interface, $_inp = [
|
||
"gold" => $data['amount'],
|
||
"cash" => $data['total_price'],
|
||
'expire_at' => now(),
|
||
'active' => true,
|
||
'order_id' => $order->id
|
||
], 2, false, true);
|
||
}
|
||
|
||
//----------------------------------------------------------------------------sms
|
||
// Determine transaction type
|
||
$transactionType = ($data['direction'] == 1) ? "خرید" : "فروش";
|
||
|
||
// User information
|
||
$customer = Customer::where("id", $data['customer_id'])->firstOrFail();
|
||
$userMobile = $customer->mobile; // User's mobile number
|
||
$userName = $customer->name; // User's name
|
||
$chargedAmount = $data['total_price']; // Total price in Rial
|
||
$chargedAmountGold = self::formatSmsGoldWeight($data['amount']); // Amount of gold
|
||
|
||
// Company name
|
||
$companyName = $this->Config('name_fa'); // Company name from configuration
|
||
|
||
// Constructing the SMS message
|
||
$message = "کاربر $userName با شماره موبایل $userMobile اقدام به $transactionType طلا به مقدار " . $chargedAmountGold . " گرم به مبلغ " . number_format($chargedAmount) . " ریال کرد. با تشکر، $companyName";
|
||
|
||
// Getting admin mobile numbers and sending the SMS
|
||
|
||
User::where('active', 1)->get()->each(function ($admin) use ($message) {
|
||
$this->Send($admin->mobile, $message);
|
||
});
|
||
|
||
$mainWallet = Wallet::where('owner', $data['customer_id'])
|
||
->where('owner_type', $customer->type)
|
||
->main()
|
||
->first();
|
||
$companyurl = self::publicSiteUrl(request());
|
||
$weight = (float) $data['amount'];
|
||
$weightLabel = self::formatSmsGoldWeight($weight);
|
||
$totalAmount = $data['total_price'];
|
||
$pricePerGram = $weight > 0 ? $totalAmount / $weight : 0;
|
||
$goldBalance = self::formatSmsGoldWeight($mainWallet->gold ?? 0);
|
||
$remainingBalance = $mainWallet->cash ?? 0;
|
||
|
||
$message = "$companyName\n";
|
||
$message .= "خرید آبشده\n";
|
||
$message .= "گرم: $weightLabel\n";
|
||
$message .= "قیمت گرم: " . number_format($pricePerGram) . "\n";
|
||
$message .= "مبلغ کل: " . number_format($totalAmount) . "\n\n";
|
||
$message .= "موجودی طلایی: $goldBalance\n";
|
||
$message .= "مانده ریالی: " . number_format($remainingBalance) . "\n";
|
||
$message .= "$companyurl" . "\n";
|
||
$this->Send($customer->mobile, $message);
|
||
return response()->json($order);
|
||
}
|
||
|
||
public function table(Request $request)
|
||
{
|
||
$records = Order::where('customer_id', auth('customer')->id())
|
||
->where('type', 2);
|
||
TableEngine::create($records, $request->all())
|
||
->initSearch([])
|
||
->select([
|
||
'*', Order::$SELECT_STS_STR
|
||
])->export($records);
|
||
return response()->json($records);
|
||
}
|
||
|
||
private function feeFree($customer_id, $count)
|
||
{
|
||
return Order::where('customer_id', $customer_id)
|
||
->where('type', 2)
|
||
->where('sts', 2)
|
||
->where('payment_sts', 4)
|
||
->count() < (int)$count;
|
||
}
|
||
|
||
private function resolveOrderAmount(array $data): ?float
|
||
{
|
||
if (isset($data['amount'])) {
|
||
return (float) $data['amount'];
|
||
}
|
||
|
||
if (!empty($data['product_id']) && isset($data['number'])) {
|
||
$product = $data['product'] ?? Product::find($data['product_id']);
|
||
if ($product) {
|
||
return $this->resolveProductOrderPricingAmount($product, (float) $data['number']);
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* مقدار ضربشونده در قیمت واحد برای مبلغ سفارش.
|
||
* قیمت فرمولی = قیمت هر عدد (وزن داخل فرمول است) → فقط تعداد.
|
||
* بقیه = تعداد × وزن واحد محصول.
|
||
*/
|
||
private function resolveProductOrderPricingAmount(Product|array|null $product, float $number): float
|
||
{
|
||
if ($product instanceof Product && FormulaPriceService::isEnabled($product)) {
|
||
return $number;
|
||
}
|
||
|
||
if (is_array($product) && ! empty($product['price_formula_enabled'])) {
|
||
return $number;
|
||
}
|
||
|
||
$unitAmount = $product instanceof Product
|
||
? (float) ($product->amount ?? 0)
|
||
: (float) (is_array($product) ? ($product['amount'] ?? 0) : 0);
|
||
|
||
return $number * $unitAmount;
|
||
}
|
||
|
||
private function assertMinimumSellCash(array $data): void
|
||
{
|
||
$minSell = Setting::where('key', 'minimum_sell')->where('is_active', 1)->first();
|
||
if (!$minSell) {
|
||
return;
|
||
}
|
||
|
||
$minRial = (float) $minSell->value;
|
||
if ($minRial <= 0) {
|
||
return;
|
||
}
|
||
|
||
$sellCash = $this->resolveSellOrderCashRial($data);
|
||
if ($sellCash === null || $sellCash >= $minRial) {
|
||
return;
|
||
}
|
||
|
||
throw new UnprocessableEntityHttpException(
|
||
'حداقل مبلغ فروش ' . number_format((int) round($minRial)) . ' ریال میباشد'
|
||
);
|
||
}
|
||
|
||
/**
|
||
* قیمت پایهٔ هر گرم/واحد برای سفارش محصول — اول price_original (زنده)، بعد DB.
|
||
*/
|
||
private function resolveProductOrderUnitPrice(array $data): float
|
||
{
|
||
$base = (float) ($data['price_original'] ?? 0);
|
||
if ($base > 0) {
|
||
return $base;
|
||
}
|
||
|
||
$product = $data['product'] ?? null;
|
||
if ($product instanceof Product) {
|
||
return (float) (($product->price ?? 0) + ($product->price_profit ?? 0));
|
||
}
|
||
if (is_array($product)) {
|
||
return (float) (($product['price'] ?? 0) + ($product['price_profit'] ?? 0));
|
||
}
|
||
|
||
return (float) ($data['price'] ?? 0);
|
||
}
|
||
|
||
private function resolveSellOrderCashRial(array $data): ?float
|
||
{
|
||
$targetCash = $this->resolveTargetCash($data);
|
||
if ($targetCash !== null) {
|
||
return (float) $targetCash;
|
||
}
|
||
|
||
if (!empty($data['product_id']) && isset($data['number'])) {
|
||
$product = $data['product'] ?? Product::find($data['product_id']);
|
||
if ($product) {
|
||
$difference = (float) ($data['difference'] ?? 0);
|
||
$amount = $this->resolveProductOrderPricingAmount($product, (float) $data['number']);
|
||
$sellUnitPrice = max(0.0, $this->resolveProductOrderUnitPrice($data) - $difference);
|
||
|
||
return $amount * $sellUnitPrice;
|
||
}
|
||
}
|
||
|
||
if (isset($data['amount'])) {
|
||
$amount = (float) $data['amount'];
|
||
if ($amount > 0) {
|
||
$difference = (float) ($data['difference'] ?? 0);
|
||
$sellUnitPrice = max(0.0, $this->resolveProductOrderUnitPrice($data) - $difference);
|
||
|
||
return $amount * $sellUnitPrice;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private function resolveTargetCash(array $data): ?int
|
||
{
|
||
if (!isset($data['target_cash'])) {
|
||
return null;
|
||
}
|
||
|
||
$target = (int) round((float) $data['target_cash']);
|
||
|
||
return $target > 0 ? $target : null;
|
||
}
|
||
|
||
private function normalizeGoldAmount(float $amount): float
|
||
{
|
||
return round($amount, 10);
|
||
}
|
||
|
||
private function 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);
|
||
}
|
||
}
|