fix: prevent duplicate suno task refunds via cas status update (#6074)

* fix: prevent duplicate suno task refunds via cas status update

* fix: reconcile failed task refunds

---------

Co-authored-by: CaIon <i@caion.me>
This commit is contained in:
feitianbubu
2026-07-20 22:03:13 +08:00
committed by GitHub
co-authored by CaIon
parent 4aa08f917e
commit e0d5156115
7 changed files with 456 additions and 16 deletions
+35 -4
View File
@@ -270,6 +270,13 @@ func getSubscriptionUsed(t *testing.T, id int) int64 {
return sub.AmountUsed
}
func getTaskQuota(t *testing.T, id int64) int {
t.Helper()
var task model.Task
require.NoError(t, model.DB.Select("quota").Where("id = ?", id).First(&task).Error)
return task.Quota
}
func getLastLog(t *testing.T) *model.Log {
t.Helper()
var log model.Log
@@ -304,8 +311,9 @@ func TestRefundTaskQuota_Wallet(t *testing.T) {
seedChannel(t, channelID)
task := makeTask(userID, channelID, preConsumed, tokenID, BillingSourceWallet, 0)
require.NoError(t, model.DB.Create(task).Error)
RefundTaskQuota(ctx, task, "task failed: upstream error")
assert.True(t, RefundTaskQuota(ctx, task, "task failed: upstream error"))
// User quota should increase by preConsumed
assert.Equal(t, initQuota+preConsumed, getUserQuota(t, userID))
@@ -320,6 +328,8 @@ func TestRefundTaskQuota_Wallet(t *testing.T) {
assert.Equal(t, model.LogTypeRefund, log.Type)
assert.Equal(t, preConsumed, log.Quota)
assert.Equal(t, "test-model", log.ModelName)
assert.Zero(t, task.Quota)
assert.Zero(t, getTaskQuota(t, task.ID))
}
func TestRefundTaskQuota_Subscription(t *testing.T) {
@@ -337,8 +347,9 @@ func TestRefundTaskQuota_Subscription(t *testing.T) {
seedSubscription(t, subID, userID, subTotal, subUsed)
task := makeTask(userID, channelID, preConsumed, tokenID, BillingSourceSubscription, subID)
require.NoError(t, model.DB.Create(task).Error)
RefundTaskQuota(ctx, task, "subscription task failed")
assert.True(t, RefundTaskQuota(ctx, task, "subscription task failed"))
// Subscription used should decrease by preConsumed
assert.Equal(t, subUsed-int64(preConsumed), getSubscriptionUsed(t, subID))
@@ -349,6 +360,7 @@ func TestRefundTaskQuota_Subscription(t *testing.T) {
log := getLastLog(t)
require.NotNil(t, log)
assert.Equal(t, model.LogTypeRefund, log.Type)
assert.Zero(t, getTaskQuota(t, task.ID))
}
func TestRefundTaskQuota_ZeroQuota(t *testing.T) {
@@ -360,7 +372,7 @@ func TestRefundTaskQuota_ZeroQuota(t *testing.T) {
task := makeTask(userID, 0, 0, 0, BillingSourceWallet, 0)
RefundTaskQuota(ctx, task, "zero quota task")
assert.True(t, RefundTaskQuota(ctx, task, "zero quota task"))
// No change to user quota
assert.Equal(t, 5000, getUserQuota(t, userID))
@@ -380,8 +392,9 @@ func TestRefundTaskQuota_NoToken(t *testing.T) {
seedChannel(t, channelID)
task := makeTask(userID, channelID, preConsumed, 0, BillingSourceWallet, 0) // TokenId=0
require.NoError(t, model.DB.Create(task).Error)
RefundTaskQuota(ctx, task, "no token task failed")
assert.True(t, RefundTaskQuota(ctx, task, "no token task failed"))
// User quota refunded
assert.Equal(t, initQuota+preConsumed, getUserQuota(t, userID))
@@ -390,6 +403,23 @@ func TestRefundTaskQuota_NoToken(t *testing.T) {
log := getLastLog(t)
require.NotNil(t, log)
assert.Equal(t, model.LogTypeRefund, log.Type)
assert.Zero(t, getTaskQuota(t, task.ID))
}
func TestRefundTaskQuota_FundingFailureKeepsPendingMarker(t *testing.T) {
truncate(t)
ctx := context.Background()
const userID, preConsumed = 5, 1200
seedUser(t, userID, 5000)
task := makeTask(userID, 0, preConsumed, 0, BillingSourceSubscription, 9999)
task.Status = model.TaskStatusFailure
require.NoError(t, model.DB.Create(task).Error)
assert.False(t, RefundTaskQuota(ctx, task, "subscription missing"))
assert.Equal(t, preConsumed, task.Quota)
assert.Equal(t, preConsumed, getTaskQuota(t, task.ID))
assert.Equal(t, int64(0), countLogs(t))
}
// ===========================================================================
@@ -608,6 +638,7 @@ func TestCASGuardedRefund_Win(t *testing.T) {
var reloaded model.Task
require.NoError(t, model.DB.First(&reloaded, task.ID).Error)
assert.EqualValues(t, model.TaskStatusFailure, reloaded.Status)
assert.Zero(t, reloaded.Quota)
// Refund should have happened
assert.Equal(t, initQuota+preConsumed, getUserQuota(t, userID))