Alerts now bind to a product_id and share one evaluator; displayed gold within a rounding step discharges the full wallet. Co-authored-by: Cursor <cursoragent@cursor.com>
1485 lines
59 KiB
PHP
1485 lines
59 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\DB\Te\TableEngine;
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Address;
|
|
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;
|
|
use App\Models\Setting;
|
|
use App\Models\User;
|
|
use App\Models\TransactionsAdmin;
|
|
use App\Services\AssetSummaryService;
|
|
use App\Services\BalanceReportService;
|
|
use App\Services\ConfigService;
|
|
use App\Services\PargarSyncService;
|
|
use App\Services\WalletBalanceService;
|
|
use App\Services\WalletBroadcastService;
|
|
use Hekmatinasser\Verta\Verta;
|
|
|
|
|
|
class WalletController extends Controller
|
|
{
|
|
/** موجودی طلا با سه رقم اعشار نمایش داده میشود؛ نصف پلهٔ نمایش = بیشترین اختلاف ممکن با مقدار واقعی */
|
|
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(
|
|
'همین سند لحظاتی پیش برای این مشتری ثبت شده است. اگر تکرارش عمدی است یک دقیقه صبر کنید.'
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* عدد نمایشی موجودی تا ۳ رقم گرد میشود؛ اگر ادمین همان عدد را بزند، کل ماندهٔ واقعی کسر شود.
|
|
*/
|
|
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);
|
|
}
|
|
}
|
|
|
|
public function charge2(Request $request)
|
|
{
|
|
$data = $request->all();
|
|
$customer = Customer::findOrFail($data['customer_id']);
|
|
$deposit = false;
|
|
if ($data['value'] > 0) {
|
|
$deposit = true;
|
|
}
|
|
$wallet_interface = new WalletInterface($data['customer_id'], (int) $customer->type);
|
|
$wallet = $wallet_interface->wallet;
|
|
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);
|
|
|
|
try {
|
|
app(PargarSyncService::class)->queueWalletDocument(
|
|
$customer,
|
|
'cash',
|
|
(float) $data['value'],
|
|
[
|
|
'withdraw_id' => $Withdraw->id,
|
|
'source' => 'admin_charge2',
|
|
]
|
|
);
|
|
} catch (\Throwable $e) {
|
|
report($e);
|
|
}
|
|
|
|
$message = 'کیف پول شما به مبلغ ' . self::formatSmsRialAmount($data['value']) . " ریال در تاریخ " . verta()->format('Y/n/j H:i:s') . " شارژ شد " . "\n"
|
|
. $this->Config('name_fa');
|
|
$this->Send($customer->mobile, $message);
|
|
try {
|
|
app(\App\Services\CustomerAppNotifyService::class)->notifyCustomer(
|
|
(int) $customer->id,
|
|
'واریز به کیف پول',
|
|
'کیف پول شما به مبلغ ' . self::formatSmsRialAmount($data['value']) . ' ریال شارژ شد.',
|
|
'success'
|
|
);
|
|
} catch (\Throwable $e) {
|
|
report($e);
|
|
}
|
|
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;
|
|
}
|
|
$wallet_interface = new WalletInterface($data['customer_id'], (int) $customer->type);
|
|
$wallet = $wallet_interface->wallet;
|
|
if (is_null($wallet) && !$deposit) {
|
|
throw new UnprocessableEntityHttpException('مقدار موجودی کمتر از درخواست شماست');
|
|
}
|
|
if (!is_null($wallet) && $wallet->cash < abs($data['value']) && !$deposit) {
|
|
throw new UnprocessableEntityHttpException('مقدار موجودی کمتر از درخواست شماست');
|
|
}
|
|
$this->assertNotDuplicateCharge((int) $customer->id, (float) $data['value'], 'cash');
|
|
$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,
|
|
]);
|
|
$type = $deposit ? 3 : 4;
|
|
new TransactionInterface($wallet_interface, $_inp = [
|
|
'gold' => 0,
|
|
'cash' => $data['value'],
|
|
'expire_at' => now(),
|
|
"note" => $data['note'],
|
|
'withdraw_id' => $Withdraw->id,
|
|
'active' => true
|
|
], $type, true, $deposit, $gold_block);
|
|
|
|
$this->recordAccountingTransaction($customer, $Withdraw, (int) $user->id, (float) $data['value'], 0, $data['note'] ?? null);
|
|
|
|
if (app(ConfigService::class)->isTahesabAccounting()) {
|
|
try {
|
|
$data['direction'] = $deposit ? 0 : 1;
|
|
$data['withdraw_id'] = $Withdraw->id;
|
|
app(TahesabController::class)->wallet($data);
|
|
} catch (\Exception $e) {
|
|
report($e);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
//send sms to users and customer
|
|
$status = $data['value'] > 0 ?" شارژ شد ": " کسر شد ";
|
|
if (self::isSmsNotifEnabled('charge_notif')) {
|
|
$message = 'کیف پول کاربر ' . $customer->name . ' به مقدار ' . self::formatSmsRialAmount($data['value']) . " ریال در تاریخ " . 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, $data) {
|
|
$this->send($admin->mobile, $message);
|
|
});
|
|
|
|
$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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
$this->broadcastCustomerWalletAssets($customer);
|
|
|
|
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;
|
|
|
|
}
|
|
if (!$deposit) {
|
|
$this->consolidateOrphanedMainWalletGold((int) $customer->id, (int) $customer->type);
|
|
}
|
|
$wallet_interface = new WalletInterface($data['customer_id'], (int) $customer->type);
|
|
$wallet = $wallet_interface->wallet;
|
|
// کسر دستی نباید موجودی طلا را منفی کند؛ شارژ مثبت بدون هیچ شرطی باز است
|
|
if (!$deposit) {
|
|
$available = is_null($wallet) ? 0.0 : (float) $wallet->gold;
|
|
$requested = abs((float) $data['value']);
|
|
$resolved = $this->resolveGoldDischargeAmount($available, $requested, 'مقدار موجودی طلا کمتر از درخواست شماست');
|
|
$data['value'] = -$resolved;
|
|
}
|
|
$this->assertNotDuplicateCharge((int) $customer->id, (float) $data['value'], 'gold');
|
|
$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,
|
|
]);
|
|
$representative = app(ConfigService::class)->mainWalletRepresentativeProduct();
|
|
new TransactionInterface($wallet_interface, $_inp = [
|
|
'cash' => 0,
|
|
'gold' => $data['value'],
|
|
'expire_at' => now(),
|
|
"note" => $data['note'],
|
|
'withdraw_id' => $Withdraw->id,
|
|
'product_id' => $representative?->id,
|
|
'active' => true
|
|
], 7, $deposit, false, $gold_block);
|
|
|
|
$this->recordAccountingTransaction($customer, $Withdraw, (int) $user->id, 0, (float) $data['value'], $data['note'] ?? null);
|
|
|
|
//send to tahesab
|
|
if (app(ConfigService::class)->isTahesabAccounting()) {
|
|
try {
|
|
$data['direction'] = $deposit ? 1 : 0;
|
|
$data['withdraw_id'] = $Withdraw->id;
|
|
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,
|
|
];
|
|
}
|
|
app(TahesabController::class)->walletGold($data);
|
|
} catch (\Exception $e) {
|
|
report($e);
|
|
}
|
|
}
|
|
|
|
if (app(ConfigService::class)->isZarinAccounting()) {
|
|
try {
|
|
$data['direction'] = $deposit ? 1 : 0;
|
|
$data['withdraw_id'] = $Withdraw->id;
|
|
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,
|
|
];
|
|
}
|
|
app(\App\Http\Controllers\Admin\ZarinController::class)->walletGold($data);
|
|
} catch (\Exception $e) {
|
|
report($e);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
//send sms to users and customer
|
|
$status = $data['value'] > 0 ?" شارژ شد ": " کسر شد ";
|
|
$weightUnit = self::tradeWeightUnit();
|
|
if (self::isSmsNotifEnabled('charge_notif')) {
|
|
$message = 'کیف پول کاربر ' . $customer->name . ' به مقدار ' . self::formatSmsGoldWeight($data['value']) . " {$weightUnit} در تاریخ " . 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, $data) {
|
|
$this->send($admin->mobile, $message);
|
|
});
|
|
|
|
$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);
|
|
}
|
|
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);
|
|
}
|
|
$this->broadcastCustomerWalletAssets($customer);
|
|
|
|
return response()->json(true);
|
|
}
|
|
|
|
/** گزینههای شارژ کیف پول — نقدی + همهٔ محصولات (فعال و غیرفعال) */
|
|
public function chargeTargets()
|
|
{
|
|
$configService = app(ConfigService::class);
|
|
$mainWalletProductIds = $configService->mainWalletProductIds();
|
|
$balanceReport = app(BalanceReportService::class);
|
|
|
|
$targets = [
|
|
[
|
|
'key' => 'money',
|
|
'kind' => 'money',
|
|
'label' => 'نقدی (ریال)',
|
|
'unit' => 'ریال',
|
|
'active' => true,
|
|
],
|
|
];
|
|
|
|
$products = Product::query()
|
|
->orderByDesc('active')
|
|
->orderBy('metal_id')
|
|
->orderBy('id')
|
|
->get(['id', 'title', 'unit', 'metal_id', 'group_type', 'trade_by_quantity', 'amount', 'direct_price', 'active']);
|
|
|
|
foreach ($products as $product) {
|
|
$usesMainWallet = in_array((int) $product->id, $mainWalletProductIds, true);
|
|
$label = (string) $product->title;
|
|
if ($usesMainWallet) {
|
|
$label .= ' (کیف اصلی)';
|
|
}
|
|
if (!(bool) $product->active) {
|
|
$label .= ' (غیرفعال)';
|
|
}
|
|
|
|
$payload = Controller::productUnitPayload($product);
|
|
$isCountBased = Controller::isCountBasedProduct($payload);
|
|
|
|
$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,
|
|
'allows_decimal' => !$isCountBased,
|
|
'group_type' => (string) ($product->group_type ?? ''),
|
|
'active' => (bool) $product->active,
|
|
];
|
|
}
|
|
|
|
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']);
|
|
$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('برای محصول تعدادی فقط عدد صحیح مجاز است');
|
|
}
|
|
|
|
$isMainWalletProduct = Controller::isMainWalletProduct($productId);
|
|
if (!$deposit && $isMainWalletProduct) {
|
|
$this->consolidateOrphanedMainWalletGold((int) $customer->id, (int) $customer->type);
|
|
}
|
|
|
|
$effectiveProductId = Controller::effectiveProductWalletId($productId);
|
|
$wallet_interface = new WalletInterface(
|
|
(int) $data['customer_id'],
|
|
(int) $customer->type,
|
|
null,
|
|
$effectiveProductId
|
|
);
|
|
$wallet = $wallet_interface->wallet;
|
|
|
|
if (!$deposit) {
|
|
$available = is_null($wallet) ? 0.0 : (float) $wallet->gold;
|
|
$resolved = $this->resolveGoldDischargeAmount(
|
|
$available,
|
|
abs($value),
|
|
'مقدار موجودی کمتر از درخواست شماست'
|
|
);
|
|
$value = -$resolved;
|
|
$data['value'] = $value;
|
|
}
|
|
$this->assertNotDuplicateCharge((int) $customer->id, (float) $data['value'], 'gold');
|
|
|
|
$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);
|
|
|
|
$this->recordAccountingTransaction(
|
|
$customer,
|
|
$Withdraw,
|
|
(int) $user->id,
|
|
0,
|
|
(float) $data['value'],
|
|
($data['note'] ?? null) ?: ('شارژ ' . $product->title)
|
|
);
|
|
|
|
if (app(ConfigService::class)->isTahesabAccounting()) {
|
|
try {
|
|
$data['direction'] = $deposit ? 1 : 0;
|
|
$data['withdraw_id'] = $Withdraw->id;
|
|
$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,
|
|
];
|
|
app(TahesabController::class)->walletGold($data);
|
|
} catch (\Exception $e) {
|
|
report($e);
|
|
}
|
|
}
|
|
|
|
if (app(ConfigService::class)->isZarinAccounting()) {
|
|
try {
|
|
$data['direction'] = $deposit ? 1 : 0;
|
|
$data['withdraw_id'] = $Withdraw->id;
|
|
$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,
|
|
];
|
|
app(\App\Http\Controllers\Admin\ZarinController::class)->walletGold($data);
|
|
} catch (\Exception $e) {
|
|
report($e);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
$status = $deposit ? ' شارژ شد ' : ' کسر شد ';
|
|
$formatted = self::formatSmsGoldWeight($data['value']);
|
|
|
|
if (self::isSmsNotifEnabled('charge_notif')) {
|
|
$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);
|
|
});
|
|
|
|
$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);
|
|
}
|
|
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);
|
|
}
|
|
$this->broadcastCustomerWalletAssets($customer);
|
|
|
|
return response()->json(true);
|
|
}
|
|
|
|
private function resolveProductChargeUnit(Product $product): string
|
|
{
|
|
$payload = Controller::productUnitPayload($product);
|
|
|
|
// تعدادی: همیشه «عدد» — حتی اگر unit محصول وزن هر قطعه (گرم) باشد
|
|
if (Controller::isCountBasedProduct($payload)) {
|
|
return 'عدد';
|
|
}
|
|
|
|
if (Controller::isWeightBasedProductUnit($payload['unit'] ?? null)) {
|
|
return trim((string) $payload['unit']);
|
|
}
|
|
|
|
$unit = trim((string) ($payload['unit'] ?? ''));
|
|
if ($unit !== '') {
|
|
return $unit;
|
|
}
|
|
|
|
return Controller::productStorageWeightUnit($product);
|
|
}
|
|
|
|
public function index(Request $request)
|
|
{
|
|
$balanceReport = app(BalanceReportService::class);
|
|
$balanceService = app(WalletBalanceService::class);
|
|
$catalogMetalIds = $balanceService->catalogMetalIds();
|
|
|
|
$records = Transaction::with([
|
|
'customer',
|
|
'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']);
|
|
},
|
|
])->whereNull('fund_id');
|
|
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);
|
|
|
|
if (isset($records['rows'])) {
|
|
$records['rows'] = collect($records['rows'])->map(function ($row) use ($balanceReport, $catalogMetalIds) {
|
|
$balanceReport->enrichTransactionTableRow($row, $catalogMetalIds);
|
|
|
|
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();
|
|
}
|
|
|
|
$records['catalogMetals'] = $balanceReport->catalogMetalsMeta();
|
|
|
|
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 = 'تراکنش با موفقیت بهروزرسانی شد.';
|
|
|
|
$customer = Customer::find($transaction->customer_id);
|
|
if ($customer) {
|
|
$this->broadcastCustomerWalletAssets($customer);
|
|
}
|
|
|
|
return response()->json([
|
|
'message' => $message,
|
|
'transaction' => $transaction,
|
|
// 'previous_amount' => $previousAmount,
|
|
// 'new_amount' => $newAmount,
|
|
'difference' => $difference,
|
|
'wallet' => $wallet_interface->wallet,
|
|
]);
|
|
}
|
|
|
|
public function indexWallet(Request $request)
|
|
{
|
|
$lastPrice = (float) (Price::latest()->first()?->price ?? 0);
|
|
$balanceService = app(WalletBalanceService::class);
|
|
$catalogMetalIds = $balanceService->catalogMetalIds();
|
|
$mainWalletMetalId = $balanceService->resolveMainWalletMetalId();
|
|
|
|
// همان فیلتر/جستجوی جدول — بدون صفحهبندی — برای مجموع ردیفهای قابل مشاهده
|
|
$sumQuery = Wallet::query();
|
|
TableEngine::create($sumQuery, $request->all())
|
|
->initSearch(['customer.name']);
|
|
|
|
$walletTableWith = [
|
|
'customer:id,name,mobile',
|
|
'fund:id,title',
|
|
'product' => static function ($q) {
|
|
$q->withTrashed()->select(['id', 'metal_id', 'unit', 'amount', 'group_type', 'trade_by_quantity', 'direct_price', 'title']);
|
|
},
|
|
];
|
|
|
|
$sumRows = (clone $sumQuery)->with($walletTableWith)->get();
|
|
$metalBalances = $balanceService->buildWalletMetalSums($sumRows, $catalogMetalIds);
|
|
$totalGold = array_sum(array_column($metalBalances, 'gold'));
|
|
$totalGoldToCash = array_sum(array_column($metalBalances, 'goldToCash'));
|
|
$totalCash = (float) (clone $sumQuery)->sum('cash');
|
|
|
|
$sums = [
|
|
'gold' => round($totalGold, 2),
|
|
'goldToCash' => round($totalGoldToCash, 2),
|
|
'cash' => round($totalCash, 2),
|
|
'cashToGold' => $lastPrice > 0 ? round($totalCash / $lastPrice, 2) : 0,
|
|
'metalBalances' => $metalBalances,
|
|
];
|
|
|
|
$records = Wallet::with($walletTableWith);
|
|
TableEngine::create($records, $request->all())
|
|
->initSearch(['customer.name', 'customer.mobile', 'product.title', 'fund.title'])
|
|
->select(['*'])
|
|
->export($records);
|
|
|
|
if (isset($records['rows'])) {
|
|
foreach ($records['rows'] as &$row) {
|
|
$balanceService->enrichWalletTableRow($row, $catalogMetalIds, $mainWalletMetalId);
|
|
}
|
|
unset($row);
|
|
}
|
|
|
|
$records['sums'] = $sums;
|
|
|
|
return response()->json($records);
|
|
}
|
|
|
|
public function indexWithdraw(Request $request)
|
|
{
|
|
$records = Withdraw::with(['customer', 'bank_account', 'address', "Transaction"]);
|
|
Withdraw::applyFlowFilter($records, $request->input('flow_type', 'all'));
|
|
TableEngine::create($records, $request->all())
|
|
->initSearch(['customer.name'])
|
|
->select([
|
|
'*', Withdraw::$SELECT_STS_STR, Withdraw::$SELECT_DAJ
|
|
])->export($records);
|
|
|
|
if (app(ConfigService::class)->isTahesabAccounting() && isset($records['rows']) && is_array($records['rows'])) {
|
|
$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);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
return response()->json($records);
|
|
}
|
|
|
|
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');
|
|
}
|
|
|
|
public function updateWithdraw(Request $request, $id)
|
|
{
|
|
$withdraw = Withdraw::with(['Transaction.product', 'customer'])->find($id);
|
|
if (!$withdraw) {
|
|
return response()->json(['error' => 'Withdraw not found'], 404);
|
|
}
|
|
|
|
$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;
|
|
$withdraw->save();
|
|
|
|
if ($withdraw->customer) {
|
|
$this->broadcastCustomerWalletAssets($withdraw->customer);
|
|
}
|
|
|
|
return response()->json(['message' => 'Withdraw updated successfully', 'withdraw' => $withdraw]);
|
|
}
|
|
|
|
/**
|
|
* برگشت موجودی کسرشدهٔ برداشت فیزیکی هنگام لغو (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);
|
|
}
|
|
}
|
|
|
|
public function getWithdraw(Request $request, $id)
|
|
{
|
|
$withdraw = Withdraw::with(['customer', 'bank_account', 'address', "Transaction"])->find($id);
|
|
if (!$withdraw) {
|
|
return response()->json(['error' => 'Withdraw not found'], 404);
|
|
}
|
|
return response()->json($withdraw);
|
|
}
|
|
|
|
public function indexPhysicalWithdraw(Request $request)
|
|
{
|
|
$records = Withdraw::with(['customer', 'address', 'Transaction.product']);
|
|
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,
|
|
'unit' => self::productQuantityUnit(
|
|
self::productUnitPayload($product)
|
|
),
|
|
'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;
|
|
});
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
$this->broadcastCustomerWalletAssets($customer);
|
|
|
|
return response()->json(['id' => $withdraw->id]);
|
|
}
|
|
|
|
public function adjustPhysicalWithdraw(Request $request, $id, AssetSummaryService $assetSummaryService)
|
|
{
|
|
$payload = $request->validate([
|
|
'items' => 'required|array|min:1',
|
|
'items.*.transaction_id' => 'required|integer|exists:transactions,id',
|
|
'items.*.value' => 'required|numeric|min:0.0001',
|
|
]);
|
|
|
|
$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 = Customer::findOrFail($withdraw->customer_id);
|
|
$transactionsById = $withdraw->Transaction->keyBy('id');
|
|
|
|
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;
|
|
}
|
|
|
|
$isCountBased = self::isCountBasedProduct(self::productUnitPayload($product));
|
|
$minValue = $isCountBased ? 1.0 : 0.001;
|
|
|
|
if ($newValue < $minValue) {
|
|
throw new UnprocessableEntityHttpException(
|
|
"حداقل مقدار برای {$product->title} برابر {$minValue} است"
|
|
);
|
|
}
|
|
|
|
if ($isCountBased && abs($newValue - round($newValue)) > 0.000001) {
|
|
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();
|
|
}
|
|
});
|
|
|
|
$this->broadcastCustomerWalletAssets($customer);
|
|
|
|
return response()->json([
|
|
'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;
|
|
$isCountBased = $product
|
|
? self::isCountBasedProduct(self::productUnitPayload($product))
|
|
: false;
|
|
|
|
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),
|
|
'min_value' => $isCountBased ? 1 : 0.001,
|
|
'allows_decimal' => !$isCountBased,
|
|
'unit' => $product
|
|
? self::productQuantityUnit(self::productUnitPayload($product))
|
|
: 'گرم',
|
|
];
|
|
})->values();
|
|
|
|
return response()->json([
|
|
'withdraw_id' => (int) $withdraw->id,
|
|
'customer_name' => (string) ($customer->name ?? ''),
|
|
'customer_mobile' => (string) ($customer->mobile ?? ''),
|
|
'lines' => $lines,
|
|
]);
|
|
}
|
|
|
|
public function completeWithdraw(Request $request)
|
|
{
|
|
$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']);
|
|
$data['user_id'] = auth('user')->id();
|
|
$data['sts'] = 2;
|
|
$data['tracked_number'] = (string) $data['tracked_number'];
|
|
if (empty($data['date_at'])) {
|
|
$data['date_at'] = now();
|
|
}
|
|
|
|
$withdraw = Withdraw::findOrFail($request->id);
|
|
$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);
|
|
}
|
|
|
|
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,
|
|
]);
|
|
}
|
|
|
|
private function broadcastCustomerWalletAssets(Customer $customer): void
|
|
{
|
|
app(WalletBroadcastService::class)->broadcastForCustomer(
|
|
(int) $customer->id,
|
|
(int) $customer->type
|
|
);
|
|
}
|
|
|
|
}
|