2026-01-24 14:03:11 +00:00
<? php
namespace App\Http\Controllers\Admin ;
use App\DB\Te\TableEngine ;
use App\Http\Controllers\Controller ;
2026-07-08 11:25:19 +03:30
use App\Models\Address ;
2026-01-24 14:03:11 +00:00
use App\Models\Customer ;
use App\Models\Order ;
use App\Models\Price ;
use App\Models\Product ;
use Illuminate\Support\Facades\DB ;
use App\Models\Withdraw ;
use Illuminate\Http\Request ;
use Illuminate\Support\Facades\Log ;
use Rooyesh\Wallet\interfaces\TransactionInterface ;
use Rooyesh\Wallet\interfaces\WalletInterface ;
use Rooyesh\Wallet\models\Transaction ;
use Rooyesh\Wallet\models\Wallet ;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException ;
use App\Http\Controllers\Admin\TahesabController ;
2026-02-09 14:09:19 +03:30
use App\Models\Setting ;
2026-02-19 16:20:36 +03:30
use App\Models\User ;
2026-06-25 23:06:29 +03:30
use App\Models\TransactionsAdmin ;
2026-07-08 11:25:19 +03:30
use App\Services\AssetSummaryService ;
2026-06-23 21:21:04 +03:30
use App\Services\BalanceReportService ;
2026-06-24 01:17:34 +03:30
use App\Services\ConfigService ;
2026-07-20 15:52:42 +03:30
use App\Services\PargarSyncService ;
2026-06-23 21:04:29 +03:30
use App\Services\WalletBalanceService ;
2026-06-27 15:51:44 +03:30
use App\Services\WalletBroadcastService ;
2026-06-09 20:43:01 +03:30
use Hekmatinasser\Verta\Verta ;
2026-01-24 14:03:11 +00:00
class WalletController extends Controller
{
2026-07-26 14:15:26 +03:30
/** موجودی طلا با سه رقم اعشار نمایش داده میشود؛ نصف پلهٔ نمایش = بیشترین اختلاف ممکن با مقدار واقعی */
private const GOLD_DISPLAY_STEP = 0.0005 ;
/** پنجرهٔ تشخیص سند تکراری — دوبار کلیک یا ارسال مجدد فرم */
private const DUPLICATE_CHARGE_WINDOW_SECONDS = 60 ;
/**
* جلوگیری از ثبت دوبارهٔ همان سند دستی. فقط روی کسر اعمال میشود تا شارژهای
* مثبتِ پشتسرهم (مثلاً دو واریز یکسان) بسته نشوند.
*/
private function assertNotDuplicateCharge ( int $customerId , float $value , string $column ) : void
{
if ( $value >= 0 ) {
return ;
}
$recent = Withdraw :: query ()
-> where ( 'customer_id' , $customerId )
-> where ( 'sts' , 4 )
-> where ( 'created_at' , '>=' , now () -> subSeconds ( self :: DUPLICATE_CHARGE_WINDOW_SECONDS ));
if ( $column === 'cash' ) {
$recent -> where ( 'cash' , ( int ) $value );
} else {
$recent -> whereBetween ( 'gold' , [ $value - 1e-6 , $value + 1e-6 ]);
}
if ( $recent -> exists ()) {
throw new UnprocessableEntityHttpException (
'همین سند لحظاتی پیش برای این مشتری ثبت شده است. اگر تکرارش عمدی است یک دقیقه صبر کنید.'
);
}
}
2026-08-18 22:23:20 +03:30
/**
* عدد نمایشی موجودی تا ۳ رقم گرد میشود؛ اگر ادمین همان عدد را بزند، کل ماندهٔ واقعی کسر شود.
*/
private function resolveGoldDischargeAmount ( float $available , float $requested , string $error ) : float
{
if ( $requested <= 0 ) {
throw new UnprocessableEntityHttpException ( $error );
}
if ( $requested <= $available ) {
return $requested ;
}
$sameDisplayed = round ( $requested , 3 ) <= round ( $available , 3 );
$withinDisplayStep = ( $requested - $available ) <= self :: GOLD_DISPLAY_STEP ;
if ( $sameDisplayed || $withinDisplayStep ) {
return $available ;
}
throw new UnprocessableEntityHttpException ( $error );
}
/**
* طلای مانده روی کیف legacy محصولات کیف اصلی را به کیف اصلی منتقل میکند
* تا موجودی نمایشی (effective) با کیف قابلکسر یکی شود.
*/
private function consolidateOrphanedMainWalletGold ( int $ownerId , int $ownerType ) : void
{
$productIds = app ( ConfigService :: class ) -> mainWalletProductIds ();
if ( $productIds === []) {
return ;
}
$orphans = Wallet :: query ()
-> where ( 'owner' , $ownerId )
-> where ( 'owner_type' , $ownerType )
-> whereNull ( 'fund_id' )
-> whereIn ( 'product_id' , $productIds )
-> get ();
$main = new WalletInterface ( $ownerId , $ownerType , null , null );
foreach ( $orphans as $orphan ) {
$amount = ( float ) $orphan -> gold ;
if ( $amount <= 1e-12 ) {
continue ;
}
$orphanWallet = new WalletInterface ( $ownerId , $ownerType , null , ( int ) $orphan -> product_id );
new TransactionInterface ( $orphanWallet , [
'gold' => $amount ,
'cash' => 0 ,
'expire_at' => now (),
'note' => 'انتقال به کیف اصلی' ,
'active' => true ,
], 7 , false , false , 0 );
new TransactionInterface ( $main , [
'gold' => $amount ,
'cash' => 0 ,
'expire_at' => now (),
'note' => 'انتقال از کیف محصول' ,
'active' => true ,
], 7 , true , false , 0 );
$main = new WalletInterface ( $ownerId , $ownerType , null , null );
}
}
2026-01-24 14:03:11 +00:00
public function charge2 ( Request $request )
{
$data = $request -> all ();
$customer = Customer :: findOrFail ( $data [ 'customer_id' ]);
$deposit = false ;
if ( $data [ 'value' ] > 0 ) {
$deposit = true ;
}
2026-06-15 11:25:10 +03:30
$wallet_interface = new WalletInterface ( $data [ 'customer_id' ], ( int ) $customer -> type );
$wallet = $wallet_interface -> wallet ;
2026-01-24 14:03:11 +00:00
if ( is_null ( $wallet ) && ! $deposit ) {
throw new UnprocessableEntityHttpException ( 'مقدار موجودی کمتر از درخواست شماست' );
}
if ( ! is_null ( $wallet ) && $wallet -> cash < abs ( $data [ 'value' ]) && ! $deposit ) {
throw new UnprocessableEntityHttpException ( 'مقدار موجودی کمتر از درخواست شماست' );
}
$orders = Order :: where ( 'customer_id' , $data [ 'customer_id' ]) -> where ( 'sts' , 1 ) -> get ();
$gold_block = 0 ;
if ( count ( $orders ) > 0 ) {
$value = $data [ 'value' ];
foreach ( $orders as $order ) {
if ( $value < $order -> payment_remind ) {
$order -> payment_amount += $value ;
$order -> payment_remind -= $value ;
$order -> save ();
break ;
} else {
$value -= $order -> payment_remind ;
$order -> payment_amount = $order -> total_price ;
$order -> payment_remind = 0 ;
$order -> sts = 2 ;
$gold_block -= $order -> amount ;
$order -> save ();
}
}
}
$user = auth () -> user ();
$customer = Customer :: findOrFail ( $data [ "customer_id" ]);
$Withdraw = Withdraw :: create ([
'user_id' => $user -> id ,
'customer_id' => $customer -> id ,
"cash" => $data [ 'value' ],
'sts' => 4 ,
]);
new TransactionInterface ( $wallet_interface , $_inp = [
'cash' => $data [ 'value' ],
'expire_at' => now (),
'withdraw_id' => $Withdraw -> id ,
'active' => true
], 3 , false , true , $gold_block );
2026-07-20 15:52:42 +03:30
try {
app ( PargarSyncService :: class ) -> queueWalletDocument (
$customer ,
'cash' ,
( float ) $data [ 'value' ],
[
'withdraw_id' => $Withdraw -> id ,
'source' => 'admin_charge2' ,
]
);
} catch ( \Throwable $e ) {
report ( $e );
}
2026-06-16 20:38:48 +03:30
$message = 'کیف پول شما به مبلغ ' . self :: formatSmsRialAmount ( $data [ 'value' ]) . " ریال در تاریخ " . verta () -> format ( 'Y/n/j H:i:s' ) . " شارژ شد " . " \n "
2026-01-24 14:03:11 +00:00
. $this -> Config ( 'name_fa' );
$this -> Send ( $customer -> mobile , $message );
2026-07-27 22:09:08 +03:30
try {
app ( \App\Services\CustomerAppNotifyService :: class ) -> notifyCustomer (
( int ) $customer -> id ,
'واریز به کیف پول' ,
'کیف پول شما به مبلغ ' . self :: formatSmsRialAmount ( $data [ 'value' ]) . ' ریال شارژ شد.' ,
'success'
);
} catch ( \Throwable $e ) {
report ( $e );
}
2026-08-23 14:16:00 +03:30
\App\Services\Automation\AutomationEvents :: fire ( 'wallet.rial_charge' , ( int ) $customer -> id );
2026-01-24 14:03:11 +00:00
return response () -> json ( true );
}
public function charge ( Request $request )
{
$data = $request -> all ();
$customer = Customer :: findOrFail ( $data [ 'customer_id' ]);
$deposit = false ;
if ( $data [ 'value' ] > 0 ) {
$deposit = true ;
}
2026-06-15 11:25:10 +03:30
$wallet_interface = new WalletInterface ( $data [ 'customer_id' ], ( int ) $customer -> type );
$wallet = $wallet_interface -> wallet ;
2026-01-24 14:03:11 +00:00
if ( is_null ( $wallet ) && ! $deposit ) {
throw new UnprocessableEntityHttpException ( 'مقدار موجودی کمتر از درخواست شماست' );
}
if ( ! is_null ( $wallet ) && $wallet -> cash < abs ( $data [ 'value' ]) && ! $deposit ) {
throw new UnprocessableEntityHttpException ( 'مقدار موجودی کمتر از درخواست شماست' );
}
2026-07-26 14:15:26 +03:30
$this -> assertNotDuplicateCharge (( int ) $customer -> id , ( float ) $data [ 'value' ], 'cash' );
2026-01-24 14:03:11 +00:00
$orders = Order :: where ( 'customer_id' , $data [ 'customer_id' ]) -> where ( 'sts' , 1 ) -> get ();
$gold_block = 0 ;
$user = auth () -> user ();
$customer = Customer :: findOrFail ( $data [ "customer_id" ]);
$Withdraw = Withdraw :: create ([
'user_id' => $user -> id ,
'customer_id' => $customer -> id ,
"cash" => $data [ 'value' ],
"note" => $data [ 'note' ],
'sts' => 4 ,
]);
2026-06-07 12:25:32 +03:30
$type = $deposit ? 3 : 4 ;
2026-01-24 14:03:11 +00:00
new TransactionInterface ( $wallet_interface , $_inp = [
'gold' => 0 ,
'cash' => $data [ 'value' ],
'expire_at' => now (),
"note" => $data [ 'note' ],
'withdraw_id' => $Withdraw -> id ,
'active' => true
2026-06-07 12:25:32 +03:30
], $type , true , $deposit , $gold_block );
2026-01-24 14:03:11 +00:00
2026-06-25 23:06:29 +03:30
$this -> recordAccountingTransaction ( $customer , $Withdraw , ( int ) $user -> id , ( float ) $data [ 'value' ], 0 , $data [ 'note' ] ?? null );
2026-07-05 16:43:34 +03:30
if ( app ( ConfigService :: class ) -> isTahesabAccounting ()) {
2026-02-09 14:09:19 +03:30
try {
2026-03-14 11:08:30 +03:30
$data [ 'direction' ] = $deposit ? 0 : 1 ;
2026-06-25 22:20:40 +03:30
$data [ 'withdraw_id' ] = $Withdraw -> id ;
app ( TahesabController :: class ) -> wallet ( $data );
} catch ( \Exception $e ) {
report ( $e );
2026-02-09 14:09:19 +03:30
}
}
2026-07-23 20:24:58 +03:30
if ( app ( ConfigService :: class ) -> isZarinAccounting ()) {
try {
$data [ 'direction' ] = $deposit ? 0 : 1 ;
$data [ 'withdraw_id' ] = $Withdraw -> id ;
app ( \App\Http\Controllers\Admin\ZarinController :: class ) -> wallet ( $data );
} catch ( \Exception $e ) {
report ( $e );
}
}
2026-07-20 15:52:42 +03:30
try {
app ( PargarSyncService :: class ) -> queueWalletDocument (
$customer ,
'cash' ,
( float ) $data [ 'value' ],
[
'withdraw_id' => $Withdraw -> id ,
'note' => $data [ 'note' ] ?? null ,
'source' => 'admin_cash' ,
]
);
} catch ( \Throwable $e ) {
report ( $e );
}
2026-02-19 16:20:36 +03:30
//send sms to users and customer
2026-01-25 09:58:32 +00:00
$status = $data [ 'value' ] > 0 ? " شارژ شد " : " کسر شد " ;
2026-07-18 22:22:01 +03:30
if ( self :: isSmsNotifEnabled ( 'charge_notif' )) {
2026-06-16 20:38:48 +03:30
$message = 'کیف پول کاربر ' . $customer -> name . ' به مقدار ' . self :: formatSmsRialAmount ( $data [ 'value' ]) . " ریال در تاریخ " . verta () -> format ( 'Y/n/j H:i:s' ) . $status . "بابت" . " \n "
2026-02-19 16:20:36 +03:30
. $data [ 'note' ]
. " \n "
. $this -> Config ( 'name_fa' );
User :: where ( 'send_sms' , 1 ) -> get () -> each ( function ( $admin ) use ( $message , $data ) {
$this -> send ( $admin -> mobile , $message );
});
2026-07-18 22:22:01 +03:30
$message = 'کیف پول شما به اندازه ' . self :: formatSmsRialAmount ( $data [ 'value' ]) . " ریال در تاریخ " . verta () -> format ( 'Y/n/j H:i:s' ) . $status . "بابت" . " \n "
. $data [ 'note' ]
. " \n "
. $this -> Config ( 'name_fa' );
$this -> Send ( $customer -> mobile , $message );
}
2026-07-27 22:09:08 +03:30
try {
$isDeposit = ( float ) $data [ 'value' ] > 0 ;
app ( \App\Services\CustomerAppNotifyService :: class ) -> notifyCustomer (
( int ) $customer -> id ,
$isDeposit ? 'واریز به کیف پول' : 'برداشت از کیف پول' ,
'کیف پول شما به اندازه ' . self :: formatSmsRialAmount ( $data [ 'value' ]) . ' ریال'
. ( $isDeposit ? ' شارژ شد.' : ' کسر شد.' )
. ( trim (( string ) ( $data [ 'note' ] ?? '' )) !== '' ? " \n " . $data [ 'note' ] : '' ),
$isDeposit ? 'success' : 'warning'
);
} catch ( \Throwable $e ) {
report ( $e );
}
2026-08-23 14:16:00 +03:30
if (( float ) $data [ 'value' ] > 0 ) {
\App\Services\Automation\AutomationEvents :: fire ( 'wallet.rial_charge' , ( int ) $customer -> id );
}
2026-06-27 15:51:44 +03:30
$this -> broadcastCustomerWalletAssets ( $customer );
2026-01-24 14:03:11 +00:00
return response () -> json ( true );
}
public function charge_gold ( Request $request )
{
$data = $request -> all ();
$customer = Customer :: findOrFail ( $data [ 'customer_id' ]);
$deposit = false ;
if ( $data [ 'value' ] > 0 ) {
$deposit = true ;
}
2026-08-18 22:23:20 +03:30
if ( ! $deposit ) {
$this -> consolidateOrphanedMainWalletGold (( int ) $customer -> id , ( int ) $customer -> type );
}
2026-06-15 11:25:10 +03:30
$wallet_interface = new WalletInterface ( $data [ 'customer_id' ], ( int ) $customer -> type );
$wallet = $wallet_interface -> wallet ;
2026-07-26 14:15:26 +03:30
// کسر دستی نباید موجودی طلا را منفی کند؛ شارژ مثبت بدون هیچ شرطی باز است
if ( ! $deposit ) {
$available = is_null ( $wallet ) ? 0.0 : ( float ) $wallet -> gold ;
$requested = abs (( float ) $data [ 'value' ]);
2026-08-18 22:23:20 +03:30
$resolved = $this -> resolveGoldDischargeAmount ( $available , $requested , 'مقدار موجودی طلا کمتر از درخواست شماست' );
$data [ 'value' ] = - $resolved ;
2026-07-26 14:15:26 +03:30
}
$this -> assertNotDuplicateCharge (( int ) $customer -> id , ( float ) $data [ 'value' ], 'gold' );
2026-01-24 14:03:11 +00:00
$gold_block = 0 ;
$user = auth () -> user ();
$customer = Customer :: findOrFail ( $data [ "customer_id" ]);
$Withdraw = Withdraw :: create ([
'user_id' => $user -> id ,
'customer_id' => $customer -> id ,
"gold" => $data [ 'value' ],
"note" => $data [ 'note' ],
'sts' => 4 ,
]);
2026-06-24 01:17:34 +03:30
$representative = app ( ConfigService :: class ) -> mainWalletRepresentativeProduct ();
2026-01-24 14:03:11 +00:00
new TransactionInterface ( $wallet_interface , $_inp = [
'cash' => 0 ,
'gold' => $data [ 'value' ],
'expire_at' => now (),
"note" => $data [ 'note' ],
'withdraw_id' => $Withdraw -> id ,
2026-06-24 01:17:34 +03:30
'product_id' => $representative ?-> id ,
2026-01-24 14:03:11 +00:00
'active' => true
], 7 , $deposit , false , $gold_block );
2026-06-25 23:06:29 +03:30
$this -> recordAccountingTransaction ( $customer , $Withdraw , ( int ) $user -> id , 0 , ( float ) $data [ 'value' ], $data [ 'note' ] ?? null );
2026-06-09 20:08:06 +03:30
//send to tahesab
2026-07-05 16:43:34 +03:30
if ( app ( ConfigService :: class ) -> isTahesabAccounting ()) {
2026-03-14 11:08:30 +03:30
try {
2026-06-07 14:30:26 +03:30
$data [ 'direction' ] = $deposit ? 1 : 0 ;
2026-06-25 22:20:40 +03:30
$data [ 'withdraw_id' ] = $Withdraw -> id ;
2026-07-28 17:44:24 +03:30
if ( $representative ) {
$data [ 'product_id' ] = $representative -> id ;
$data [ 'product' ] = [
'id' => $representative -> id ,
'title' => $representative -> title ,
'group_type' => $representative -> group_type ,
'metal_id' => $representative -> metal_id ,
'amount' => $representative -> amount ,
'unit' => $representative -> unit ,
'trade_by_quantity' => $representative -> trade_by_quantity ,
];
}
2026-06-25 22:20:40 +03:30
app ( TahesabController :: class ) -> walletGold ( $data );
} catch ( \Exception $e ) {
report ( $e );
2026-03-14 11:08:30 +03:30
}
}
2026-07-20 15:52:42 +03:30
2026-07-23 20:24:58 +03:30
if ( app ( ConfigService :: class ) -> isZarinAccounting ()) {
try {
$data [ 'direction' ] = $deposit ? 1 : 0 ;
$data [ 'withdraw_id' ] = $Withdraw -> id ;
2026-07-28 17:44:24 +03:30
if ( $representative ) {
$data [ 'product_id' ] = $representative -> id ;
$data [ 'product' ] = [
'id' => $representative -> id ,
'title' => $representative -> title ,
'group_type' => $representative -> group_type ,
'metal_id' => $representative -> metal_id ,
'amount' => $representative -> amount ,
'unit' => $representative -> unit ,
'trade_by_quantity' => $representative -> trade_by_quantity ,
];
}
2026-07-23 20:24:58 +03:30
app ( \App\Http\Controllers\Admin\ZarinController :: class ) -> walletGold ( $data );
} catch ( \Exception $e ) {
report ( $e );
}
}
2026-07-20 15:52:42 +03:30
try {
app ( PargarSyncService :: class ) -> queueWalletDocument (
$customer ,
'gold' ,
( float ) $data [ 'value' ],
[
'withdraw_id' => $Withdraw -> id ,
'note' => $data [ 'note' ] ?? null ,
'product_id' => $representative ?-> id ,
'source' => 'admin_gold' ,
]
);
} catch ( \Throwable $e ) {
report ( $e );
}
2026-02-19 16:20:36 +03:30
//send sms to users and customer
$status = $data [ 'value' ] > 0 ? " شارژ شد " : " کسر شد " ;
2026-06-24 01:17:34 +03:30
$weightUnit = self :: tradeWeightUnit ();
2026-07-18 22:22:01 +03:30
if ( self :: isSmsNotifEnabled ( 'charge_notif' )) {
2026-06-24 01:17:34 +03:30
$message = 'کیف پول کاربر ' . $customer -> name . ' به مقدار ' . self :: formatSmsGoldWeight ( $data [ 'value' ]) . " { $weightUnit } در تاریخ " . verta () -> format ( 'Y/n/j H:i:s' ) . $status . "بابت" . " \n "
2026-02-19 16:20:36 +03:30
. $data [ 'note' ]
. " \n "
. $this -> Config ( 'name_fa' );
User :: where ( 'send_sms' , 1 ) -> get () -> each ( function ( $admin ) use ( $message , $data ) {
$this -> send ( $admin -> mobile , $message );
});
2026-07-18 22:22:01 +03:30
$message = 'کیف پول شما به اندازه ' . self :: formatSmsGoldWeight ( $data [ 'value' ]) . " { $weightUnit } در تاریخ " . verta () -> format ( 'Y/n/j H:i:s' ) . $status . " بابت" . " \n "
. $data [ 'note' ]
. $this -> Config ( 'name_fa' );
$this -> Send ( $customer -> mobile , $message );
}
2026-07-27 22:09:08 +03:30
try {
$isDeposit = ( float ) $data [ 'value' ] > 0 ;
$weightUnit = self :: tradeWeightUnit ();
app ( \App\Services\CustomerAppNotifyService :: class ) -> notifyCustomer (
( int ) $customer -> id ,
$isDeposit ? 'واریز طلا' : 'برداشت طلا' ,
'کیف پول شما به اندازه ' . self :: formatSmsGoldWeight ( $data [ 'value' ]) . " { $weightUnit } "
. ( $isDeposit ? ' شارژ شد.' : ' کسر شد.' ),
$isDeposit ? 'success' : 'warning'
);
} catch ( \Throwable $e ) {
report ( $e );
}
2026-06-27 15:51:44 +03:30
$this -> broadcastCustomerWalletAssets ( $customer );
2026-01-24 14:03:11 +00:00
return response () -> json ( true );
}
2026-07-11 17:59:50 +03:30
/** گزینههای شارژ کیف پول — نقدی + همهٔ محصولات (فعال و غیرفعال) */
2026-06-23 21:59:38 +03:30
public function chargeTargets ()
2026-01-24 14:03:11 +00:00
{
2026-06-23 21:59:38 +03:30
$configService = app ( ConfigService :: class );
$mainWalletProductIds = $configService -> mainWalletProductIds ();
$balanceReport = app ( BalanceReportService :: class );
$targets = [
[
'key' => 'money' ,
'kind' => 'money' ,
'label' => 'نقدی (ریال)' ,
'unit' => 'ریال' ,
2026-07-11 17:59:50 +03:30
'active' => true ,
2026-06-23 21:59:38 +03:30
],
];
$products = Product :: query ()
2026-07-11 17:59:50 +03:30
-> orderByDesc ( 'active' )
2026-06-23 21:59:38 +03:30
-> orderBy ( 'metal_id' )
-> orderBy ( 'id' )
2026-07-11 17:59:50 +03:30
-> get ([ 'id' , 'title' , 'unit' , 'metal_id' , 'group_type' , 'trade_by_quantity' , 'amount' , 'direct_price' , 'active' ]);
2026-06-23 21:59:38 +03:30
foreach ( $products as $product ) {
$usesMainWallet = in_array (( int ) $product -> id , $mainWalletProductIds , true );
$label = ( string ) $product -> title ;
if ( $usesMainWallet ) {
$label .= ' (کیف اصلی)' ;
}
2026-07-11 17:59:50 +03:30
if ( ! ( bool ) $product -> active ) {
$label .= ' (غیرفعال)' ;
}
2026-06-23 21:59:38 +03:30
2026-07-28 17:44:24 +03:30
$payload = Controller :: productUnitPayload ( $product );
$isCountBased = Controller :: isCountBasedProduct ( $payload );
2026-06-23 21:59:38 +03:30
$targets [] = [
'key' => 'product:' . $product -> id ,
'kind' => 'metal' ,
'product_id' => ( int ) $product -> id ,
'label' => $label ,
'unit' => $this -> resolveProductChargeUnit ( $product ),
'metal_id' => max ( 1 , ( int ) ( $product -> metal_id ?? 1 )),
'uses_main_wallet' => $usesMainWallet ,
'trade_by_quantity' => ( bool ) $product -> trade_by_quantity ,
2026-07-28 17:44:24 +03:30
'allows_decimal' => ! $isCountBased ,
2026-06-23 21:59:38 +03:30
'group_type' => ( string ) ( $product -> group_type ?? '' ),
2026-07-11 17:59:50 +03:30
'active' => ( bool ) $product -> active ,
2026-06-23 21:59:38 +03:30
];
}
return response () -> json ([
'targets' => $targets ,
'catalogMetals' => $balanceReport -> catalogMetalsMeta (),
]);
}
/**
* شارژ یکپارچه کیف پول — نقدی یا محصول (با رعایت کیف اصلی برای آبشده).
*/
public function charge_wallet ( Request $request )
{
$productId = ( int ) $request -> input ( 'product_id' , 0 );
if ( $productId > 0 ) {
return $this -> chargeProductMetal ( $request , $productId );
}
return $this -> charge ( $request );
}
private function chargeProductMetal ( Request $request , int $productId )
{
$data = $request -> all ();
$product = Product :: findOrFail ( $productId );
$customer = Customer :: findOrFail ( $data [ 'customer_id' ]);
2026-07-28 17:44:24 +03:30
$value = ( float ) ( $data [ 'value' ] ?? 0 );
$deposit = $value > 0 ;
$payload = Controller :: productUnitPayload ( $product );
if ( Controller :: isCountBasedProduct ( $payload ) && abs ( $value - round ( $value )) > 0.000001 ) {
throw new UnprocessableEntityHttpException ( 'برای محصول تعدادی فقط عدد صحیح مجاز است' );
}
2026-06-23 21:59:38 +03:30
2026-08-18 22:23:20 +03:30
$isMainWalletProduct = Controller :: isMainWalletProduct ( $productId );
if ( ! $deposit && $isMainWalletProduct ) {
$this -> consolidateOrphanedMainWalletGold (( int ) $customer -> id , ( int ) $customer -> type );
}
2026-06-23 21:59:38 +03:30
$effectiveProductId = Controller :: effectiveProductWalletId ( $productId );
$wallet_interface = new WalletInterface (
( int ) $data [ 'customer_id' ],
( int ) $customer -> type ,
null ,
$effectiveProductId
);
$wallet = $wallet_interface -> wallet ;
2026-08-18 22:23:20 +03:30
if ( ! $deposit ) {
$available = is_null ( $wallet ) ? 0.0 : ( float ) $wallet -> gold ;
$resolved = $this -> resolveGoldDischargeAmount (
$available ,
abs ( $value ),
'مقدار موجودی کمتر از درخواست شماست'
);
$value = - $resolved ;
$data [ 'value' ] = $value ;
2026-06-23 21:59:38 +03:30
}
2026-07-26 14:15:26 +03:30
$this -> assertNotDuplicateCharge (( int ) $customer -> id , ( float ) $data [ 'value' ], 'gold' );
2026-06-23 21:59:38 +03:30
$goldBlock = 0 ;
$user = auth () -> user ();
$unitLabel = $this -> resolveProductChargeUnit ( $product );
$Withdraw = Withdraw :: create ([
'user_id' => $user -> id ,
'customer_id' => $customer -> id ,
'gold' => $data [ 'value' ],
'note' => $data [ 'note' ] ?? null ,
'sts' => 4 ,
]);
new TransactionInterface ( $wallet_interface , [
'cash' => 0 ,
'gold' => $data [ 'value' ],
'product_id' => $productId ,
'expire_at' => now (),
'note' => $data [ 'note' ] ?? null ,
'withdraw_id' => $Withdraw -> id ,
'active' => true ,
], 7 , $deposit , false , $goldBlock );
2026-06-25 23:06:29 +03:30
$this -> recordAccountingTransaction (
$customer ,
$Withdraw ,
( int ) $user -> id ,
0 ,
( float ) $data [ 'value' ],
( $data [ 'note' ] ?? null ) ?: ( 'شارژ ' . $product -> title )
);
2026-07-05 16:43:34 +03:30
if ( app ( ConfigService :: class ) -> isTahesabAccounting ()) {
2026-06-23 21:59:38 +03:30
try {
$data [ 'direction' ] = $deposit ? 1 : 0 ;
2026-06-25 22:20:40 +03:30
$data [ 'withdraw_id' ] = $Withdraw -> id ;
2026-07-28 17:44:24 +03:30
$data [ 'product_id' ] = $productId ;
$data [ 'product' ] = [
'id' => $product -> id ,
'title' => $product -> title ,
'group_type' => $product -> group_type ,
'metal_id' => $product -> metal_id ,
'amount' => $product -> amount ,
'unit' => $product -> unit ,
'trade_by_quantity' => $product -> trade_by_quantity ,
];
2026-06-25 22:20:40 +03:30
app ( TahesabController :: class ) -> walletGold ( $data );
2026-06-23 21:59:38 +03:30
} catch ( \Exception $e ) {
2026-06-25 22:20:40 +03:30
report ( $e );
2026-06-23 21:59:38 +03:30
}
}
2026-07-23 20:24:58 +03:30
if ( app ( ConfigService :: class ) -> isZarinAccounting ()) {
try {
$data [ 'direction' ] = $deposit ? 1 : 0 ;
$data [ 'withdraw_id' ] = $Withdraw -> id ;
2026-07-28 17:44:24 +03:30
$data [ 'product_id' ] = $productId ;
$data [ 'product' ] = [
'id' => $product -> id ,
'title' => $product -> title ,
'group_type' => $product -> group_type ,
'metal_id' => $product -> metal_id ,
'amount' => $product -> amount ,
'unit' => $product -> unit ,
'trade_by_quantity' => $product -> trade_by_quantity ,
];
2026-07-23 20:24:58 +03:30
app ( \App\Http\Controllers\Admin\ZarinController :: class ) -> walletGold ( $data );
} catch ( \Exception $e ) {
report ( $e );
}
}
2026-07-20 15:52:42 +03:30
try {
// فقط محصول متصل به کیف اصلی → سند طلای پرگار
app ( PargarSyncService :: class ) -> queueWalletDocument (
$customer ,
'gold' ,
( float ) $data [ 'value' ],
[
'withdraw_id' => $Withdraw -> id ,
'note' => $data [ 'note' ] ?? null ,
'product_id' => $productId ,
'source' => 'admin_product' ,
]
);
} catch ( \Throwable $e ) {
report ( $e );
}
2026-06-23 21:59:38 +03:30
$status = $deposit ? ' شارژ شد ' : ' کسر شد ' ;
$formatted = self :: formatSmsGoldWeight ( $data [ 'value' ]);
2026-07-18 22:22:01 +03:30
if ( self :: isSmsNotifEnabled ( 'charge_notif' )) {
2026-06-23 21:59:38 +03:30
$message = 'کیف پول کاربر ' . $customer -> name . ' به مقدار ' . $formatted . ' ' . $unitLabel . ' در تاریخ ' . verta () -> format ( 'Y/n/j H:i:s' ) . $status . 'بابت' . " \n "
. ( $data [ 'note' ] ?? '' )
. " \n "
. $this -> Config ( 'name_fa' );
User :: where ( 'send_sms' , 1 ) -> get () -> each ( function ( $admin ) use ( $message ) {
$this -> send ( $admin -> mobile , $message );
});
2026-07-18 22:22:01 +03:30
$message = 'کیف پول شما به اندازه ' . $formatted . ' ' . $unitLabel . ' در تاریخ ' . verta () -> format ( 'Y/n/j H:i:s' ) . $status . ' بابت' . " \n "
. ( $data [ 'note' ] ?? '' )
. " \n "
. $this -> Config ( 'name_fa' );
$this -> Send ( $customer -> mobile , $message );
}
2026-07-27 22:09:08 +03:30
try {
$isDeposit = ( bool ) $deposit ;
app ( \App\Services\CustomerAppNotifyService :: class ) -> notifyCustomer (
( int ) $customer -> id ,
$isDeposit ? 'واریز محصول' : 'برداشت محصول' ,
'کیف پول شما به اندازه ' . $formatted . ' ' . $unitLabel
. ( $isDeposit ? ' شارژ شد.' : ' کسر شد.' ),
$isDeposit ? 'success' : 'warning'
);
} catch ( \Throwable $e ) {
report ( $e );
}
2026-06-27 15:51:44 +03:30
$this -> broadcastCustomerWalletAssets ( $customer );
2026-06-23 21:59:38 +03:30
return response () -> json ( true );
}
private function resolveProductChargeUnit ( Product $product ) : string
{
$payload = Controller :: productUnitPayload ( $product );
2026-07-28 17:44:24 +03:30
// تعدادی: همیشه «عدد» — حتی اگر unit محصول وزن هر قطعه (گرم) باشد
if ( Controller :: isCountBasedProduct ( $payload )) {
return 'عدد' ;
2026-06-23 21:59:38 +03:30
}
if ( Controller :: isWeightBasedProductUnit ( $payload [ 'unit' ] ?? null )) {
return trim (( string ) $payload [ 'unit' ]);
}
2026-07-28 17:44:24 +03:30
$unit = trim (( string ) ( $payload [ 'unit' ] ?? '' ));
if ( $unit !== '' ) {
return $unit ;
}
2026-06-23 21:59:38 +03:30
return Controller :: productStorageWeightUnit ( $product );
}
2026-01-24 14:03:11 +00:00
public function index ( Request $request )
{
2026-06-23 21:21:04 +03:30
$balanceReport = app ( BalanceReportService :: class );
$balanceService = app ( WalletBalanceService :: class );
$catalogMetalIds = $balanceService -> catalogMetalIds ();
$records = Transaction :: with ([
'customer' ,
2026-07-21 22:23:09 +03:30
'wallet.product' => static function ( $q ) {
$q -> withTrashed () -> select ([ 'id' , 'title' , 'metal_id' , 'group_type' , 'unit' , 'amount' , 'trade_by_quantity' , 'direct_price' ]);
},
'product' => static function ( $q ) {
$q -> withTrashed () -> select ([ 'id' , 'title' , 'metal_id' , 'group_type' , 'unit' , 'amount' , 'trade_by_quantity' , 'direct_price' ]);
},
2026-06-23 21:21:04 +03:30
]) -> whereNull ( 'fund_id' );
2026-01-24 14:03:11 +00:00
TableEngine :: create ( $records , $request -> all ())
-> initSearch ([ 'order_id' , 'customer.name' ])
-> select ([
'*' , Transaction :: $SELECT_CAJ , Transaction :: $SELECT_TYPE_STR , Transaction :: $SELECT_SAJ , Transaction :: $SELECT_EAJ
]) -> export ( $records );
2026-06-24 01:17:34 +03:30
if ( isset ( $records [ 'rows' ])) {
2026-06-24 01:44:38 +03:30
$records [ 'rows' ] = collect ( $records [ 'rows' ]) -> map ( function ( $row ) use ( $balanceReport , $catalogMetalIds ) {
2026-06-23 21:21:04 +03:30
$balanceReport -> enrichTransactionTableRow ( $row , $catalogMetalIds );
2026-06-24 01:44:38 +03:30
if ( $row instanceof Transaction ) {
$array = $row -> toArray ();
$rawTypeStr = $row -> getAttributes ()[ 'type_str' ] ?? null ;
if ( is_string ( $rawTypeStr ) && ! is_numeric ( trim ( $rawTypeStr ))) {
$array [ 'type_str' ] = $rawTypeStr ;
}
return $array ;
}
return is_array ( $row ) ? $row : ( array ) $row ;
}) -> values () -> all ();
2026-06-24 01:17:34 +03:30
}
2026-06-23 21:21:04 +03:30
$records [ 'catalogMetals' ] = $balanceReport -> catalogMetalsMeta ();
2026-01-24 14:03:11 +00:00
return response () -> json ( $records );
}
public function updateTransaction ( Request $request )
{
$message = 'تراکنش تغیری نکرده.' ;
$transaction = Transaction :: find ( $request -> id );
$product = Product :: find ( $transaction -> product_id );
if ( ! $transaction ) return response () -> json ([ 'error' => 'تراکنش یافت نشد.' ], 404 );
$difference = $request -> input ( 'number' ) - $transaction -> number ;
if ( abs ( $difference ) < 0.0001 ) {
return response () -> json ([ 'message' => $message ], 422 );
}
$transaction -> number = $request -> input ( 'number' );
$wallet_interface = new WalletInterface ( $transaction -> customer_id , 2 );
if ( $wallet_interface -> wallet -> gold < $difference ) {
if ( ! ( isset ( $request -> force ) && $request -> force === true )){
return response () -> json ([ 'message' => "کاربر مورد نظر موجودی ندارد. موجودی فعلی: " . $wallet_interface -> wallet -> gold . " مقدار مورد نیاز: " . $difference , 'type' => 406 ], 422 );
}
}
$isDiposit = $difference > 0 ? 0 : 1 ;
new TransactionInterface ( $wallet_interface , [
"title" => "اصلاح سند " . $transaction -> withdraw_id ,
"note" => $product -> title ,
"product_id" => $product -> id ,
"number" => 0 ,
"gold" => $difference ,
'expire_at' => now (),
'active' => true ,
'withdraw_id' => $transaction -> withdraw_id ,
'start_at' => now (),
], 51 , $isDiposit , false );
$transaction -> save ();
$message = 'تراکنش با موفقیت بهروزرسانی شد.' ;
2026-06-27 15:51:44 +03:30
$customer = Customer :: find ( $transaction -> customer_id );
if ( $customer ) {
$this -> broadcastCustomerWalletAssets ( $customer );
}
2026-01-24 14:03:11 +00:00
return response () -> json ([
'message' => $message ,
'transaction' => $transaction ,
// 'previous_amount' => $previousAmount,
// 'new_amount' => $newAmount,
'difference' => $difference ,
'wallet' => $wallet_interface -> wallet ,
]);
}
public function indexWallet ( Request $request )
{
2026-06-09 20:32:27 +03:30
$lastPrice = ( float ) ( Price :: latest () -> first () ?-> price ?? 0 );
2026-06-23 21:04:29 +03:30
$balanceService = app ( WalletBalanceService :: class );
$catalogMetalIds = $balanceService -> catalogMetalIds ();
$mainWalletMetalId = $balanceService -> resolveMainWalletMetalId ();
2026-06-09 20:32:27 +03:30
// همان فیلتر/جستجوی جدول — بدون صفحهبندی — برای مجموع ردیفهای قابل مشاهده
$sumQuery = Wallet :: query ();
TableEngine :: create ( $sumQuery , $request -> all ())
-> initSearch ([ 'customer.name' ]);
2026-07-01 20:59:24 +03:30
$walletTableWith = [
'customer:id,name,mobile' ,
'fund:id,title' ,
2026-07-21 22:28:04 +03:30
'product' => static function ( $q ) {
$q -> withTrashed () -> select ([ 'id' , 'metal_id' , 'unit' , 'amount' , 'group_type' , 'trade_by_quantity' , 'direct_price' , 'title' ]);
},
2026-07-01 20:59:24 +03:30
];
$sumRows = ( clone $sumQuery ) -> with ( $walletTableWith ) -> get ();
2026-06-23 21:04:29 +03:30
$metalBalances = $balanceService -> buildWalletMetalSums ( $sumRows , $catalogMetalIds );
$totalGold = array_sum ( array_column ( $metalBalances , 'gold' ));
$totalGoldToCash = array_sum ( array_column ( $metalBalances , 'goldToCash' ));
2026-06-09 20:32:27 +03:30
$totalCash = ( float ) ( clone $sumQuery ) -> sum ( 'cash' );
$sums = [
'gold' => round ( $totalGold , 2 ),
2026-06-23 21:04:29 +03:30
'goldToCash' => round ( $totalGoldToCash , 2 ),
2026-06-09 20:32:27 +03:30
'cash' => round ( $totalCash , 2 ),
'cashToGold' => $lastPrice > 0 ? round ( $totalCash / $lastPrice , 2 ) : 0 ,
2026-06-23 21:04:29 +03:30
'metalBalances' => $metalBalances ,
2026-06-09 20:32:27 +03:30
];
2026-07-01 20:59:24 +03:30
$records = Wallet :: with ( $walletTableWith );
2026-01-24 14:03:11 +00:00
TableEngine :: create ( $records , $request -> all ())
2026-07-01 20:59:24 +03:30
-> initSearch ([ 'customer.name' , 'customer.mobile' , 'product.title' , 'fund.title' ])
2026-01-24 14:03:11 +00:00
-> select ([ '*' ])
-> export ( $records );
2026-06-23 21:04:29 +03:30
if ( isset ( $records [ 'rows' ])) {
foreach ( $records [ 'rows' ] as & $row ) {
$balanceService -> enrichWalletTableRow ( $row , $catalogMetalIds , $mainWalletMetalId );
}
unset ( $row );
}
2026-01-24 14:03:11 +00:00
$records [ 'sums' ] = $sums ;
return response () -> json ( $records );
}
public function indexWithdraw ( Request $request )
{
2026-07-27 16:05:54 +03:30
$records = Withdraw :: with ([ 'customer' , 'bank_account' , 'address' , "Transaction" ]);
2026-06-09 20:43:01 +03:30
Withdraw :: applyFlowFilter ( $records , $request -> input ( 'flow_type' , 'all' ));
2026-01-24 14:03:11 +00:00
TableEngine :: create ( $records , $request -> all ())
-> initSearch ([ 'customer.name' ])
-> select ([
'*' , Withdraw :: $SELECT_STS_STR , Withdraw :: $SELECT_DAJ
]) -> export ( $records );
2026-06-25 22:20:40 +03:30
2026-07-05 16:43:34 +03:30
if ( app ( ConfigService :: class ) -> isTahesabAccounting () && isset ( $records [ 'rows' ]) && is_array ( $records [ 'rows' ])) {
2026-06-25 22:20:40 +03:30
$withdrawIds = collect ( $records [ 'rows' ]) -> pluck ( 'id' ) -> filter () -> values () -> all ();
if ( $withdrawIds !== []) {
$failedByWithdraw = \App\Models\Tahesab :: query ()
-> whereIn ( 'withdraw_id' , $withdrawIds )
-> where ( function ( $query ) {
$query -> whereNull ( 'tahesab_id' );
if ( \Illuminate\Support\Facades\Schema :: hasColumn ( 'tahesab' , 'sync_status' )) {
$query -> orWhere ( 'sync_status' , \App\Services\TahesabSyncService :: STATUS_FAILED );
}
})
-> orderByDesc ( 'id' )
-> get ()
-> unique ( 'withdraw_id' )
-> keyBy ( 'withdraw_id' );
foreach ( $records [ 'rows' ] as & $row ) {
$tahesab = $failedByWithdraw -> get ( $row [ 'id' ] ?? null );
if ( $tahesab ) {
$row [ 'tahesab_failed_id' ] = $tahesab -> id ;
}
}
unset ( $row );
}
}
2026-07-23 20:24:58 +03:30
if ( app ( ConfigService :: class ) -> isZarinAccounting () && isset ( $records [ 'rows' ]) && is_array ( $records [ 'rows' ])) {
$withdrawIds = collect ( $records [ 'rows' ]) -> pluck ( 'id' ) -> filter () -> values () -> all ();
if ( $withdrawIds !== []) {
$failedByWithdraw = \App\Models\Zarin :: query ()
-> whereIn ( 'withdraw_id' , $withdrawIds )
-> where ( function ( $query ) {
$query -> whereNull ( 'zarin_id' );
if ( \Illuminate\Support\Facades\Schema :: hasColumn ( 'zarin' , 'sync_status' )) {
$query -> orWhere ( 'sync_status' , \App\Services\ZarinSyncService :: STATUS_FAILED );
}
})
-> orderByDesc ( 'id' )
-> get ()
-> unique ( 'withdraw_id' )
-> keyBy ( 'withdraw_id' );
foreach ( $records [ 'rows' ] as & $row ) {
$zarin = $failedByWithdraw -> get ( $row [ 'id' ] ?? null );
if ( $zarin ) {
$row [ 'zarin_failed_id' ] = $zarin -> id ;
}
}
unset ( $row );
}
}
2026-01-24 14:03:11 +00:00
return response () -> json ( $records );
}
2026-06-09 20:43:01 +03:30
public function exportWithdrawReport ( Request $request )
{
$flowType = $request -> input ( 'flow_type' , 'all' );
$query = Withdraw :: with ([ 'customer' , 'bank_account' ]);
Withdraw :: applyFlowFilter ( $query , $flowType );
TableEngine :: create ( $query , $request -> all ())
-> initSearch ([ 'customer.name' ]);
$rows = $query -> orderBy ( 'id' , 'desc' ) -> get ();
$titles = [
'all' => 'گزارش برداشت و واریز' ,
'withdraw' => 'گزارش برداشت' ,
'deposit' => 'گزارش واریز' ,
];
return response () -> view ( 'admin.withdrawReport' , [
'rows' => $rows ,
'title' => $titles [ $flowType ] ?? $titles [ 'all' ],
'flowType' => $flowType ,
'generatedAt' => Verta :: now () -> format ( 'Y/n/j H:i:s' ),
]) -> header ( 'Content-Type' , 'text/html; charset=UTF-8' );
}
2026-01-24 14:03:11 +00:00
public function updateWithdraw ( Request $request , $id )
{
2026-07-16 17:29:18 +03:30
$withdraw = Withdraw :: with ([ 'Transaction.product' , 'customer' ]) -> find ( $id );
2026-01-24 14:03:11 +00:00
if ( ! $withdraw ) {
return response () -> json ([ 'error' => 'Withdraw not found' ], 404 );
}
2026-07-16 17:29:18 +03:30
$newSts = ( int ) $request -> sts ;
$oldSts = ( int ) $withdraw -> sts ;
if (
Withdraw :: isPhysicalWithdrawNote ( $withdraw -> note )
&& $newSts === 52
&& in_array ( $oldSts , [ 50 , 51 ], true )
) {
$this -> refundPhysicalWithdrawGold ( $withdraw );
}
$withdraw -> sts = $newSts ;
2026-01-24 14:03:11 +00:00
$withdraw -> save ();
2026-07-16 17:29:18 +03:30
if ( $withdraw -> customer ) {
$this -> broadcastCustomerWalletAssets ( $withdraw -> customer );
}
2026-01-24 14:03:11 +00:00
return response () -> json ([ 'message' => 'Withdraw updated successfully' , 'withdraw' => $withdraw ]);
}
2026-07-16 17:29:18 +03:30
/**
* برگشت موجودی کسرشدهٔ برداشت فیزیکی هنگام لغو (sts=52).
*/
protected function refundPhysicalWithdrawGold ( Withdraw $withdraw ) : void
{
$alreadyRefunded = Transaction :: query ()
-> where ( 'withdraw_id' , $withdraw -> id )
-> where ( 'note' , 'like' , '%لغو سند برداشت فیزیکی%' )
-> exists ();
if ( $alreadyRefunded ) {
return ;
}
$customer = $withdraw -> customer ?: Customer :: find ( $withdraw -> customer_id );
if ( ! $customer ) {
return ;
}
$assetSummaryService = app ( AssetSummaryService :: class );
foreach ( $withdraw -> Transaction as $transaction ) {
// فقط تراکنشهای کسر (منفی) را برگردان — نه تراکنشهای برگشت قبلی
$gold = ( float ) ( $transaction -> gold ?? 0 );
if ( $gold >= 0 ) {
continue ;
}
$amount = abs ( $gold );
if ( $amount < 0.000001 ) {
continue ;
}
$product = $transaction -> product ;
$productId = ( int ) ( $transaction -> product_id ?? ( $product -> id ?? 0 ));
if ( $productId <= 0 ) {
continue ;
}
$effectiveProductId = $assetSummaryService -> effectiveProductWalletIdForCustomer (
( int ) $customer -> id ,
( int ) $customer -> type ,
$productId
);
$walletInterface = new WalletInterface (
( int ) $customer -> id ,
( int ) $customer -> type ,
null ,
$effectiveProductId
);
$title = $product -> title ?? 'محصول' ;
new TransactionInterface ( $walletInterface , [
'gold' => $amount ,
'number' => $transaction -> number ?? null ,
'product_id' => $productId ,
'expire_at' => now (),
'active' => true ,
'title' => "برگشت برداشت فیزیکی { $title } " ,
'note' => "لغو سند برداشت فیزیکی شماره { $withdraw -> id } " ,
'withdraw_id' => $withdraw -> id ,
], 6 , true );
}
}
2026-01-24 14:03:11 +00:00
public function getWithdraw ( Request $request , $id )
{
2026-07-27 16:05:54 +03:30
$withdraw = Withdraw :: with ([ 'customer' , 'bank_account' , 'address' , "Transaction" ]) -> find ( $id );
2026-01-24 14:03:11 +00:00
if ( ! $withdraw ) {
return response () -> json ([ 'error' => 'Withdraw not found' ], 404 );
}
2026-06-09 20:08:06 +03:30
return response () -> json ( $withdraw );
2026-01-24 14:03:11 +00:00
}
2026-07-08 11:25:19 +03:30
public function indexPhysicalWithdraw ( Request $request )
{
2026-07-27 16:05:54 +03:30
$records = Withdraw :: with ([ 'customer' , 'address' , 'Transaction.product' ]);
2026-07-08 11:25:19 +03:30
Withdraw :: scopePhysicalWithdrawals ( $records );
TableEngine :: create ( $records , $request -> all ())
-> initSearch ([ 'customer.name' , 'customer.mobile' ])
-> select ([
'*' , Withdraw :: $SELECT_STS_STR , Withdraw :: $SELECT_DAJ
]) -> export ( $records );
return response () -> json ( $records );
}
public function physicalWithdrawOptions ( $customerId , AssetSummaryService $assetSummaryService )
{
$customer = Customer :: findOrFail ( $customerId );
$summary = $assetSummaryService -> buildForCustomer (( int ) $customer -> id , ( int ) $customer -> type );
$inventoryByProduct = collect ( $summary [ 'inventory_cards' ] ?? []) -> keyBy ( 'product_id' );
$configService = app ( ConfigService :: class );
$mainWalletProductIds = $configService -> mainWalletProductIds ();
$products = Product :: where ( function ( $query ) use ( $customer , $mainWalletProductIds ) {
$query -> whereHas ( 'wallet' , function ( $q ) use ( $customer ) {
$q -> where ( 'owner' , $customer -> id );
});
if ( $mainWalletProductIds !== []) {
$query -> orWhereIn ( 'id' , $mainWalletProductIds );
}
}) -> with ([ 'wallet' => function ( $query ) use ( $customer ) {
$query -> where ( 'owner' , $customer -> id );
}]) -> get ();
$representativeId = null ;
if ( $mainWalletProductIds !== []) {
foreach ( $mainWalletProductIds as $pid ) {
if ( $products -> firstWhere ( 'id' , $pid )) {
$representativeId = ( int ) $pid ;
break ;
}
}
$products = $products -> filter ( function ( $product ) use ( $mainWalletProductIds , $representativeId ) {
if ( in_array (( int ) $product -> id , array_map ( 'intval' , $mainWalletProductIds ), true )) {
return ( int ) $product -> id === $representativeId ;
}
return true ;
}) -> values ();
}
$productRows = $products -> map ( function ( $product ) use ( $customer , $assetSummaryService , $inventoryByProduct , $representativeId ) {
$card = $inventoryByProduct -> get (( int ) $product -> id );
$displayQty = $card !== null ? ( float ) ( $card [ 'quantity' ] ?? 0 ) : 0.0 ;
$walletInterface = new WalletInterface (
( int ) $customer -> id ,
( int ) $customer -> type ,
null ,
$assetSummaryService -> effectiveProductWalletIdForCustomer (
( int ) $customer -> id ,
( int ) $customer -> type ,
( int ) $product -> id
)
);
$wallet = $walletInterface -> wallet ;
if ( ! $wallet ) {
return null ;
}
return [
'product_id' => ( int ) $product -> id ,
'title' => ( string ) $product -> title ,
'wallet_id' => ( int ) $wallet -> id ,
'available' => $displayQty ,
2026-07-16 17:29:18 +03:30
'unit' => self :: productQuantityUnit (
self :: productUnitPayload ( $product )
),
2026-07-08 11:25:19 +03:30
'is_main_wallet' => $representativeId !== null && ( int ) $product -> id === $representativeId ,
];
}) -> filter ( function ( $row ) {
return $row !== null && ( $row [ 'available' ] ?? 0 ) > 0 && $row [ 'title' ] !== 'آبشده نقدی' ;
}) -> values ();
$addresses = Address :: query ()
-> where ( 'customer_id' , $customer -> id )
-> orderByDesc ( 'default_address' )
-> orderByDesc ( 'id' )
-> get ([ 'id' , 'province' , 'city' , 'address' , 'phone' , 'zip' , 'default_address' ]);
return response () -> json ([
'products' => $productRows ,
'addresses' => $addresses ,
]);
}
public function createPhysicalWithdraw ( Request $request , AssetSummaryService $assetSummaryService )
{
$data = $request -> validate ([
'customer_id' => 'required|integer|exists:customers,id' ,
'address_id' => 'required|integer|exists:addresses,id' ,
'wallets' => 'required|array|min:1' ,
'wallets.*.wallet_id' => 'required|integer|exists:wallets,id' ,
'wallets.*.value' => 'required|numeric|min:0.0001' ,
]);
$customer = Customer :: findOrFail ( $data [ 'customer_id' ]);
$address = Address :: find ( $data [ 'address_id' ]);
if ( ! $address || ( int ) $address -> customer_id !== ( int ) $customer -> id ) {
return response () -> json ([ 'message' => 'آدرس معتبر نیست یا متعلق به این مشتری نمیباشد' ], 403 );
}
$withdraw = DB :: transaction ( function () use ( $data , $customer , $address , $assetSummaryService ) {
$withdraw = new Withdraw ();
$withdraw -> note = 'برداشت فیزیکی' ;
$withdraw -> address_id = $address -> id ;
$withdraw -> customer_id = $customer -> id ;
$withdraw -> user_id = auth ( 'user' ) -> id ();
$withdraw -> sts = 50 ;
$withdraw -> save ();
foreach ( $data [ 'wallets' ] as $walletRow ) {
$wallet = Wallet :: with ( 'product' ) -> findOrFail ( $walletRow [ 'wallet_id' ]);
if (( int ) $wallet -> owner !== ( int ) $customer -> id ) {
throw new UnprocessableEntityHttpException ( 'کیف پول انتخابشده متعلق به این مشتری نیست' );
}
if ( ! $wallet -> product ) {
throw new UnprocessableEntityHttpException ( 'محصولی در کیف پول مورد نظر یافت نشد' );
}
$productId = ( int ) $wallet -> product_id ;
$value = ( float ) $walletRow [ 'value' ];
$effectiveProductId = $assetSummaryService -> effectiveProductWalletIdForCustomer (
( int ) $customer -> id ,
( int ) $customer -> type ,
$productId
);
$availableGold = $assetSummaryService -> productDisplayQuantity (
( int ) $customer -> id ,
( int ) $customer -> type ,
$productId
);
if ( $value > $availableGold ) {
throw new UnprocessableEntityHttpException (
"مشتری به اندازه کافی از { $wallet -> product -> title } ندارد"
);
}
$walletInterface = new WalletInterface (
( int ) $customer -> id ,
( int ) $customer -> type ,
null ,
$effectiveProductId
);
new TransactionInterface ( $walletInterface , [
'gold' => $value ,
'number' => ( float ) $wallet -> product -> amount * $value ,
'product_id' => $wallet -> product -> id ,
'expire_at' => now (),
'active' => true ,
'title' => " برداشت { $wallet -> product -> title } " ,
'note' => " برداشت محصول { $wallet -> product -> title } شماره سند { $withdraw -> id } " ,
'withdraw_id' => $withdraw -> id ,
], 6 , false );
}
return $withdraw ;
});
2026-07-28 17:44:24 +03:30
foreach ( $data [ 'wallets' ] as $walletRow ) {
$wallet = Wallet :: with ( 'product' ) -> find ( $walletRow [ 'wallet_id' ] ?? 0 );
$product = $wallet ?-> product ;
if ( ! $product ) {
continue ;
}
try {
app ( \App\Services\AccountingWalletPush :: class ) -> pushProductWalletGold (
$customer ,
$product ,
( float ) $walletRow [ 'value' ],
false ,
( int ) $withdraw -> id ,
'برداشت فیزیکی ' . $product -> title
);
} catch ( \Throwable $e ) {
report ( $e );
}
}
2026-07-08 11:25:19 +03:30
$this -> broadcastCustomerWalletAssets ( $customer );
return response () -> json ([ 'id' => $withdraw -> id ]);
}
public function adjustPhysicalWithdraw ( Request $request , $id , AssetSummaryService $assetSummaryService )
{
$payload = $request -> validate ([
2026-07-08 17:58:55 +03:30
'items' => 'required|array|min:1' ,
'items.*.transaction_id' => 'required|integer|exists:transactions,id' ,
'items.*.value' => 'required|numeric|min:0.0001' ,
2026-07-08 11:25:19 +03:30
]);
2026-07-08 17:58:55 +03:30
$withdraw = Withdraw :: with ([ 'Transaction.product' , 'customer' ]) -> findOrFail ( $id );
2026-07-08 11:25:19 +03:30
if ( ! Withdraw :: isPhysicalWithdrawNote ( $withdraw -> note )) {
return response () -> json ([ 'message' => 'این سند برداشت فیزیکی نیست' ], 422 );
}
if (( int ) $withdraw -> sts !== 50 ) {
return response () -> json ([ 'message' => 'تغییر مقدار فقط در وضعیت «در انتظار ارسال» مجاز است' ], 422 );
}
$customer = Customer :: findOrFail ( $withdraw -> customer_id );
2026-07-08 17:58:55 +03:30
$transactionsById = $withdraw -> Transaction -> keyBy ( 'id' );
2026-07-08 11:25:19 +03:30
2026-07-08 17:58:55 +03:30
DB :: transaction ( function () use (
$payload ,
$withdraw ,
$customer ,
$transactionsById ,
$assetSummaryService
) {
foreach ( $payload [ 'items' ] as $item ) {
$transaction = $transactionsById -> get (( int ) $item [ 'transaction_id' ]);
if ( ! $transaction ) {
throw new UnprocessableEntityHttpException ( 'تراکنش به این سند تعلق ندارد' );
}
$product = $transaction -> product ;
if ( ! $product ) {
throw new UnprocessableEntityHttpException ( 'محصول تراکنش یافت نشد' );
}
$newValue = round (( float ) $item [ 'value' ], 6 );
$currentUnits = round ( abs (( float ) $transaction -> gold ), 6 );
if ( abs ( $newValue - $currentUnits ) < 0.000001 ) {
continue ;
}
2026-07-16 17:29:18 +03:30
$isCountBased = self :: isCountBasedProduct ( self :: productUnitPayload ( $product ));
$minValue = $isCountBased ? 1.0 : 0.001 ;
2026-07-08 17:58:55 +03:30
if ( $newValue < $minValue ) {
throw new UnprocessableEntityHttpException (
"حداقل مقدار برای { $product -> title } برابر { $minValue } است"
);
}
2026-07-16 17:29:18 +03:30
if ( $isCountBased && abs ( $newValue - round ( $newValue )) > 0.000001 ) {
2026-07-08 17:58:55 +03:30
throw new UnprocessableEntityHttpException (
"مقدار { $product -> title } باید عدد صحیح باشد"
);
}
$delta = round ( $newValue - $currentUnits , 6 );
$effectiveProductId = $assetSummaryService -> effectiveProductWalletIdForCustomer (
( int ) $customer -> id ,
( int ) $customer -> type ,
( int ) $product -> id
);
$walletInterface = new WalletInterface (
( int ) $customer -> id ,
( int ) $customer -> type ,
null ,
$effectiveProductId
);
$wallet = $walletInterface -> wallet ;
if ( ! $wallet ) {
throw new UnprocessableEntityHttpException ( 'کیف پول مشتری یافت نشد' );
}
if ( $delta > 0 ) {
$available = $assetSummaryService -> productDisplayQuantity (
( int ) $customer -> id ,
( int ) $customer -> type ,
( int ) $product -> id
);
if ( $delta > $available + 0.000001 ) {
throw new UnprocessableEntityHttpException (
"موجودی { $product -> title } کافی نیست. حداکثر قابل افزودن: { $available } "
);
}
$wallet -> gold -= $delta ;
} else {
$wallet -> gold += abs ( $delta );
}
$transaction -> gold = - $newValue ;
$transaction -> number = ( float ) $product -> amount * $newValue ;
$transaction -> balance_gold = $wallet -> gold ;
$wallet -> save ();
$transaction -> save ();
$walletInterface -> set_hash ();
2026-07-08 11:25:19 +03:30
}
2026-07-08 17:58:55 +03:30
});
2026-07-08 11:25:19 +03:30
$this -> broadcastCustomerWalletAssets ( $customer );
return response () -> json ([
2026-07-08 17:58:55 +03:30
'message' => 'مقادیر برداشت بهروزرسانی شد' ,
'withdraw' => $withdraw -> fresh ([ 'Transaction.product' , 'customer' ]),
]);
}
public function physicalWithdrawAdjustInfo ( $id , AssetSummaryService $assetSummaryService )
{
$withdraw = Withdraw :: with ([ 'Transaction.product' , 'customer' ]) -> findOrFail ( $id );
if ( ! Withdraw :: isPhysicalWithdrawNote ( $withdraw -> note )) {
return response () -> json ([ 'message' => 'این سند برداشت فیزیکی نیست' ], 422 );
}
if (( int ) $withdraw -> sts !== 50 ) {
return response () -> json ([ 'message' => 'تغییر مقدار فقط در وضعیت «در انتظار ارسال» مجاز است' ], 422 );
}
$customer = $withdraw -> customer ;
if ( ! $customer ) {
return response () -> json ([ 'message' => 'مشتری یافت نشد' ], 422 );
}
$lines = $withdraw -> Transaction -> map ( function ( Transaction $transaction ) use (
$customer ,
$assetSummaryService
) {
$product = $transaction -> product ;
$current = round ( abs (( float ) $transaction -> gold ), 6 );
$availableAdd = $product
? $assetSummaryService -> productDisplayQuantity (
( int ) $customer -> id ,
( int ) $customer -> type ,
( int ) $product -> id
)
: 0.0 ;
2026-07-16 17:29:18 +03:30
$isCountBased = $product
? self :: isCountBasedProduct ( self :: productUnitPayload ( $product ))
: false ;
2026-07-08 17:58:55 +03:30
return [
'transaction_id' => ( int ) $transaction -> id ,
'title' => trim (( string ) $transaction -> title ) ?: ( $product -> title ?? 'محصول' ),
'current_value' => $current ,
'available_add' => round (( float ) $availableAdd , 6 ),
'max_value' => round ( $current + ( float ) $availableAdd , 6 ),
2026-07-16 17:29:18 +03:30
'min_value' => $isCountBased ? 1 : 0.001 ,
'allows_decimal' => ! $isCountBased ,
2026-07-08 17:58:55 +03:30
'unit' => $product
2026-07-16 17:29:18 +03:30
? self :: productQuantityUnit ( self :: productUnitPayload ( $product ))
2026-07-08 17:58:55 +03:30
: 'گرم' ,
];
}) -> values ();
return response () -> json ([
'withdraw_id' => ( int ) $withdraw -> id ,
'customer_name' => ( string ) ( $customer -> name ?? '' ),
'customer_mobile' => ( string ) ( $customer -> mobile ?? '' ),
'lines' => $lines ,
2026-07-08 11:25:19 +03:30
]);
}
2026-01-24 14:03:11 +00:00
public function completeWithdraw ( Request $request )
{
2026-06-09 20:08:06 +03:30
$request -> validate ([
'id' => 'required|integer|exists:withdraws,id' ,
'tracked_number' => 'required|string|max:50' ,
'date_at' => 'nullable|date' ,
]);
$data = $request -> only ([ 'tracked_number' , 'date_at' ]);
2026-01-24 14:03:11 +00:00
$data [ 'user_id' ] = auth ( 'user' ) -> id ();
$data [ 'sts' ] = 2 ;
2026-06-09 20:08:06 +03:30
$data [ 'tracked_number' ] = ( string ) $data [ 'tracked_number' ];
if ( empty ( $data [ 'date_at' ])) {
$data [ 'date_at' ] = now ();
}
$withdraw = Withdraw :: findOrFail ( $request -> id );
2026-01-24 14:03:11 +00:00
$withdraw -> update ( $data );
// $tahesab = new TahesabController();
// $tahesab->wallet([
// 'customer_id' => $withdraw->customer_id,
// 'value' => $withdraw->cash,
// 'direction' => 1,
// 'note' => 'برداشت از کیف پول شماره ' . $withdraw->id,
// ]);
return response () -> json ( true );
}
2026-06-25 23:06:29 +03:30
private function recordAccountingTransaction (
Customer $customer ,
Withdraw $withdraw ,
int $adminUserId ,
float $cash = 0 ,
float $gold = 0 ,
? string $note = null
) : void {
$transaction = Transaction :: where ( 'withdraw_id' , $withdraw -> id ) -> latest ( 'id' ) -> first ();
TransactionsAdmin :: create ([
'customer_id' => $customer -> id ,
'customer_to_id' => $customer -> id ,
'user_id' => $adminUserId ,
'wallet' => null ,
'transaction_id' => $transaction ?-> id ,
'cash' => $cash ,
'note' => $note ?? $withdraw -> note ,
'type' => 1 ,
'gold' => $gold ,
'balance_cash' => 0 ,
'withdraw_id' => $withdraw -> id ,
]);
}
2026-06-27 15:51:44 +03:30
private function broadcastCustomerWalletAssets ( Customer $customer ) : void
{
app ( WalletBroadcastService :: class ) -> broadcastForCustomer (
( int ) $customer -> id ,
( int ) $customer -> type
);
}
2026-01-24 14:03:11 +00:00
}