fix(wallet): absorb cash left on old owner type

Deposits credited before customer type was set stayed on owner_type 0 and assets ignored them.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-14 14:41:24 +03:30
co-authored by Cursor
parent bdf70a4c05
commit 97d1077db6
5 changed files with 108 additions and 0 deletions
@@ -89,6 +89,7 @@ class WalletController extends Controller
$customer = auth('customer')->user();
$customerId = (int) $customer->id;
$ownerType = (int) $customer->type;
$balanceService->absorbMismatchedOwnerTypeBalances($customerId, $ownerType);
$wallets = Wallet::query()
->where('owner', $customerId)
+4
View File
@@ -26,6 +26,10 @@ class AssetSummaryService
public function buildForCustomer(int $customerId, int $ownerType, bool $includeInactiveProducts = false): array
{
if ($customerId > 0 && $ownerType > 0) {
$this->walletBalanceService->absorbMismatchedOwnerTypeBalances($customerId, $ownerType);
}
$catalogQuery = Product::query()->with('file');
if (!$includeInactiveProducts) {
$catalogQuery->where('active', 1);
+86
View File
@@ -7,6 +7,8 @@ use App\Models\Price;
use App\Models\Product;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Rooyesh\Wallet\interfaces\TransactionInterface;
use Rooyesh\Wallet\interfaces\WalletInterface;
use Rooyesh\Wallet\models\Wallet;
/**
@@ -17,6 +19,11 @@ use Rooyesh\Wallet\models\Wallet;
*/
class WalletBalanceService
{
/** @var array<string, true> */
private static array $absorbedOwnerTypes = [];
private static bool $absorbingOwnerType = false;
public function __construct(
private ConfigService $configService,
private BalanceReportService $balanceReportService
@@ -161,6 +168,85 @@ class WalletBalanceService
return true;
}
/**
* ماندهٔ کیف‌هایی که owner_typeشان با نوع فعلی مشتری یکی نیست
* (مثلاً واریز وقتی type هنوز ۰ بوده) به کیف همان محصول/قلک روی نوع فعلی منتقل می‌شود.
*/
public function absorbMismatchedOwnerTypeBalances(int $ownerId, int $canonicalType): bool
{
if ($ownerId <= 0 || $canonicalType <= 0 || self::$absorbingOwnerType) {
return false;
}
$key = $ownerId.':'.$canonicalType;
if (isset(self::$absorbedOwnerTypes[$key])) {
return false;
}
self::$absorbedOwnerTypes[$key] = true;
$foreign = Wallet::query()
->where('owner', $ownerId)
->where('owner_type', '!=', $canonicalType)
->where(function ($query) {
$query->where('cash', '!=', 0)->orWhere('gold', '!=', 0);
})
->get();
if ($foreign->isEmpty()) {
return false;
}
self::$absorbingOwnerType = true;
try {
foreach ($foreign as $row) {
DB::transaction(function () use ($row, $ownerId, $canonicalType) {
$locked = Wallet::query()->whereKey($row->id)->lockForUpdate()->first();
if (!$locked) {
return;
}
$cash = (int) round((float) $locked->cash);
$gold = (float) $locked->gold;
if ($cash === 0 && abs($gold) < 0.0000001) {
return;
}
$from = new WalletInterface($ownerId, (int) $locked->owner_type, $locked->fund_id, $locked->product_id);
$to = new WalletInterface($ownerId, $canonicalType, $locked->fund_id, $locked->product_id);
$note = 'انتقال مانده به کیف اصلی مشتری';
if ($cash !== 0) {
$payload = [
'cash' => abs($cash),
'gold' => 0,
'note' => $note,
'expire_at' => now(),
'active' => true,
];
new TransactionInterface($from, $payload, 7, false, $cash < 0);
new TransactionInterface($to, $payload, 7, false, $cash > 0);
}
if (abs($gold) >= 0.0000001) {
$payload = [
'gold' => abs($gold),
'cash' => 0,
'note' => $note,
'expire_at' => now(),
'active' => true,
];
new TransactionInterface($from, $payload, 7, $gold < 0, false);
new TransactionInterface($to, $payload, 7, $gold > 0, false);
}
});
}
} finally {
self::$absorbingOwnerType = false;
}
return true;
}
/** ادغام همهٔ کیف‌های legacy محصولات main_wallet_products برای یک کاربر */
public function consolidateAllMainWalletProductsForOwner(int $ownerId, int $ownerType): void
{