Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -173,6 +173,9 @@ func (l *RefundOrderLogic) RefundOrder(req *types.RefundOrderRequest) error {
|
|||||||
orderUser := &modeluser.User{Id: orderInfo.UserId}
|
orderUser := &modeluser.User{Id: orderInfo.UserId}
|
||||||
userCacheTargets = append(userCacheTargets, orderUser)
|
userCacheTargets = append(userCacheTargets, orderUser)
|
||||||
}
|
}
|
||||||
|
if userSub.UserId > 0 && userSub.UserId != orderInfo.UserId {
|
||||||
|
userCacheTargets = append(userCacheTargets, &modeluser.User{Id: userSub.UserId})
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -199,16 +202,16 @@ func (l *RefundOrderLogic) RefundOrder(req *types.RefundOrderRequest) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *RefundOrderLogic) lockRefundTargetSubscription(tx *gorm.DB, orderInfo *modelorder.Order) (*modeluser.Subscribe, error) {
|
func (l *RefundOrderLogic) lockRefundTargetSubscription(tx *gorm.DB, orderInfo *modelorder.Order) (*modeluser.Subscribe, error) {
|
||||||
var userSub modeluser.Subscribe
|
if userSub, err := l.lockSubscriptionByOrderID(tx, orderInfo.Id); err != nil {
|
||||||
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
return nil, err
|
||||||
Model(&modeluser.Subscribe{}).
|
} else if userSub != nil {
|
||||||
Where("order_id = ?", orderInfo.Id).
|
return userSub, nil
|
||||||
First(&userSub).Error
|
|
||||||
if err == nil {
|
|
||||||
return &userSub, nil
|
|
||||||
}
|
}
|
||||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query refund subscription failed: %v", err)
|
if userSub, err := l.lockSubscriptionByEntitlement(tx, orderInfo); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if userSub != nil {
|
||||||
|
return userSub, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if orderInfo.Type != 2 {
|
if orderInfo.Type != 2 {
|
||||||
@@ -219,17 +222,122 @@ func (l *RefundOrderLogic) lockRefundTargetSubscription(tx *gorm.DB, orderInfo *
|
|||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.OrderRefundNoSubscription), "renewal order %d has no parent order", orderInfo.Id)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.OrderRefundNoSubscription), "renewal order %d has no parent order", orderInfo.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
if userSub, err := l.lockSubscriptionByOrderID(tx, orderInfo.ParentId); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if userSub != nil {
|
||||||
|
return userSub, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if userSub, err := l.lockRenewalParentEntitlementSubscription(tx, orderInfo); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if userSub != nil {
|
||||||
|
return userSub, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.OrderRefundNoSubscription), "renewal order %d parent subscription not found", orderInfo.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *RefundOrderLogic) lockSubscriptionByOrderID(tx *gorm.DB, orderID int64) (*modeluser.Subscribe, error) {
|
||||||
|
if orderID <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var userSub modeluser.Subscribe
|
||||||
|
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||||
Model(&modeluser.Subscribe{}).
|
Model(&modeluser.Subscribe{}).
|
||||||
Where("order_id = ?", orderInfo.ParentId).
|
Where("order_id = ?", orderID).
|
||||||
First(&userSub).Error
|
First(&userSub).Error
|
||||||
|
if err == nil {
|
||||||
|
return &userSub, nil
|
||||||
|
}
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query refund subscription failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *RefundOrderLogic) lockSubscriptionByEntitlement(tx *gorm.DB, orderInfo *modelorder.Order) (*modeluser.Subscribe, error) {
|
||||||
|
entitlementUserID := orderInfo.SubscriptionUserId
|
||||||
|
if entitlementUserID == 0 {
|
||||||
|
entitlementUserID = orderInfo.UserId
|
||||||
|
}
|
||||||
|
if entitlementUserID <= 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if userSub, err := l.lockExactEntitlementSubscription(tx, orderInfo, entitlementUserID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if userSub != nil {
|
||||||
|
return userSub, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var userSub modeluser.Subscribe
|
||||||
|
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||||
|
Model(&modeluser.Subscribe{}).
|
||||||
|
Where("user_id = ? AND subscribe_id = ?", entitlementUserID, orderInfo.SubscribeId).
|
||||||
|
Where("status IN ?", []int64{0, 1, 2, 3, 5}).
|
||||||
|
Order("expire_time DESC").
|
||||||
|
Order("updated_at DESC").
|
||||||
|
Order("id DESC").
|
||||||
|
First(&userSub).Error
|
||||||
|
if err == nil {
|
||||||
|
return &userSub, nil
|
||||||
|
}
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query entitlement subscription failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *RefundOrderLogic) lockExactEntitlementSubscription(tx *gorm.DB, orderInfo *modelorder.Order, entitlementUserID int64) (*modeluser.Subscribe, error) {
|
||||||
|
if orderInfo.Id <= 0 && orderInfo.SubscribeToken == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
query := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||||
|
Model(&modeluser.Subscribe{}).
|
||||||
|
Where("user_id = ? AND subscribe_id = ?", entitlementUserID, orderInfo.SubscribeId)
|
||||||
|
if orderInfo.Id > 0 && orderInfo.SubscribeToken != "" {
|
||||||
|
query = query.Where("(order_id = ? OR token = ?)", orderInfo.Id, orderInfo.SubscribeToken)
|
||||||
|
} else if orderInfo.Id > 0 {
|
||||||
|
query = query.Where("order_id = ?", orderInfo.Id)
|
||||||
|
} else {
|
||||||
|
query = query.Where("token = ?", orderInfo.SubscribeToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
var userSub modeluser.Subscribe
|
||||||
|
err := query.Order("id DESC").First(&userSub).Error
|
||||||
|
if err == nil {
|
||||||
|
return &userSub, nil
|
||||||
|
}
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query exact entitlement subscription failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *RefundOrderLogic) lockRenewalParentEntitlementSubscription(tx *gorm.DB, orderInfo *modelorder.Order) (*modeluser.Subscribe, error) {
|
||||||
|
var parentOrder modelorder.Order
|
||||||
|
err := tx.Model(&modelorder.Order{}).
|
||||||
|
Where("id = ?", orderInfo.ParentId).
|
||||||
|
First(&parentOrder).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.OrderRefundNoSubscription), "renewal order %d parent subscription not found", orderInfo.Id)
|
return nil, nil
|
||||||
}
|
}
|
||||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query renewal subscription failed: %v", err)
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query renewal parent order failed: %v", err)
|
||||||
}
|
}
|
||||||
return &userSub, nil
|
|
||||||
|
if parentOrder.SubscriptionUserId == 0 && orderInfo.SubscriptionUserId > 0 {
|
||||||
|
parentOrder.SubscriptionUserId = orderInfo.SubscriptionUserId
|
||||||
|
}
|
||||||
|
if parentOrder.UserId == 0 {
|
||||||
|
parentOrder.UserId = orderInfo.UserId
|
||||||
|
}
|
||||||
|
if parentOrder.SubscribeId == 0 {
|
||||||
|
parentOrder.SubscribeId = orderInfo.SubscribeId
|
||||||
|
}
|
||||||
|
return l.lockSubscriptionByEntitlement(tx, &parentOrder)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *RefundOrderLogic) lockCommissionSource(tx *gorm.DB, orderNo string, orderCommission int64) (*modeluser.User, int64, error) {
|
func (l *RefundOrderLogic) lockCommissionSource(tx *gorm.DB, orderNo string, orderCommission int64) (*modeluser.User, int64, error) {
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/DATA-DOG/go-sqlmock"
|
"github.com/DATA-DOG/go-sqlmock"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
|
||||||
modelorder "github.com/perfect-panel/server/internal/model/order"
|
modelorder "github.com/perfect-panel/server/internal/model/order"
|
||||||
modeluser "github.com/perfect-panel/server/internal/model/user"
|
modeluser "github.com/perfect-panel/server/internal/model/user"
|
||||||
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
"github.com/perfect-panel/server/internal/types"
|
"github.com/perfect-panel/server/internal/types"
|
||||||
"github.com/perfect-panel/server/pkg/constant"
|
"github.com/perfect-panel/server/pkg/constant"
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
@@ -195,6 +195,141 @@ func TestRefundOrder_RejectsWhenStatusNotRefundable(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLockRefundTargetSubscription_FamilyMemberPurchaseLocksOwnerSubscription(t *testing.T) {
|
||||||
|
const (
|
||||||
|
orderID = int64(2001)
|
||||||
|
memberUID = int64(101)
|
||||||
|
ownerUID = int64(201)
|
||||||
|
subscribeID = int64(301)
|
||||||
|
userSubID = int64(401)
|
||||||
|
)
|
||||||
|
|
||||||
|
db, mock, cleanup := newRefundOrderTestDB(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
mock.ExpectQuery("FROM `user_subscribe`").
|
||||||
|
WithArgs(orderID, 1).
|
||||||
|
WillReturnError(gorm.ErrRecordNotFound)
|
||||||
|
mock.ExpectQuery("FROM `user_subscribe`").
|
||||||
|
WithArgs(ownerUID, subscribeID, orderID, 1).
|
||||||
|
WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "order_id", "subscribe_id", "status", "expire_time"}).
|
||||||
|
AddRow(userSubID, ownerUID, orderID, subscribeID, uint8(1), time.Now().Add(24*time.Hour)))
|
||||||
|
|
||||||
|
logic := &RefundOrderLogic{}
|
||||||
|
got, err := logic.lockRefundTargetSubscription(db, &modelorder.Order{
|
||||||
|
Id: orderID,
|
||||||
|
UserId: memberUID,
|
||||||
|
SubscriptionUserId: ownerUID,
|
||||||
|
Type: 1,
|
||||||
|
SubscribeId: subscribeID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("lockRefundTargetSubscription error: %v", err)
|
||||||
|
}
|
||||||
|
if got.Id != userSubID || got.UserId != ownerUID {
|
||||||
|
t.Fatalf("locked subscription = %+v, want id=%d user_id=%d", got, userSubID, ownerUID)
|
||||||
|
}
|
||||||
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
|
t.Fatalf("unmet sql expectations: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLockRefundTargetSubscription_RenewalFallsBackToParentOwnerEntitlement(t *testing.T) {
|
||||||
|
const (
|
||||||
|
orderID = int64(2101)
|
||||||
|
parentOrderID = int64(2100)
|
||||||
|
memberUID = int64(111)
|
||||||
|
ownerUID = int64(211)
|
||||||
|
subscribeID = int64(311)
|
||||||
|
userSubID = int64(411)
|
||||||
|
)
|
||||||
|
|
||||||
|
db, mock, cleanup := newRefundOrderTestDB(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
mock.ExpectQuery("FROM `user_subscribe`").
|
||||||
|
WithArgs(orderID, 1).
|
||||||
|
WillReturnError(gorm.ErrRecordNotFound)
|
||||||
|
mock.ExpectQuery("FROM `user_subscribe`").
|
||||||
|
WithArgs(ownerUID, subscribeID, orderID, "renew-token", 1).
|
||||||
|
WillReturnError(gorm.ErrRecordNotFound)
|
||||||
|
mock.ExpectQuery("FROM `user_subscribe`").
|
||||||
|
WithArgs(ownerUID, subscribeID, int64(0), int64(1), int64(2), int64(3), int64(5), 1).
|
||||||
|
WillReturnError(gorm.ErrRecordNotFound)
|
||||||
|
mock.ExpectQuery("FROM `user_subscribe`").
|
||||||
|
WithArgs(parentOrderID, 1).
|
||||||
|
WillReturnError(gorm.ErrRecordNotFound)
|
||||||
|
mock.ExpectQuery("FROM `order`").
|
||||||
|
WithArgs(parentOrderID, 1).
|
||||||
|
WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "subscription_user_id", "subscribe_id", "subscribe_token"}).
|
||||||
|
AddRow(parentOrderID, memberUID, ownerUID, subscribeID, "owner-token"))
|
||||||
|
mock.ExpectQuery("FROM `user_subscribe`").
|
||||||
|
WithArgs(ownerUID, subscribeID, parentOrderID, "owner-token", 1).
|
||||||
|
WillReturnRows(sqlmock.NewRows([]string{"id", "user_id", "order_id", "subscribe_id", "status", "expire_time", "token"}).
|
||||||
|
AddRow(userSubID, ownerUID, parentOrderID, subscribeID, uint8(1), time.Now().Add(24*time.Hour), "owner-token"))
|
||||||
|
|
||||||
|
logic := &RefundOrderLogic{}
|
||||||
|
got, err := logic.lockRefundTargetSubscription(db, &modelorder.Order{
|
||||||
|
Id: orderID,
|
||||||
|
ParentId: parentOrderID,
|
||||||
|
UserId: memberUID,
|
||||||
|
SubscriptionUserId: ownerUID,
|
||||||
|
Type: 2,
|
||||||
|
SubscribeId: subscribeID,
|
||||||
|
SubscribeToken: "renew-token",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("lockRefundTargetSubscription error: %v", err)
|
||||||
|
}
|
||||||
|
if got.Id != userSubID || got.UserId != ownerUID || got.OrderId != parentOrderID {
|
||||||
|
t.Fatalf("locked subscription = %+v, want id=%d user_id=%d order_id=%d", got, userSubID, ownerUID, parentOrderID)
|
||||||
|
}
|
||||||
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
|
t.Fatalf("unmet sql expectations: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRefundOrder_NoTargetSubscriptionDoesNotRefundOrder(t *testing.T) {
|
||||||
|
const (
|
||||||
|
orderID = int64(2201)
|
||||||
|
orderNo = "ORD-NO-SUB"
|
||||||
|
operatorUID = int64(519)
|
||||||
|
userID = int64(7001)
|
||||||
|
subscribeID = int64(8001)
|
||||||
|
)
|
||||||
|
|
||||||
|
db, mock, cleanup := newRefundOrderTestDB(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
mock.ExpectBegin()
|
||||||
|
mock.ExpectQuery("FROM `order`").
|
||||||
|
WithArgs(orderID, 1).
|
||||||
|
WillReturnRows(sqlmock.NewRows([]string{"id", "order_no", "status", "type", "commission", "user_id", "subscribe_id"}).
|
||||||
|
AddRow(orderID, orderNo, uint8(5), uint8(1), int64(0), userID, subscribeID))
|
||||||
|
mock.ExpectQuery("FROM `system_logs`").
|
||||||
|
WithArgs(uint8(33), fmt.Sprintf(`%%"order_no":"%s"%%`, orderNo)).
|
||||||
|
WillReturnRows(sqlmock.NewRows([]string{"id", "content"}))
|
||||||
|
mock.ExpectQuery("FROM `user_subscribe`").
|
||||||
|
WithArgs(orderID, 1).
|
||||||
|
WillReturnError(gorm.ErrRecordNotFound)
|
||||||
|
mock.ExpectQuery("FROM `user_subscribe`").
|
||||||
|
WithArgs(userID, subscribeID, orderID, 1).
|
||||||
|
WillReturnError(gorm.ErrRecordNotFound)
|
||||||
|
mock.ExpectQuery("FROM `user_subscribe`").
|
||||||
|
WithArgs(userID, subscribeID, int64(0), int64(1), int64(2), int64(3), int64(5), 1).
|
||||||
|
WillReturnError(gorm.ErrRecordNotFound)
|
||||||
|
mock.ExpectRollback()
|
||||||
|
|
||||||
|
logic := newTestRefundOrderLogic(t, db, operatorUID)
|
||||||
|
err := logic.RefundOrder(&types.RefundOrderRequest{Id: orderID, Reason: "missing subscription"})
|
||||||
|
if !isErrCode(err, xerr.OrderRefundNoSubscription) {
|
||||||
|
t.Fatalf("RefundOrder error code = %v, want OrderRefundNoSubscription; raw=%v", errCodeOf(err), err)
|
||||||
|
}
|
||||||
|
if err := mock.ExpectationsWereMet(); err != nil {
|
||||||
|
t.Fatalf("unmet sql expectations: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func newRefundOrderTestDB(t *testing.T) (*gorm.DB, sqlmock.Sqlmock, func()) {
|
func newRefundOrderTestDB(t *testing.T) (*gorm.DB, sqlmock.Sqlmock, func()) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user