feat(server): evaluate price alerts per product and fix gold discharge rounding

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>
This commit is contained in:
2026-08-18 22:23:20 +03:30
co-authored by Cursor
parent 341b2da2c9
commit b0296c6e94
10 changed files with 454 additions and 55 deletions
+86 -10
View File
@@ -67,6 +67,73 @@ class WalletController extends Controller
}
}
/**
* عدد نمایشی موجودی تا ۳ رقم گرد می‌شود؛ اگر ادمین همان عدد را بزند، کل ماندهٔ واقعی کسر شود.
*/
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();
@@ -271,20 +338,17 @@ class WalletController extends Controller
$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']);
// ادمین عدد را از نمایش سه‌رقمی می‌خواند؛ اختلاف تا نصف پلهٔ نمایش یعنی «کل موجودی»
if ($requested > $available && $requested - $available <= self::GOLD_DISPLAY_STEP) {
$requested = $available;
$data['value'] = -$available;
}
if ($requested <= 0 || $requested > $available) {
throw new UnprocessableEntityHttpException('مقدار موجودی طلا کمتر از درخواست شماست');
}
$resolved = $this->resolveGoldDischargeAmount($available, $requested, 'مقدار موجودی طلا کمتر از درخواست شماست');
$data['value'] = -$resolved;
}
$this->assertNotDuplicateCharge((int) $customer->id, (float) $data['value'], 'gold');
$gold_block = 0;
@@ -489,6 +553,11 @@ class WalletController extends Controller
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'],
@@ -498,8 +567,15 @@ class WalletController extends Controller
);
$wallet = $wallet_interface->wallet;
if (!$deposit && $wallet !== null && (float) $wallet->gold < abs((float) $data['value'])) {
throw new UnprocessableEntityHttpException('مقدار موجودی کمتر از درخواست شماست');
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');