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:
@@ -13,6 +13,9 @@ class PriceTriggerController extends Controller
|
||||
public function table(Request $request)
|
||||
{
|
||||
$query = PriceTrigger::query()->with('customer');
|
||||
if (PriceTrigger::hasProductIdColumn()) {
|
||||
$query->with('product:id,title');
|
||||
}
|
||||
|
||||
TableEngine::create($query, $request->all())
|
||||
->initSearch(['customer_id', 'target_price', 'action', 'price_condition'])
|
||||
@@ -31,7 +34,12 @@ class PriceTriggerController extends Controller
|
||||
'is_active' => $data['is_active'] ?? true,
|
||||
]);
|
||||
|
||||
return response()->json($trigger->load('customer'), 201);
|
||||
$relations = ['customer'];
|
||||
if (PriceTrigger::hasProductIdColumn()) {
|
||||
$relations[] = 'product:id,title';
|
||||
}
|
||||
|
||||
return response()->json($trigger->load($relations), 201);
|
||||
}
|
||||
|
||||
public function update(Request $request, int $id)
|
||||
@@ -71,12 +79,17 @@ class PriceTriggerController extends Controller
|
||||
{
|
||||
$rules = [
|
||||
'customer_id' => ($partial ? 'sometimes|' : '') . 'required|integer|exists:customers,id',
|
||||
'product_id' => ($partial ? 'sometimes|' : '') . 'required|integer|exists:products,id',
|
||||
'target_price' => ($partial ? 'sometimes|' : '') . 'required|numeric|min:1',
|
||||
'price_condition' => ($partial ? 'sometimes|' : '') . 'required|in:>,<',
|
||||
'action' => ($partial ? 'sometimes|' : '') . 'required|in:sms,email',
|
||||
'is_active' => 'sometimes|boolean',
|
||||
];
|
||||
|
||||
if (! PriceTrigger::hasProductIdColumn()) {
|
||||
unset($rules['product_id']);
|
||||
}
|
||||
|
||||
$data = $request->validate($rules);
|
||||
|
||||
if (isset($data['price_condition']) && !in_array($data['price_condition'], ['>', '<'], true)) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user