2026-01-24 14:03:11 +00:00
<? 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 ;
2026-02-09 14:09:19 +03:30
use App\Models\Setting ;
2026-03-10 15:39:30 +03:30
use App\Models\Cnt ;
2026-05-25 12:47:04 +03:30
use App\Services\MontzarService ;
use App\Services\ConfigService ;
2026-07-04 18:00:57 +03:30
use App\Services\ProductPriceService ;
2026-06-28 10:46:09 +03:30
use App\Services\ProductDisplayPriceService ;
2026-08-10 16:47:29 +03:30
use App\Services\FormulaPriceService ;
2026-07-01 22:27:51 +03:30
use App\Services\ReferralGiftService ;
2026-08-27 14:01:34 +03:30
use App\Services\WalletBalanceService ;
2026-06-27 15:51:44 +03:30
use App\Services\WalletBroadcastService ;
2026-01-24 14:03:11 +00:00
class SingleOrderController extends Controller
{
public function store ( Request $request )
{
2026-04-04 07:47:17 +00:00
$data = $request -> all ();
2026-06-22 19:14:24 +03:30
$data [ 'payment_product_id' ] = $this -> resolveGoldPaymentProductId ( $data );
if ( preg_match ( '/^gold:\d+$/' , ( string ) ( $data [ 'method' ] ?? '' ))) {
$data [ 'method' ] = 'gold' ;
}
2026-06-03 19:53:14 +03:30
$data [ 'is_katod' ] = ( $data [ 'method' ] ?? null ) === 'katod' ;
if ( $data [ 'is_katod' ]) {
2026-06-03 19:45:08 +03:30
$data [ 'method' ] = 'gold' ;
}
2026-05-25 12:47:04 +03:30
$config = app ( ConfigService :: class );
2026-04-04 07:47:17 +00:00
2026-06-28 15:16:39 +03:30
// فیلتر ساعت معامله per-product (سراسری یا override محصول) پایینتر در assertProductTradingOpen اعمال میشود.
2026-06-14 22:00:21 +03:30
2026-01-24 14:03:11 +00:00
// throw new UnprocessableEntityHttpException('به دلیل نوسانات سفارشات بسته میباشد.');
$transactionType = $data [ 'direction' ] == 1 ? "خرید" : "فروش" ;
2026-06-05 16:24:15 +03:30
$customer = Customer :: find ( auth ( 'customer' ) -> id () ?? ( $data [ 'customer_id' ] ?? null ));
$data [ 'first_buy' ] = auth ( 'customer' ) -> id () && $customer && $customer -> first_buy_order !== 1 ;
2026-01-24 14:03:11 +00:00
if ( auth ( 'customer' ) -> id ()) {
2026-07-16 11:36:57 +03:30
if ( ! $customer || ! $customer -> trade_active ) {
throw new UnprocessableEntityHttpException ( 'امکان سفارش برای شما غیرفعال است.' );
}
2026-07-18 22:22:01 +03:30
self :: assertCustomerIdentityVerified ( $customer );
2026-07-16 11:36:57 +03:30
2026-01-24 14:03:11 +00:00
$data [ 'customer_id' ] = auth ( 'customer' ) -> id ();
2026-05-02 18:47:02 +00:00
2026-06-28 10:46:09 +03:30
$orderProductId = ( int ) ( $data [ 'abshode_id' ] ?? $data [ 'product_id' ] ?? $config -> mainProductId () ?? 0 );
$orderProduct = $orderProductId > 0 ? Product :: find ( $orderProductId ) : null ;
2026-08-10 19:21:56 +03:30
$orderDirection = isset ( $data [ 'direction' ]) ? ( int ) $data [ 'direction' ] : null ;
// پایه وسطِ نرخ خرید/فروش همکار است — سفارش با همان سمتی حساب شود که با همکار معامله میکنیم
$basePrice = app ( ProductDisplayPriceService :: class )
-> resolveOrderTradePrice ( $orderProduct , $orderDirection );
2026-06-28 10:46:09 +03:30
if ( $basePrice <= 0 ) {
2026-06-14 22:00:21 +03:30
if ( $config -> isOutsideTradingHours ()) {
throw new UnprocessableEntityHttpException ( $config -> tradingClosedMessage ());
}
2026-05-02 18:47:02 +00:00
throw new UnprocessableEntityHttpException ( 'عدم ارائه سرویس.' );
}
2026-06-28 10:46:09 +03:30
$latestPrice = Price :: latest () -> first ();
2026-01-24 14:03:11 +00:00
$data [ 'all_price' ] = $latestPrice ;
2026-06-28 10:46:09 +03:30
$data [ 'price' ] = $basePrice ;
$data [ 'price_id' ] = $latestPrice ?-> id ;
2026-08-10 19:21:56 +03:30
$data [ 'difference' ] = $config -> orderPriceDiffRial ( $orderDirection );
2026-07-01 16:33:15 +03:30
$data [ 'service_charge_percentage' ] = $config -> orderServiceChargePercentage ();
2026-02-10 11:19:51 +03:30
$fee_free = Setting :: where ( 'key' , 'fee_free' ) -> where ( 'is_active' , true ) -> value ( 'value' );
2026-03-10 15:39:30 +03:30
if ( $fee_free && $this -> feeFree ( $data [ 'customer_id' ], $fee_free )) {
2026-02-10 11:19:51 +03:30
$data [ 'service_charge_percentage' ] = 0 ;
2026-02-22 15:06:01 +03:30
// $data['difference'] = 0;
2026-02-10 11:19:51 +03:30
$data [ 'fee_free' ] = true ;
}
2026-01-24 14:03:11 +00:00
$data [ 'pay_at' ] = ( new \DateTime ()) -> format ( 'Y-m-d H:i:s' );
} else if ( auth ( 'user' ) -> id ()) { //admin
$data [ 'service_charge_percentage' ] = 0 ;
}
2026-08-29 13:50:12 +03:30
$this -> ensureOrderUnitPriceIsPerGram ( $data );
2026-03-31 18:28:27 +03:30
if ( $data [ 'price' ] === 0 ){
throw new UnprocessableEntityHttpException ( ' سفارشات بسته میباشد.' );
}
2026-01-24 14:03:11 +00:00
$data [ 'sts' ] = 2 ;
$data [ 'type' ] = 2 ;
$data [ 'payment_sts' ] = 4 ;
$data [ 'price_original' ] = $data [ 'price' ];
$data [ 'payment_remind' ] = 0 ;
$data [ 'fee' ] = $data [ 'difference' ];
2026-06-15 11:25:10 +03:30
$ownerType = ( int ) ( $customer ?-> type ?? 2 );
2026-08-27 14:01:34 +03:30
[ $wallet , $wallet_interface ] = $this -> resolveOrderWallet (( int ) $data [ 'customer_id' ], $customer , $ownerType );
2026-05-28 11:53:27 +03:30
if ( ! $wallet ) {
throw new UnprocessableEntityHttpException ( 'کیف پول یافت نشد.' );
}
2026-01-24 14:03:11 +00:00
$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' ])) {
2026-06-14 22:00:21 +03:30
$config -> assertProductTradingOpen ( $data [ 'product' ]);
2026-01-24 14:03:11 +00:00
// Check if the user wants to buy
if ( $data [ 'direction' ] == 1 ) {
2026-07-20 21:29:30 +03:30
$config -> assertMainGoldZeroBeforeOtherProductBuy ( $wallet , $data [ 'product' ]);
2026-07-23 12:28:24 +03:30
// کارمزد فقط از fee_service_charge_percentage (بالا ست شده) — percent_fee_* مربوط به کارمزد نیست
if ( ! empty ( $data [ 'fee_free' ])) {
$data [ 'service_charge_percentage' ] = 0 ;
}
2026-08-10 19:21:56 +03:30
$data [ 'price_original' ] = $this -> resolveProductUnitBasePrice ( $data [ 'product' ], 1 );
2026-07-18 17:37:41 +03:30
$this -> applyCashPriceDiff ( $config , $data [ 'product' ], 1 , $data , ( float ) $data [ 'price_original' ]);
$data [ 'total_fee' ] = $data [ 'number' ] * $data [ 'fee' ];
2026-08-10 16:47:29 +03:30
$data [ 'total_price_original' ] = $this -> resolveProductOrderPricingAmount ( $data [ 'product' ], 1.0 )
* ( $data [ 'price_original' ] ?? 0 );
2026-01-24 14:03:11 +00:00
// Check if the method is cash
if ( $data [ 'method' ] == "cash" ) {
2026-03-10 15:39:30 +03:30
$response [ 'buy' ] = $this -> processBuyTransactionProduct ( $data , $wallet , $wallet_interface );
2026-01-24 14:03:11 +00:00
} // Check if the method is gold
else if ( $data [ 'method' ] == "gold" ) {
$response [ 'buy' ] = $this -> processBuyTransactionProductByGold ( $data , $wallet , $wallet_interface );
2026-06-03 19:41:00 +03:30
} else {
throw new UnprocessableEntityHttpException ( 'روش پرداخت نامعتبر است.' );
2026-01-24 14:03:11 +00:00
}
} // Check if the user wants to sell
else if ( $data [ 'direction' ] == 2 ) {
2026-07-23 12:28:24 +03:30
if ( ! empty ( $data [ 'fee_free' ])) {
$data [ 'service_charge_percentage' ] = 0 ;
}
2026-08-10 19:21:56 +03:30
$data [ 'price_original' ] = $this -> resolveProductUnitBasePrice ( $data [ 'product' ], 2 );
2026-07-18 17:37:41 +03:30
$this -> applyCashPriceDiff ( $config , $data [ 'product' ], 2 , $data , ( float ) $data [ 'price_original' ]);
$data [ 'total_fee' ] = $data [ 'number' ] * $data [ 'fee' ];
2026-08-10 16:47:29 +03:30
$data [ 'total_price_original' ] = $this -> resolveProductOrderPricingAmount ( $data [ 'product' ], 1.0 )
* ( $data [ 'price_original' ] ?? 0 );
2026-01-24 14:03:11 +00:00
2026-07-09 18:17:09 +03:30
$this -> assertMinimumSellCash ( $data );
2026-01-24 14:03:11 +00:00
// 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 );
2026-06-03 19:41:00 +03:30
} else {
throw new UnprocessableEntityHttpException ( 'روش پرداخت نامعتبر است.' );
2026-01-24 14:03:11 +00:00
}
}
} else {
// Calculate total price and fee for non-product (gold) transaction
$data [ 'total_price_original' ] = $data [ 'amount' ] * ( $data [ 'price_original' ] ?? 0 );
2026-06-04 14:19:54 +03:30
$mainProductId = app ( \App\Services\ConfigService :: class ) -> mainProductId ();
2026-06-04 13:48:37 +03:30
$productId = $data [ 'abshode_id' ] ?? $data [ 'product_id' ] ?? $mainProductId ;
$data [ 'product' ] = $productId ? Product :: find ( $productId ) : null ;
2026-05-28 11:53:27 +03:30
if ( ! $data [ 'product' ]) {
throw new UnprocessableEntityHttpException ( 'محصول آبشده یافت نشد.' );
}
2026-06-14 22:00:21 +03:30
$config -> assertProductTradingOpen ( $data [ 'product' ]);
2026-06-04 13:48:37 +03:30
if ( $data [ 'direction' ] == 1 ) {
2026-07-18 17:37:41 +03:30
$this -> applyCashPriceDiff ( $config , $data [ 'product' ], 1 , $data , ( float ) ( $data [ 'price_original' ] ?? 0 ));
2026-07-23 12:28:24 +03:30
// کارمزد همان fee_service_charge_percentage — override با percent_fee_* ممنوع
if ( ! empty ( $data [ 'fee_free' ])) {
$data [ 'service_charge_percentage' ] = 0 ;
}
2026-06-04 13:48:37 +03:30
} elseif ( $data [ 'direction' ] == 2 ) {
2026-07-18 17:37:41 +03:30
$this -> applyCashPriceDiff ( $config , $data [ 'product' ], 2 , $data , ( float ) ( $data [ 'price_original' ] ?? 0 ));
2026-07-23 12:28:24 +03:30
if ( ! empty ( $data [ 'fee_free' ])) {
$data [ 'service_charge_percentage' ] = 0 ;
}
2026-06-04 13:48:37 +03:30
}
2026-07-18 16:38:46 +03:30
// fee اولیه از orderPriceDiffRial ممکن است با اختلاف واقعی محصول فرق کند
2026-06-04 13:48:37 +03:30
$data [ 'total_fee' ] = $data [ 'amount' ] * $data [ 'difference' ];
2026-05-28 11:53:27 +03:30
$data [ 'product_id' ] = $data [ 'product' ] -> id ;
2026-01-24 14:03:11 +00:00
// 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 ( 'خرید این محصول در این لحظه مقدور نمیباشد' );
}
2026-07-09 18:17:09 +03:30
$this -> assertMinimumSellCash ( $data );
2026-01-24 14:03:11 +00:00
$response [ 'sell' ] = $this -> processSellTransaction ( $data , $wallet , $wallet_interface );
}
}
2026-06-28 15:16:39 +03:30
// دفترکل داخلی همیشه ثبت شود — مستقل از اتصال به حسابداری بیرونی
if ( isset ( $data [ 'order' ])) {
try {
app ( \App\Services\AccountingLedgerService :: class ) -> recordTrade ( $data [ 'order' ]);
} catch ( \Throwable $e ) {
report ( $e );
}
}
2026-07-05 16:43:34 +03:30
if ( $config -> isTahesabAccounting ()) {
2026-02-09 14:09:19 +03:30
try {
2026-06-25 22:20:40 +03:30
$data [ 'tahesab' ] = app ( TahesabController :: class ) -> storeTrade ( $data );
2026-03-10 15:39:30 +03:30
} catch ( \Exception $e ) {
2026-06-25 22:20:40 +03:30
report ( $e );
2026-02-09 14:09:19 +03:30
}
2026-02-08 08:02:11 +00:00
}
2026-07-05 16:43:34 +03:30
2026-07-23 20:24:58 +03:30
if ( $config -> isZarinAccounting ()) {
try {
$data [ 'zarin' ] = app ( \App\Http\Controllers\Admin\ZarinController :: class ) -> storeTrade ( $data );
} catch ( \Exception $e ) {
report ( $e );
}
}
2026-07-05 16:43:34 +03:30
if ( $config -> isPargarAccounting ()) {
try {
2026-07-20 15:52:42 +03:30
$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 ,
]
);
2026-07-05 16:43:34 +03:30
} catch ( \Throwable $e ) {
report ( $e );
}
}
2026-08-26 16:59:27 +03:30
if ( isset ( $data [ 'order' ])) {
try {
app ( \App\Services\ColleagueAccountingService :: class ) -> recordCoveredTrade ( $data );
} catch ( \Throwable $e ) {
report ( $e );
}
}
2026-08-10 16:48:53 +03:30
$response [ 'reward' ] = $this -> give_order_reward ( $data , $wallet , $wallet_interface );
2026-06-05 16:24:15 +03:30
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 );
}
}
2026-07-08 11:09:11 +03:30
if ( $customer && isset ( $data [ 'order' ])) {
try {
app ( ReferralGiftService :: class ) -> tryGiveOnFirstOrder ( $customer );
} catch ( \Throwable $e ) {
report ( $e );
}
}
2026-06-03 19:41:00 +03:30
if ( isset ( $data [ 'order' ])) {
2026-06-09 10:21:30 +03:30
try {
$response [ 'Notifications' ] = $this -> sendTransactionNotifications ( $data , $transactionType );
} catch ( \Throwable $e ) {
report ( $e );
}
2026-06-03 19:41:00 +03:30
}
2026-06-27 15:51:44 +03:30
if ( $customer ) {
app ( WalletBroadcastService :: class ) -> broadcastForCustomer (
( int ) $customer -> id ,
$ownerType
);
}
2026-01-24 14:03:11 +00:00
return response () -> json ([ $response , $data ]);
}
private function processBuyTransaction ( & $data , $wallet , $wallet_interface )
{
$response = [];
2026-07-01 20:59:24 +03:30
$config = app ( ConfigService :: class );
2026-07-02 20:59:26 +03:30
$targetCash = $this -> resolveTargetCash ( $data );
2026-07-10 22:52:28 +03:30
// منطق واحد و مستقل از 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 );
2026-08-29 13:50:12 +03:30
$this -> ensureOrderUnitPriceIsPerGram ( $data );
2026-01-24 14:03:11 +00:00
$data [ 'price' ] += $data [ 'difference' ];
2026-07-04 12:06:56 +03:30
2026-07-10 22:52:28 +03:30
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 ;
2026-07-04 12:06:56 +03:30
} else {
2026-07-10 22:52:28 +03:30
// وزن دقیق — کارمزد به مبلغ ریالی پرداختی اضافه میشود
2026-07-04 12:06:56 +03:30
$data [ 'amount' ] = $this -> normalizeGoldAmount (( float ) ( $data [ 'amount' ] ?? 0 ));
2026-07-10 22:52:28 +03:30
$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' ];
2026-07-04 12:06:56 +03:30
}
2026-07-10 23:29:36 +03:30
// سمت کسر کارمزد برای گزارش تفکیکشده در داشبورد
$data [ 'fee_asset' ] = $rialLocked ? 'gold' : 'rial' ;
2026-07-10 00:19:30 +03:30
$this -> enforceProductOrderLimits ( $data );
2026-06-06 22:06:53 +03:30
2026-06-05 16:24:15 +03:30
if ( ! empty ( $data [ 'first_buy' ]) && ( $minFirstBuy = Setting :: where ( 'is_active' , 1 ) -> where ( 'key' , 'min_first_buy' ) -> value ( 'value' ))) {
2026-07-10 22:52:28 +03:30
if ( $data [ 'payment_amount' ] < ( int ) $minFirstBuy ) {
2026-06-05 16:24:15 +03:30
throw new UnprocessableEntityHttpException ( 'حداقل سفارش اول ' . number_format (( int ) $minFirstBuy ) . ' ریال میباشد.' );
}
}
2026-01-24 14:03:11 +00:00
if ( $wallet -> cash < $data [ 'payment_amount' ]) {
throw new UnprocessableEntityHttpException ( 'موجودی شما کمتر از میزان سفارش است' );
}
2026-07-02 21:24:26 +03:30
2026-08-31 22:14:36 +03:30
$order = $this -> openOrderThenCoverWithProvider ( $data );
2026-06-09 13:34:43 +03:30
$abshode = $this -> resolveAbshodeProductContext ( $data );
2026-06-22 21:42:48 +03:30
$productId = ( int ) ( $data [ 'product_id' ] ?? $abshode [ 'id' ] ?? 0 );
2026-07-09 17:47:27 +03:30
$usesProductWallet = $productId > 0 && ! $config -> isMainWalletProduct ( $productId );
2026-08-31 22:14:36 +03:30
$this -> runAfterProviderCover ( $order , $data , function () use ( & $data , $wallet_interface , $productId , $usesProductWallet , $order ) {
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 );
2026-07-09 17:47:27 +03:30
}
2026-08-31 22:14:36 +03:30
});
2026-01-24 14:03:11 +00:00
$response [ '1' ] = 2 ;
2026-07-02 21:24:26 +03:30
2026-01-24 14:03:11 +00:00
return $response ;
}
private function processBuyTransactionProduct ( & $data , $wallet , $wallet_interface )
{
2026-07-29 17:00:51 +03:30
// همان قیمت پایهٔ سفارش (price_original / قیمت اپ) — نه ستون کهنهٔ products.price
$data [ 'price' ] = $this -> resolveProductOrderUnitPrice ( $data ) + ( float ) ( $data [ 'difference' ] ?? 0 );
2026-08-10 16:47:29 +03:30
$data [ 'amount' ] = $this -> resolveProductOrderPricingAmount ( $data [ 'product' ], ( float ) $data [ 'number' ]);
2026-07-23 12:28:24 +03:30
// محصول تعدادی: کارمزد فقط از ریال (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' ;
2026-06-05 16:24:15 +03:30
2026-06-06 22:06:53 +03:30
$this -> enforceProductOrderLimits ( $data , ( float ) $data [ 'total_price' ]);
2026-06-05 16:24:15 +03:30
if ( ! empty ( $data [ 'first_buy' ]) && ( $minFirstBuy = Setting :: where ( 'is_active' , 1 ) -> where ( 'key' , 'min_first_buy' ) -> value ( 'value' ))) {
2026-07-23 12:28:24 +03:30
if ( $data [ 'payment_amount' ] < ( int ) $minFirstBuy ) {
2026-06-05 16:24:15 +03:30
throw new UnprocessableEntityHttpException ( 'حداقل سفارش اول ' . number_format (( int ) $minFirstBuy ) . ' ریال میباشد.' );
}
}
2026-01-24 14:03:11 +00:00
$data [ 'wallet' ] = $wallet ;
if ( $wallet -> cash < $data [ 'payment_amount' ]) {
throw new UnprocessableEntityHttpException ( 'موجودی شما کمتر از میزان سفارش است' );
}
if ( ! $data [ 'product' ][ 'open_buy' ]) {
throw new UnprocessableEntityHttpException ( 'خرید این محصول در این لحظه مقدور نمیباشد' );
}
2026-07-18 16:38:46 +03:30
$this -> syncOrderProfitFields ( $data );
2026-01-24 14:03:11 +00:00
$data [ 'order' ] = $order = Order :: create ( $data );
$data [ 'transaction' ][] = new TransactionInterface ( $wallet_interface , $_inp = [
"number" => $data [ 'number' ],
2026-07-23 12:28:24 +03:30
"cash" => $data [ 'payment_amount' ],
2026-01-24 14:03:11 +00:00
"product_id" => $data [ "product_id" ],
'expire_at' => now (),
'active' => true ,
'order_id' => $order -> id ,
'title' => "برداشت وجه نقد"
2026-03-10 15:39:30 +03:30
], 12 , false , false );
2026-06-10 17:31:38 +03:30
$wallet_interface_product = $this -> productWalletInterface (( int ) $data [ "product_id" ]);
$this -> normalizeProductWalletGold ( $wallet_interface_product );
2026-01-24 14:03:11 +00:00
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 (),
2026-06-22 21:42:48 +03:30
'title' => $this -> meltedOrProductBuyTitle ( $data ),
2026-01-24 14:03:11 +00:00
], 1 , true , false );
}
private function processBuyTransactionProductByGold ( & $data , $wallet , $wallet_interface )
{
$data [ 'price' ] += $data [ 'difference' ];
2026-08-10 16:47:29 +03:30
$data [ 'amount' ] = $this -> resolveProductOrderPricingAmount ( $data [ 'product' ], ( float ) $data [ 'number' ]);
2026-07-23 12:28:24 +03:30
// محصول تعدادی: کارمزد از ریال (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' ]);
2026-01-24 14:03:11 +00:00
$data [ 'service_charge_price' ] = $data [ 'service_charge' ] * $data [ 'price_original' ];
2026-07-23 12:28:24 +03:30
$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' ]);
2026-01-24 14:03:11 +00:00
$data [ 'wallet' ] = $wallet ;
2026-06-22 19:14:24 +03:30
$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' ]) {
2026-06-03 19:53:14 +03:30
throw new UnprocessableEntityHttpException ( $this -> insufficientGoldBalanceMessage ( $data ));
2026-01-24 14:03:11 +00:00
}
2026-07-23 12:28:24 +03:30
if ( $data [ 'service_charge_price' ] > 0 && $wallet -> cash < $data [ 'service_charge_price' ]) {
throw new UnprocessableEntityHttpException ( 'موجودی ریالی شما برای کارمزد کافی نیست' );
}
2026-01-24 14:03:11 +00:00
if ( ! $data [ 'product' ][ 'open_buy' ]) {
throw new UnprocessableEntityHttpException ( 'خرید این محصول در این لحظه مقدور نمیباشد' );
}
2026-07-18 16:38:46 +03:30
$this -> syncOrderProfitFields ( $data );
2026-01-24 14:03:11 +00:00
$data [ 'order' ] = $order = Order :: create ( $data );
2026-06-22 19:14:24 +03:30
$paymentSellTitle = $paymentProduct
? self :: productSellTitle ( $paymentProduct -> title ?? '' )
: self :: mainMetalSellTitle ();
$paymentTransaction = [
2026-01-24 14:03:11 +00:00
"number" => $data [ 'number' ],
"gold" => $data [ 'total_amount' ],
'expire_at' => now (),
'active' => true ,
'order_id' => $order -> id ,
2026-06-22 19:14:24 +03:30
'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
);
}
2026-07-23 12:28:24 +03:30
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 );
}
2026-06-10 17:31:38 +03:30
$wallet_interface_product = $this -> productWalletInterface (( int ) $data [ "product_id" ]);
$this -> normalizeProductWalletGold ( $wallet_interface_product );
2026-01-24 14:03:11 +00:00
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 (),
2026-06-22 21:42:48 +03:30
'title' => $this -> meltedOrProductBuyTitle ( $data ),
2026-01-24 14:03:11 +00:00
], 1 , true , false );
}
private function processSellTransaction ( & $data , $wallet , $wallet_interface )
{
$response = [];
2026-07-01 20:59:24 +03:30
$config = app ( ConfigService :: class );
2026-07-04 12:06:56 +03:30
$targetCash = $this -> resolveTargetCash ( $data );
2026-07-10 22:52:28 +03:30
// منطق واحد و مستقل از 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 );
2026-08-29 13:50:12 +03:30
$this -> ensureOrderUnitPriceIsPerGram ( $data );
2026-01-24 14:03:11 +00:00
$data [ 'price' ] -= $data [ 'difference' ];
2026-07-04 12:06:56 +03:30
2026-07-10 22:52:28 +03:30
if ( $rialLocked ) {
// مبلغ ریالی دقیق دریافتی — کارمزد از طلای کیف (کسر اضافهتر) تأمین میشود
$unitPrice = ( float ) $data [ 'price' ];
$data [ 'amount' ] = $unitPrice > 0
? $this -> normalizeGoldAmount ( $targetCash / $unitPrice )
2026-07-04 12:06:56 +03:30
: 0.0 ;
2026-07-10 22:52:28 +03:30
$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' ]);
2026-07-04 12:06:56 +03:30
} else {
2026-07-10 22:52:28 +03:30
// وزن دقیق فروش — کارمزد از مبلغ ریالی دریافتی کم میشود
2026-07-04 12:06:56 +03:30
$data [ 'amount' ] = $this -> normalizeGoldAmount (( float ) ( $data [ 'amount' ] ?? 0 ));
2026-07-10 22:52:28 +03:30
$data [ 'service_charge' ] = $this -> normalizeGoldAmount ( $feeRate * $data [ 'amount' ]);
$data [ 'service_charge_price' ] = $data [ 'service_charge' ] * $data [ 'price_original' ];
2026-07-01 20:59:24 +03:30
$data [ 'total_price' ] = $data [ 'amount' ] * $data [ 'price' ] - $data [ 'service_charge_price' ];
$data [ 'payment_amount' ] = $data [ 'total_price' ];
$goldDebit = $data [ 'amount' ];
}
2026-07-02 20:59:26 +03:30
2026-07-10 23:29:36 +03:30
// سمت کسر کارمزد برای گزارش تفکیکشده در داشبورد
$data [ 'fee_asset' ] = $rialLocked ? 'gold' : 'rial' ;
2026-07-29 19:57:32 +03:30
if ( ! $rialLocked && ( float ) $data [ 'total_price' ] < 0 ) {
throw new UnprocessableEntityHttpException (
'مبلغ دریافتی فروش پس از کارمزد منفی شد. درصد کارمزد سفارش را بررسی کنید (باید اعشاری باشد؛ مثلاً ۰٫۰۱ برای ۱٪).'
);
}
2026-07-10 22:52:28 +03:30
$grossTotal = $data [ 'amount' ] * $data [ 'price' ];
$this -> enforceProductOrderLimits ( $data , ( float ) $grossTotal );
2026-07-02 20:59:26 +03:30
2026-07-09 17:47:27 +03:30
$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 ) {
2026-06-03 19:53:14 +03:30
throw new UnprocessableEntityHttpException ( $this -> insufficientGoldBalanceMessage ( $data ));
2026-01-24 14:03:11 +00:00
}
2026-07-02 21:24:26 +03:30
2026-08-31 22:14:36 +03:30
$order = $this -> openOrderThenCoverWithProvider ( $data );
2026-07-02 21:24:26 +03:30
2026-08-31 22:14:36 +03:30
$this -> runAfterProviderCover ( $order , $data , function () use ( & $data , $wallet_interface , $wallet_interface_product , $productId , $usesProductWallet , $goldDebit , $order ) {
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 );
2026-07-09 17:47:27 +03:30
}
2026-08-31 22:14:36 +03:30
});
2026-01-24 14:03:11 +00:00
}
private function processSellTransactionProduct ( & $data , $wallet , $wallet_interface )
{
2026-07-29 17:00:51 +03:30
// همان قیمت پایهٔ سفارش (price_original / قیمت اپ) — نه ستون کهنهٔ products.price
$data [ 'price' ] = max ( 0.0 , $this -> resolveProductOrderUnitPrice ( $data ) - ( float ) ( $data [ 'difference' ] ?? 0 ));
2026-08-10 16:47:29 +03:30
$data [ 'amount' ] = $this -> resolveProductOrderPricingAmount ( $data [ 'product' ], ( float ) $data [ 'number' ]);
2026-07-23 12:28:24 +03:30
// محصول تعدادی: کارمزد فقط از ریال
$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' ];
2026-01-24 14:03:11 +00:00
$data [ 'payment_amount' ] = $data [ 'total_price' ];
2026-07-23 12:28:24 +03:30
$data [ 'fee_asset' ] = 'rial' ;
2026-07-29 19:57:32 +03:30
if ( $data [ 'total_price' ] < 0 ) {
throw new UnprocessableEntityHttpException (
'مبلغ دریافتی فروش پس از کارمزد منفی شد. درصد کارمزد سفارش را بررسی کنید (باید اعشاری باشد؛ مثلاً ۰٫۰۱ برای ۱٪).'
);
}
2026-06-06 22:06:53 +03:30
$grossTotal = ( float ) ( $data [ 'amount' ] * $data [ 'price' ]);
$this -> enforceProductOrderLimits ( $data , $grossTotal );
2026-06-10 17:31:38 +03:30
$wallet_interface_product = $this -> productWalletInterface (( int ) $data [ "product_id" ]);
2026-01-24 14:03:11 +00:00
if ( $wallet_interface_product -> wallet -> gold < $data [ 'number' ]) {
throw new UnprocessableEntityHttpException ( 'موجودی شما کمتر از میزان سفارش است' );
}
if ( ! $data [ 'product' ][ 'open_sell' ]) {
throw new UnprocessableEntityHttpException ( 'فروش این محصول در این لحظه مقدور نمیباشد' );
}
2026-07-18 16:38:46 +03:30
$this -> syncOrderProfitFields ( $data );
2026-01-24 14:03:11 +00:00
$data [ 'order' ] = $order = Order :: create ( $data );
2026-06-10 17:31:38 +03:30
$this -> normalizeProductWalletGold ( $wallet_interface_product );
2026-01-24 14:03:11 +00:00
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 (),
2026-06-22 21:42:48 +03:30
'title' => $this -> meltedOrProductSellTitle ( $data ),
2026-01-24 14:03:11 +00:00
], 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' => "واریز وجه نقد"
2026-03-10 15:39:30 +03:30
], 12 , false , true );
2026-01-24 14:03:11 +00:00
}
2026-03-10 15:39:30 +03:30
2026-01-24 14:03:11 +00:00
private function processSellTransactionProductByGold ( & $data , $wallet , $wallet_interface )
{
$data [ 'price' ] -= $data [ 'difference' ];
2026-08-10 16:47:29 +03:30
$data [ 'amount' ] = $this -> resolveProductOrderPricingAmount ( $data [ 'product' ], ( float ) $data [ 'number' ]);
2026-07-23 12:28:24 +03:30
// محصول تعدادی: کارمزد فقط از ریال
$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' ];
2026-01-24 14:03:11 +00:00
$data [ 'payment_amount' ] = $data [ 'total_price' ];
2026-07-23 12:28:24 +03:30
$data [ 'fee_asset' ] = 'rial' ;
$data [ 'total_fee' ] = 0 ;
2026-06-06 22:06:53 +03:30
$grossTotal = ( float ) ( $data [ 'amount' ] * $data [ 'price' ]);
$this -> enforceProductOrderLimits ( $data , $grossTotal );
2026-06-10 17:31:38 +03:30
$wallet_interface_product = $this -> productWalletInterface (( int ) $data [ "product_id" ]);
2026-01-24 14:03:11 +00:00
if ( $wallet_interface_product -> wallet -> gold < $data [ 'number' ]) {
throw new UnprocessableEntityHttpException ( 'موجودی شما کمتر از میزان سفارش است' );
}
2026-07-23 12:28:24 +03:30
if ( $data [ 'service_charge_price' ] > 0 && $wallet -> cash < $data [ 'service_charge_price' ]) {
throw new UnprocessableEntityHttpException ( 'موجودی ریالی شما برای کارمزد کافی نیست' );
}
2026-01-24 14:03:11 +00:00
if ( ! $data [ 'product' ][ 'open_sell' ]) {
throw new UnprocessableEntityHttpException ( 'فروش این محصول در این لحظه مقدور نمیباشد' );
}
2026-07-18 16:38:46 +03:30
$this -> syncOrderProfitFields ( $data );
2026-01-24 14:03:11 +00:00
$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 ,
2026-06-22 21:42:48 +03:30
'title' => $this -> meltedOrProductSellTitle ( $data ),
2026-03-10 15:39:30 +03:30
], 12 , true , false );
2026-07-23 12:28:24 +03:30
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 );
}
2026-06-10 17:31:38 +03:30
$this -> normalizeProductWalletGold ( $wallet_interface_product );
2026-01-24 14:03:11 +00:00
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 (),
2026-06-22 21:42:48 +03:30
'title' => $this -> meltedOrProductSellTitle ( $data ),
2026-01-24 14:03:11 +00:00
], 1 , false , false );
}
2026-06-10 17:31:38 +03:30
2026-08-27 14:01:34 +03:30
/**
* کیف سفارش: مثل goldali — اول legacyهای main_wallet_products روی کیف اصلی ادغام،
* بعد موجودی مؤثر (جمع) روی $wallet برای چک خرید/فروش.
*
* @return array{0: Wallet|null, 1: WalletInterface}
*/
private function resolveOrderWallet ( int $customerId , ? Customer $customer = null , ? int $ownerType = null ) : array
{
$walletOwnerType = $ownerType
?? ( int ) ( $customer ?-> type ?? 2 );
$balanceService = app ( WalletBalanceService :: class );
$balanceService -> consolidateAllMainWalletProductsForOwner ( $customerId , $walletOwnerType );
$walletInterface = new WalletInterface ( $customerId , $walletOwnerType );
$wallet = Wallet :: query ()
-> where ( 'owner' , $customerId )
-> where ( 'owner_type' , $walletOwnerType )
-> whereNull ( 'fund_id' )
-> whereNull ( 'product_id' )
-> first ();
if ( $wallet === null ) {
$wallet = $walletInterface -> wallet ;
} else {
$wallet -> refresh ();
}
if ( $wallet && $balanceService -> mainWalletProductIds () !== []) {
$wallet -> cash = $balanceService -> effectiveMainWalletCash ( $customerId , $walletOwnerType );
$wallet -> gold = $balanceService -> effectiveMainWalletGold ( $customerId , $walletOwnerType );
}
return [ $wallet , $walletInterface ];
}
2026-06-10 17:31:38 +03:30
/**
* کیف مؤثر محصول برای تراکنش سفارش.
* محصولات متصل به کیف اصلی (main_wallet_products) مستقیماً روی کیف اصلی مینشینند.
*/
private function productWalletInterface ( int $productId ) : WalletInterface
{
2026-08-27 14:01:34 +03:30
$customer = auth ( 'customer' ) -> user ();
$customerId = ( int ) ( $customer ?-> id ?? 0 );
$walletOwnerType = ( int ) ( $customer ?-> type ?? 2 );
if ( $customerId > 0 && self :: isMainWalletProduct ( $productId )) {
app ( WalletBalanceService :: class ) -> consolidateOrphanedForOwner (
$customerId ,
$walletOwnerType ,
$productId
);
}
2026-06-10 17:31:38 +03:30
return new WalletInterface (
2026-08-27 14:01:34 +03:30
$customerId ,
$walletOwnerType ,
2026-06-10 17:31:38 +03:30
null ,
self :: effectiveProductWalletId ( $productId )
);
}
2026-08-27 14:01:34 +03:30
/**
* رُند به عدد صحیح فقط برای محصول تعدادی (سکه/شمش قطعهای).
* روی طلای وزنی/اعشاری و کیف اصلی هرگز اعمال نشود — همان باگ از دست رفتن ۰.۹۱۳ و ۰.۳۹۱.
*/
2026-06-10 17:31:38 +03:30
private function normalizeProductWalletGold ( WalletInterface $walletInterface ) : void
{
2026-08-27 14:01:34 +03:30
$wallet = $walletInterface -> wallet ;
if ( ! $wallet || $wallet -> product_id === null ) {
2026-06-10 17:31:38 +03:30
return ;
}
2026-08-27 14:01:34 +03:30
$product = Product :: query () -> find (( int ) $wallet -> product_id );
if ( ! $product || ! self :: isCountBasedProduct ( self :: productUnitPayload ( $product ))) {
return ;
}
$wallet -> gold = intval ( $wallet -> gold );
$wallet -> save ();
2026-06-10 17:31:38 +03:30
}
2026-03-10 15:39:30 +03:30
2026-01-24 14:03:11 +00:00
private function give_order_reward ( & $data , $wallet , $wallet_interface )
{
2026-08-10 16:48:53 +03:30
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 ;
}
2026-01-24 14:03:11 +00:00
$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 ()) {
2026-08-10 16:48:53 +03:30
$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 ;
2026-01-24 14:03:11 +00:00
}
2026-08-10 16:48:53 +03:30
$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 ,
]);
2026-01-24 14:03:11 +00:00
}
2026-08-10 16:48:53 +03:30
if ( $i == 100 ) {
break ;
}
$i ++ ;
2026-01-24 14:03:11 +00:00
}
return $data ;
}
2026-08-10 16:48:53 +03:30
/** @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 : [];
}
2026-01-24 14:03:11 +00:00
private function sendTransactionNotifications ( array $data , string $transactionType )
{
$customer = Customer :: findOrFail ( $data [ 'customer_id' ]);
$companyName = $this -> Config ( 'name_fa' );
2026-06-03 19:41:00 +03:30
$amount = $data [ 'amount' ] ?? (
isset ( $data [ 'number' ], $data [ 'product' ][ 'amount' ])
? $data [ 'number' ] * $data [ 'product' ][ 'amount' ]
: 0
);
$totalPrice = $data [ 'total_price' ] ?? ( $data [ 'payment_amount' ] ?? 0 );
2026-06-03 21:37:05 +03:30
$subjectTitle = $this -> transactionSubjectTitle ( $data );
2026-01-24 14:03:11 +00:00
2026-08-10 16:48:53 +03:30
$amountLabel = self :: formatSmsGoldWeight ( $amount );
2026-06-03 21:37:05 +03:30
$message = "کاربر { $customer -> name } با شماره موبایل { $customer -> mobile } اقدام به $transactionType { $subjectTitle } به مقدار " .
2026-08-10 16:48:53 +03:30
" { $amountLabel } گرم به مبلغ " . number_format ( $totalPrice ) .
2026-01-24 14:03:11 +00:00
" ریال کرد. با تشکر، $companyName " ;
2026-07-18 22:22:01 +03:30
if ( self :: isSmsNotifEnabled ( 'sms_notif_order_admin' )) {
User :: where ( 'send_sms' , 1 ) -> get () -> each ( function ( $admin ) use ( $message ) {
$this -> send ( $admin -> mobile , $message );
});
}
2026-06-14 21:09:43 +03:30
$wallet = Wallet :: where ( 'owner' , $data [ 'customer_id' ])
-> whereNull ( 'fund_id' )
-> whereNull ( 'product_id' )
-> first ();
2026-06-03 21:37:05 +03:30
$balanceLabel = self :: metalTypeLabel ();
2026-06-14 21:09:43 +03:30
$goldBalance = $wallet ? ( $wallet -> gold ?? 0 ) : 0 ;
$cashBalance = $wallet ? ( $wallet -> cash ?? 0 ) : 0 ;
2026-08-10 16:48:53 +03:30
$goldBalanceLabel = self :: formatSmsGoldWeight ( $goldBalance );
$customerMessage = " $companyName \n $transactionType { $subjectTitle } \n گرم: { $amountLabel } \n " .
2026-06-03 19:41:00 +03:30
"مبلغ کل: " . number_format ( $totalPrice ) . " \n\n " .
2026-08-10 16:48:53 +03:30
"موجودی { $balanceLabel } : { $goldBalanceLabel } \n " .
2026-06-14 21:09:43 +03:30
"مانده ریالی: " . number_format ( $cashBalance ) . " \n " .
2026-07-21 13:25:18 +03:30
self :: publicSiteUrl ( request ());
2026-01-24 14:03:11 +00:00
2026-07-18 22:22:01 +03:30
if ( self :: isSmsNotifEnabled ( 'sms_notif_order_customer' )) {
$this -> send ( $customer -> mobile , $customerMessage );
}
2026-07-27 22:09:08 +03:30
try {
app ( \App\Services\CustomerAppNotifyService :: class ) -> notifyCustomer (
( int ) $customer -> id ,
$transactionType . ' ' . $subjectTitle ,
"گرم: { $amount } \n مبلغ کل: " . number_format (( float ) $totalPrice ) . " ریال" ,
'success'
);
} catch ( \Throwable $e ) {
report ( $e );
}
2026-08-23 14:16:00 +03:30
$event = ( int ) ( $data [ 'direction' ] ?? 0 ) === 2 ? 'order.sell_success' : 'order.buy_success' ;
\App\Services\Automation\AutomationEvents :: fire ( $event , ( int ) $customer -> id );
2026-01-24 14:03:11 +00:00
}
2026-07-02 21:24:26 +03:30
private function assertProviderOrderAccepted ( array & $data ) : void
2026-05-24 22:29:43 +03:30
{
2026-07-02 21:24:26 +03:30
if ( ! app ( ConfigService :: class ) -> usesMontzarPricing ()) {
2026-05-24 22:29:43 +03:30
return ;
}
2026-08-31 22:14:36 +03:30
if ( ! isset ( $data [ 'order' ])) {
$this -> assertNoRecentDuplicateLocalOrder ( $data );
}
2026-07-02 21:24:26 +03:30
$data [ '_montzar_parsed' ] = app ( MontzarService :: class ) -> submitOrder ( $data );
}
2026-08-31 22:14:36 +03:30
/**
* اول رکورد محلی (در انتظار همکار)، بعد ارسال به لیمان، بعد کیف.
* اگر همکار رد کند یا کیف بترکد، همان رکورد با وضعیت ناموفق و شرح خطا میماند.
*/
private function openOrderThenCoverWithProvider ( array & $data ) : Order
{
$this -> syncOrderProfitFields ( $data );
$montzar = app ( ConfigService :: class ) -> usesMontzarPricing ();
if ( $montzar ) {
$this -> assertNoRecentDuplicateLocalOrder ( $data );
$data [ 'sts' ] = 4 ;
$data [ 'payment_sts' ] = 1 ;
$this -> stampColleagueQuoteOnOrderData ( $data );
}
$order = Order :: create ( $data );
$data [ 'order' ] = $order ;
if ( $montzar ) {
try {
$this -> assertProviderOrderAccepted ( $data );
$this -> finalizeProviderOrder ( $data );
$order -> refresh ();
} catch ( \Throwable $e ) {
$this -> markOrderFailed ( $order , $e -> getMessage ());
throw $e instanceof UnprocessableEntityHttpException
? $e
: new UnprocessableEntityHttpException (
$e -> getMessage () !== '' ? $e -> getMessage () : 'ارسال سفارش به همکار ناموفق بود'
);
}
}
return $order ;
}
private function runAfterProviderCover ( Order $order , array & $data , callable $walletFn ) : void
{
try {
$walletFn ();
$this -> markOrderSucceeded ( $order );
} catch ( \Throwable $e ) {
$this -> markOrderFailed ( $order , $e -> getMessage ());
throw $e instanceof UnprocessableEntityHttpException
? $e
: new UnprocessableEntityHttpException (
$e -> getMessage () !== '' ? $e -> getMessage () : 'ثبت کیف پس از ارسال به همکار ناموفق بود'
);
}
}
private function stampColleagueQuoteOnOrderData ( array & $data ) : void
{
$config = app ( ConfigService :: class );
$live = ( float ) $config -> get ( 'montzar_live_price' , 0 );
$diff = ( float ) $config -> get ( 'montzar_diff' , 0 );
$unit = (( int ) ( $data [ 'direction' ] ?? 0 ) === 1 )
? ( $live + $diff )
: ( $live - $diff );
$data [ 'total_price_montzar' ] = $unit > 0 ? $unit : 0 ;
}
private function markOrderSucceeded ( Order $order ) : void
{
$order -> sts = 2 ;
$order -> payment_sts = 4 ;
$order -> description = null ;
$order -> save ();
}
private function markOrderFailed ( Order $order , string $message ) : void
{
$order -> sts = 3 ;
$order -> payment_sts = 7 ;
$trimmed = trim ( $message );
$order -> description = $trimmed !== '' ? mb_substr ( $trimmed , 0 , 1000 ) : 'ناموفق' ;
$order -> save ();
}
2026-07-02 21:24:26 +03:30
private function finalizeProviderOrder ( array & $data ) : void
{
if ( ! app ( ConfigService :: class ) -> usesMontzarPricing ()) {
return ;
2026-05-24 22:29:43 +03:30
}
2026-07-02 21:24:26 +03:30
$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 )
2026-08-31 22:14:36 +03:30
-> whereIn ( 'sts' , [ 2 , 4 ])
2026-07-02 21:24:26 +03:30
-> where ( 'created_at' , '>=' , now () -> subSeconds ( $windowSeconds ))
-> whereRaw ( 'ROUND(amount, 6) = ?' , [ $amount ])
-> exists ();
if ( $duplicate ) {
throw new UnprocessableEntityHttpException (
'در حال حاضر سفارش مشابه ثبت شده است. لطفاً چند ثانیه دیگر تلاش کنید.'
);
}
2026-05-24 22:29:43 +03:30
}
2026-01-24 14:03:11 +00:00
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' );
2026-06-15 11:25:10 +03:30
$customer = Customer :: findOrFail ( $data [ 'customer_id' ]);
$wallet_interface = new WalletInterface ( $data [ 'customer_id' ], ( int ) $customer -> type );
$wallet = $wallet_interface -> wallet ;
2026-01-24 14:03:11 +00:00
$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 ( 'موجودی شما کمتر از میزان سفارش است' );
}
2026-07-18 16:38:46 +03:30
$this -> syncOrderProfitFields ( $data );
2026-01-24 14:03:11 +00:00
$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 ( 'موجودی شما کمتر از میزان سفارش است' );
}
2026-07-18 16:38:46 +03:30
$this -> syncOrderProfitFields ( $data );
2026-01-24 14:03:11 +00:00
$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
2026-08-10 16:48:53 +03:30
$chargedAmountGold = self :: formatSmsGoldWeight ( $data [ 'amount' ]); // Amount of gold
2026-01-24 14:03:11 +00:00
// 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 );
});
2026-06-15 11:25:10 +03:30
$mainWallet = Wallet :: where ( 'owner' , $data [ 'customer_id' ])
-> where ( 'owner_type' , $customer -> type )
-> main ()
-> first ();
2026-07-21 13:25:18 +03:30
$companyurl = self :: publicSiteUrl ( request ());
2026-08-10 16:48:53 +03:30
$weight = ( float ) $data [ 'amount' ];
$weightLabel = self :: formatSmsGoldWeight ( $weight );
2026-01-24 14:03:11 +00:00
$totalAmount = $data [ 'total_price' ];
2026-06-15 11:25:10 +03:30
$pricePerGram = $weight > 0 ? $totalAmount / $weight : 0 ;
2026-08-10 16:48:53 +03:30
$goldBalance = self :: formatSmsGoldWeight ( $mainWallet -> gold ?? 0 );
2026-06-15 11:25:10 +03:30
$remainingBalance = $mainWallet -> cash ?? 0 ;
2026-01-24 14:03:11 +00:00
$message = " $companyName \n " ;
$message .= "خرید آبشده \n " ;
2026-08-10 16:48:53 +03:30
$message .= "گرم: $weightLabel \n " ;
2026-01-24 14:03:11 +00:00
$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 );
}
2026-02-10 11:19:51 +03:30
private function feeFree ( $customer_id , $count )
{
return Order :: where ( 'customer_id' , $customer_id )
2026-03-10 15:39:30 +03:30
-> where ( 'type' , 2 )
-> where ( 'sts' , 2 )
-> where ( 'payment_sts' , 4 )
-> count () < ( int ) $count ;
2026-02-10 11:19:51 +03:30
}
2026-06-03 19:53:14 +03:30
2026-06-09 09:57:58 +03:30
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 ) {
2026-08-10 16:47:29 +03:30
return $this -> resolveProductOrderPricingAmount ( $product , ( float ) $data [ 'number' ]);
2026-06-09 09:57:58 +03:30
}
}
return null ;
}
2026-08-10 16:48:53 +03:30
/**
* مقدار ضربشونده در قیمت واحد برای مبلغ سفارش.
* قیمت فرمولی = قیمت هر عدد (وزن داخل فرمول است) → فقط تعداد.
* بقیه = تعداد × وزن واحد محصول.
*/
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 ;
}
2026-07-09 18:17:09 +03:30
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 )) . ' ریال میباشد'
);
}
2026-07-29 17:00:51 +03:30
/**
* قیمت پایهٔ هر گرم/واحد برای سفارش محصول — اول 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 );
}
2026-07-09 18:17:09 +03:30
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 );
2026-08-10 16:47:29 +03:30
$amount = $this -> resolveProductOrderPricingAmount ( $product , ( float ) $data [ 'number' ]);
2026-07-29 17:00:51 +03:30
$sellUnitPrice = max ( 0.0 , $this -> resolveProductOrderUnitPrice ( $data ) - $difference );
2026-07-09 18:17:09 +03:30
return $amount * $sellUnitPrice ;
}
}
if ( isset ( $data [ 'amount' ])) {
$amount = ( float ) $data [ 'amount' ];
if ( $amount > 0 ) {
$difference = ( float ) ( $data [ 'difference' ] ?? 0 );
2026-07-29 17:00:51 +03:30
$sellUnitPrice = max ( 0.0 , $this -> resolveProductOrderUnitPrice ( $data ) - $difference );
2026-07-09 18:17:09 +03:30
return $amount * $sellUnitPrice ;
}
}
return null ;
}
2026-07-02 20:59:26 +03:30
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 ;
}
2026-07-04 12:06:56 +03:30
private function normalizeGoldAmount ( float $amount ) : float
{
return round ( $amount , 10 );
}
2026-08-29 13:50:12 +03:30
/** مظنهٔ زندهٔ هر گرم (ریال) — برای تشخیص قیمت مثقال بهجای گرم */
private function liveGramQuoteRial () : float
{
$base = ( float ) app ( ProductDisplayPriceService :: class ) -> resolveAppBasePrice ();
if ( $base > 0 ) {
return $base ;
}
$montzar = ( float ) app ( ConfigService :: class ) -> get ( 'montzar_live_price' , 0 );
if ( $montzar > 0 ) {
return $montzar ;
}
return ( float ) ( Price :: latest () -> first () ?-> price ?? 0 );
}
/**
* اگر قیمت واحد سفارش حدود قیمت مثقال باشد، به قیمت هر گرم برگردان.
* جلوگیری از ۱۰ میلیون تومان → ۱۰۵ سوت وقتی مثقال در فیلد «هر گرم» نشسته.
*/
private function ensureOrderUnitPriceIsPerGram ( array & $data ) : void
{
$live = $this -> liveGramQuoteRial ();
$config = app ( ConfigService :: class );
$price = $config -> gramUnitPriceIfMesghalQuoted (( float ) ( $data [ 'price' ] ?? 0 ), $live );
if ( $price <= 0 || abs ( $price - ( float ) ( $data [ 'price' ] ?? 0 )) < 1 ) {
return ;
}
$data [ 'price' ] = $price ;
if ( isset ( $data [ 'price_original' ])) {
$data [ 'price_original' ] = $config -> gramUnitPriceIfMesghalQuoted (
( float ) $data [ 'price_original' ],
$live
);
}
}
2026-06-22 19:14:24 +03:30
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 ;
}
2026-06-09 13:34:43 +03:30
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 ),
];
}
2026-06-09 13:28:02 +03:30
private function productTitleFromData ( array $data ) : ? string
{
$product = $data [ 'product' ] ?? null ;
2026-06-22 21:42:48 +03:30
$title = null ;
2026-06-09 13:28:02 +03:30
2026-06-22 21:42:48 +03:30
if ( $product ) {
if ( is_array ( $product )) {
$title = $product [ 'title' ] ?? null ;
} else {
$title = $product -> title ?? null ;
}
2026-06-09 13:28:02 +03:30
}
$title = trim (( string ) $title );
2026-06-22 21:42:48 +03:30
if ( $title === '' && ! empty ( $data [ 'product_id' ])) {
$title = trim (( string ) Product :: query () -> whereKey (( int ) $data [ 'product_id' ]) -> value ( 'title' ));
}
2026-06-09 13:28:02 +03:30
return $title !== '' ? $title : null ;
}
2026-06-03 21:37:05 +03:30
private function isProductOrder ( array $data ) : bool
2026-06-03 19:53:14 +03:30
{
2026-06-09 13:28:02 +03:30
return ! empty ( $data [ 'product_id' ]) && $this -> productTitleFromData ( $data ) !== null ;
2026-06-03 19:53:14 +03:30
}
2026-06-03 21:37:05 +03:30
private function transactionSubjectTitle ( array $data ) : string
2026-06-03 19:53:14 +03:30
{
2026-06-09 13:28:02 +03:30
return $this -> productTitleFromData ( $data ) ?? self :: meltedGoldLabel ();
}
2026-06-03 21:37:05 +03:30
2026-06-09 13:28:02 +03:30
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 ();
2026-06-03 19:53:14 +03:30
}
private function insufficientGoldBalanceMessage ( array $data ) : string
{
2026-06-03 20:24:07 +03:30
return 'موجودی ' . self :: metalTypeLabel () . ' شما کمتر از میزان سفارش است' ;
2026-06-03 19:53:14 +03:30
}
2026-06-04 15:30:34 +03:30
/**
2026-07-18 17:37:41 +03:30
* اختلاف قیمت سفارش:
2026-07-28 10:33:11 +03:30
* - محصول با «قیمت اپ»: buy_fee / sell_fee + تب اختلاف محصول
* - بدون قیمت اپ: فقط تب اختلاف محصول (cash_fee)
2026-07-18 17:37:41 +03:30
*
* @return array{
* amount: float,
* source: string,
* key: string,
* label: string,
* uses_app_price: bool,
* base_price: float,
* lines: list<array<string, mixed>>
* }
2026-06-04 15:30:34 +03:30
*/
2026-07-18 17:37:41 +03:30
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 ? 'خرید' : 'فروش' ;
2026-06-04 15:30:34 +03:30
2026-07-18 17:37:41 +03:30
$base = ( float ) ( $basePriceRial ?? 0 );
if ( $base <= 0 ) {
2026-08-10 19:21:56 +03:30
$base = ( float ) $this -> resolveProductUnitBasePrice ( $product , $direction );
2026-07-18 17:37:41 +03:30
}
if ( $base <= 0 ) {
$base = ProductPriceService :: productBasePriceRial ( $product );
}
2026-06-04 15:30:34 +03:30
2026-07-18 17:37:41 +03:30
$lines = [];
$amount = 0.0 ;
$keys = [];
if ( $usesAppPrice ) {
if ( $isBuy ) {
2026-07-28 10:33:11 +03:30
$settingsAmt = $config -> displayBuyPriceDiffRial ();
$settingsKey = 'buy_fee' ;
$settingsTitle = 'اختلاف پایه اپ · اضافه قیمت خرید (buy_fee)' ;
2026-07-18 17:37:41 +03:30
} else {
2026-07-28 10:33:11 +03:30
$settingsAmt = $config -> displaySellPriceDiffRial ();
$settingsKey = 'sell_fee' ;
$settingsTitle = 'اختلاف پایه اپ · کسر قیمت فروش (sell_fee)' ;
2026-07-18 17:37:41 +03:30
}
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 ) . ' ریال / گرم' ,
];
2026-06-04 15:30:34 +03:30
}
}
2026-07-18 17:37:41 +03:30
$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 ,
];
}
2026-08-16 22:00:23 +03:30
$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 );
2026-08-17 13:11:00 +03:30
if ( $specialAmt !== 0 ) {
2026-08-16 22:00:23 +03:30
$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 ,
];
}
2026-07-18 17:37:41 +03:30
$hasApp = collect ( $lines ) -> contains ( fn ( $l ) => ( $l [ 'kind' ] ?? '' ) === 'app_spread' );
$hasProduct = collect ( $lines ) -> contains ( fn ( $l ) => ( $l [ 'kind' ] ?? '' ) === 'product_spread' );
2026-08-16 22:00:23 +03:30
$hasSpecial = collect ( $lines ) -> contains ( fn ( $l ) => ( $l [ 'kind' ] ?? '' ) === 'special_pricing' );
2026-07-18 17:37:41 +03:30
2026-08-16 22:00:23 +03:30
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 ) {
2026-07-18 17:37:41 +03:30
$source = 'app+product' ;
2026-07-24 16:49:42 +03:30
$label = 'قیمت صفحه اصلی اپلیکیشن + تب اختلاف محصول = ' . number_format ( $amount ) . ' ریال / گرم' ;
2026-07-18 17:37:41 +03:30
} 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' ];
2026-06-04 15:30:34 +03:30
}
2026-06-05 16:24:15 +03:30
2026-07-13 19:03:30 +03:30
/**
* قیمت پایهٔ هر واحد محصول (بدون ضرب در وزن — وزن در total_price_original اعمال میشود).
2026-07-29 17:13:19 +03:30
* - قیمت اپ → مظنهٔ زندهٔ اپ
* - reading_method (مثل شمش zaarbox) → همان قیمت enrichشدهٔ لیست/UI، نه ستون کهنه/صفر DB
* - بقیه → قیمت ذخیرهشدهٔ محصول
2026-07-13 19:03:30 +03:30
*/
2026-08-10 19:21:56 +03:30
private function resolveProductUnitBasePrice ( Product $product , ? int $direction = null ) : int
2026-07-13 19:03:30 +03:30
{
$display = app ( ProductDisplayPriceService :: class );
2026-07-29 17:13:19 +03:30
// قیمت اپ یا فید مرجع — همان منبع نمایش؛ DB خام برای reading_method اغلب ۰ /کهنه است
if ( $display -> usesAppPrice ( $product ) || $product -> reading_method ) {
2026-08-10 19:21:56 +03:30
return ( int ) round ( $display -> resolveOrderTradePrice ( $product , $direction ));
2026-07-13 19:03:30 +03:30
}
return ( int ) (( $product -> price ?? 0 ) + ( $product -> price_profit ?? 0 ));
}
2026-07-18 17:37:41 +03:30
/**
* 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 ;
}
}
2026-07-18 16:38:46 +03:30
/**
* سود هر گرم / سود کل را از قیمت نهایی و قیمت بدون سود همگام کن
2026-07-18 17:37:41 +03:30
* و فاکتور اقلام (مثل فاکتور رستوران) را کامل کن.
2026-07-18 16:38:46 +03:30
*/
private function syncOrderProfitFields ( array & $data ) : void
{
2026-07-18 17:37:41 +03:30
$this -> ensureOrderProductId ( $data );
2026-07-18 16:38:46 +03:30
$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' ];
2026-07-18 17:37:41 +03:30
$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 );
2026-07-18 17:49:36 +03:30
$total = ( int ) round ( $perUnit * $amount );
2026-07-18 17:37:41 +03:30
$spreadTotal += $total ;
2026-07-18 17:49:36 +03:30
$amountLabel = rtrim ( rtrim ( number_format ( $amount , 4 , '.' , '' ), '0' ), '.' );
2026-07-18 17:37:41 +03:30
$receiptLines [] = array_merge ( $line , [
'amount_units' => $amount ,
'total' => $total ,
'line_text' => ( $line [ 'detail' ] ?? number_format ( $perUnit ) . ' ریال / گرم' )
. ( $amount > 0
2026-07-18 17:49:36 +03:30
? ( ' × ' . $amountLabel . ' گرم = ' . number_format ( $total ) . ' ریال' )
2026-07-18 17:37:41 +03:30
: '' ),
]);
}
// اگر اقلام ذخیره نشده بود، حداقل یک خط از اختلاف واقعی بساز
if ( $receiptLines === [] && $data [ 'fee' ] > 0 ) {
2026-07-18 17:49:36 +03:30
$feeTotal = ( int ) round (( float ) $data [ 'total_fee' ]);
2026-07-18 17:37:41 +03:30
$receiptLines [] = [
'kind' => 'spread' ,
'key' => $data [ 'fee_source_key' ] ?? 'fee' ,
'label' => $data [ 'fee_source_label' ] ?? 'اختلاف قیمت' ,
'is_percent' => false ,
'per_unit' => $data [ 'fee' ],
'amount_units' => $amount ,
2026-07-18 17:49:36 +03:30
'total' => $feeTotal ,
2026-07-18 17:37:41 +03:30
'detail' => number_format ( $data [ 'fee' ]) . ' ریال / گرم' ,
'line_text' => number_format ( $data [ 'fee' ]) . ' ریال / گرم × '
. rtrim ( rtrim ( number_format ( $amount , 4 , '.' , '' ), '0' ), '.' )
2026-07-18 17:49:36 +03:30
. ' گرم = ' . number_format ( $feeTotal ) . ' ریال' ,
2026-07-18 17:37:41 +03:30
];
2026-07-18 17:49:36 +03:30
$spreadTotal = $feeTotal ;
2026-07-18 17:37:41 +03:30
}
$scGold = ( float ) ( $data [ 'service_charge' ] ?? 0 );
2026-07-18 17:49:36 +03:30
$scRial = ( int ) round (( float ) ( $data [ 'service_charge_price' ] ?? 0 ));
2026-07-18 17:37:41 +03:30
$scPct = ( float ) ( $data [ 'service_charge_percentage' ] ?? 0 );
if ( $scGold > 0 || $scRial > 0 ) {
2026-07-23 17:53:10 +03:30
$feeAsset = ( string ) ( $data [ 'fee_asset' ] ?? 'rial' );
$fromMetal = $feeAsset === 'gold' ;
$assetLabel = $fromMetal ? 'از فلز/مقدار' : 'از ریال' ;
2026-07-18 17:37:41 +03:30
$goldLabel = rtrim ( rtrim ( number_format ( $scGold , 4 , '.' , '' ), '0' ), '.' ) . ' گرم' ;
$detailParts = [];
if ( $scPct > 0 ) {
$detailParts [] = rtrim ( rtrim ( number_format ( $scPct , 4 , '.' , '' ), '0' ), '.' ) . '%' ;
}
2026-07-23 17:53:10 +03:30
if ( $fromMetal && $scGold > 0 && $priceOriginal > 0 ) {
2026-07-18 17:37:41 +03:30
$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 ) . ' ریال' ;
}
2026-07-23 17:53:10 +03:30
$detailParts [] = $assetLabel ;
2026-07-18 17:37:41 +03:30
$receiptLines [] = [
'kind' => 'service_charge' ,
'key' => 'service_charge' ,
2026-07-23 17:53:10 +03:30
'label' => 'کارمزد معامله · ' . $assetLabel ,
2026-07-18 17:37:41 +03:30
'is_percent' => $scPct > 0 ,
'raw' => $scPct ,
'gold' => $scGold ,
'rial' => $scRial ,
2026-07-23 17:53:10 +03:30
'fee_asset' => $feeAsset ,
2026-07-18 17:37:41 +03:30
'per_unit' => $amount > 0 ? ( $scRial / $amount ) : 0 ,
'amount_units' => $amount ,
'total' => $scRial ,
'detail' => implode ( ' · ' , $detailParts ),
'line_text' => implode ( ' · ' , $detailParts ),
];
}
2026-07-18 17:49:36 +03:30
$baseTotal = ( int ) round ( $amount * $priceOriginal );
$customerTotal = ( int ) round (( float ) ( $data [ 'total_price' ] ?? ( $amount * $price )));
$spreadTotal = ( int ) round ( $spreadTotal );
2026-07-18 17:37:41 +03:30
$grandTotal = $spreadTotal + $scRial ;
2026-07-18 17:49:36 +03:30
2026-07-18 17:37:41 +03:30
$data [ 'fee_breakdown' ] = [
'uses_app_price' => ( bool ) ( $data [ '_fee_uses_app_price' ] ?? false ),
'amount_units' => $amount ,
'price_original' => $priceOriginal ,
'price_customer' => $price ,
2026-07-18 17:49:36 +03:30
'base_total' => $baseTotal ,
'customer_total' => $customerTotal ,
2026-07-18 17:37:41 +03:30
'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' ]
);
2026-08-31 16:14:02 +03:30
$this -> roundOrderMoneyFields ( $data );
}
/** اعداد پولی سفارش را قبل از INSERT گرد کن تا با DECIMAL(20,4) سازگار باشد. */
private function roundOrderMoneyFields ( array & $data ) : void
{
foreach ([
'price' ,
'price_original' ,
'total_price' ,
'total_price_original' ,
'total_price_liman' ,
'fee' ,
'total_fee' ,
'service_charge_price' ,
'payment_amount' ,
'payment_remind' ,
] as $key ) {
if ( ! isset ( $data [ $key ]) || ! is_numeric ( $data [ $key ])) {
continue ;
}
$data [ $key ] = round (( float ) $data [ $key ], 4 );
}
2026-07-18 16:38:46 +03:30
}
2026-06-05 16:24:15 +03:30
private function bonusGift ( $customer , $referrer )
{
2026-08-22 15:10:54 +03:30
$bonusRaw = app ( \App\Services\ConfigService :: class ) -> getString ( 'referral_bonus_amount' );
if ( $bonusRaw === '' ) {
2026-06-05 16:24:15 +03:30
return false ;
}
2026-08-22 15:10:54 +03:30
$bonus = json_decode ( $bonusRaw , true );
2026-06-05 16:24:15 +03:30
$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 );
2026-07-26 14:15:26 +03:30
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 );
}
2026-06-05 16:24:15 +03:30
$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 );
2026-07-26 14:15:26 +03:30
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 );
}
2026-06-05 16:24:15 +03:30
$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 ;
}
2026-06-06 22:06:53 +03:30
2026-07-10 00:19:30 +03:30
private function enforceProductOrderLimits ( array & $data , ? float $totalPriceRial = null ) : void
2026-06-06 22:06:53 +03:30
{
$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 ) {
2026-07-10 00:19:30 +03:30
$product -> validateOrderCashLimit ( $this -> resolveOrderLimitCashRial ( $data , $totalPriceRial ));
2026-06-06 22:06:53 +03:30
}
}
2026-07-10 00:19:30 +03:30
/**
* مبلغ مؤثر برای 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 );
}
2026-01-24 14:03:11 +00:00
}