fix(backend): Make monthly top-up adjust the target balance instead of top-up the target amount (#9295)

To maintain the transaction running-balance integrity, the monthly
top-up balance has to adjust the top-up amount to maintain the balance
of the user to be at least X amount, instead of top-up the user balance
with a fixed X amount.

### Changes 🏗️

* Add an additional query to sum the remaining un-snapshotted balance
(if any).
* Monthly top-up transaction set the amount target_amount -
current_balance instead of fixed target_amount.

### Checklist 📋

#### For code changes:
- [ ] I have clearly listed my changes in the PR description
- [ ] I have made a test plan
- [ ] I have tested my changes according to the test plan:
  <!-- Put your test plan here: -->
  - [ ] ...

<details>
  <summary>Example test plan</summary>
  
  - [ ] Create from scratch and execute an agent with at least 3 blocks
- [ ] Import an agent from file upload, and confirm it executes
correctly
  - [ ] Upload agent to marketplace
- [ ] Import an agent from marketplace and confirm it executes correctly
  - [ ] Edit an agent from monitor, and confirm it executes correctly
</details>

#### For configuration changes:
- [ ] `.env.example` is updated or already compatible with my changes
- [ ] `docker-compose.yml` is updated or already compatible with my
changes
- [ ] I have included a list of my configuration changes in the PR
description (under **Changes**)

<details>
  <summary>Examples of configuration changes</summary>

  - Changing ports
  - Adding new services that need to communicate with each other
  - Secrets or environment variable changes
  - New or infrastructure changes such as databases
</details>
This commit is contained in:
Zamil Majdy
2025-01-24 21:43:43 +07:00
committed by GitHub
parent 56330b1dd3
commit 88f711e486
2 changed files with 29 additions and 25 deletions

View File

@@ -110,24 +110,35 @@ class UserCreditBase(ABC):
},
order={"createdAt": "desc"},
)
if snapshot:
return snapshot.runningBalance or 0, snapshot.createdAt
datetime_min = datetime.min.replace(tzinfo=timezone.utc)
snapshot_balance = snapshot.runningBalance or 0 if snapshot else 0
snapshot_time = snapshot.createdAt if snapshot else datetime_min
# No snapshot: Manually calculate balance using current month's transactions.
low_time = top_time.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
# Get transactions after the snapshot, this should not exist, but just in case.
transactions = await CreditTransaction.prisma().group_by(
by=["userId"],
sum={"amount": True},
max={"createdAt": True},
where={
"userId": user_id,
"createdAt": {"gte": low_time, "lte": top_time},
"createdAt": {
"gt": snapshot_time,
"lte": top_time,
},
"isActive": True,
},
)
transaction_balance = (
transactions[0].get("_sum", {}).get("amount", 0) if transactions else 0
transactions[0].get("_sum", {}).get("amount", 0) + snapshot_balance
if transactions
else snapshot_balance
)
return transaction_balance, datetime.min
transaction_time = (
transactions[0].get("_max", {}).get("createdAt", datetime_min)
if transactions
else snapshot_time
)
return transaction_balance, transaction_time
async def _enable_transaction(
self, transaction_key: str, user_id: str, metadata: Json
@@ -389,22 +400,15 @@ class BetaUserCredit(UserCredit):
return balance
try:
await CreditTransaction.prisma().create(
data={
"transactionKey": f"MONTHLY-CREDIT-TOP-UP-{cur_time}",
"userId": user_id,
"amount": self.num_user_credits_refill,
"runningBalance": self.num_user_credits_refill,
"type": CreditTransactionType.TOP_UP,
"metadata": Json({}),
"isActive": True,
"createdAt": self.time_now(),
}
return await self._add_transaction(
user_id=user_id,
amount=max(self.num_user_credits_refill - balance, 0),
transaction_type=CreditTransactionType.TOP_UP,
transaction_key=f"MONTHLY-CREDIT-TOP-UP-{cur_time}",
)
except UniqueViolationError:
pass # Already refilled this month
return self.num_user_credits_refill
# Already refilled this month
return (await self._get_credits(user_id))[0]
class DisabledUserCredit(UserCreditBase):

View File

@@ -1,4 +1,4 @@
from datetime import datetime
from datetime import datetime, timezone
import pytest
from prisma.models import CreditTransaction
@@ -83,17 +83,17 @@ async def test_block_credit_reset(server: SpinTestServer):
month2 = 2
# set the calendar to month 2 but use current time from now
user_credit.time_now = lambda: datetime.now().replace(month=month2)
user_credit.time_now = lambda: datetime.now(timezone.utc).replace(month=month2)
month2credit = await user_credit.get_credits(DEFAULT_USER_ID)
# Month 1 result should only affect month 1
user_credit.time_now = lambda: datetime.now().replace(month=month1)
user_credit.time_now = lambda: datetime.now(timezone.utc).replace(month=month1)
month1credit = await user_credit.get_credits(DEFAULT_USER_ID)
await user_credit.top_up_credits(DEFAULT_USER_ID, 100)
assert await user_credit.get_credits(DEFAULT_USER_ID) == month1credit + 100
# Month 2 balance is unaffected
user_credit.time_now = lambda: datetime.now().replace(month=month2)
user_credit.time_now = lambda: datetime.now(timezone.utc).replace(month=month2)
assert await user_credit.get_credits(DEFAULT_USER_ID) == month2credit