From 59b7056a209b4cd9c9251753655484e8e3181f16 Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Thu, 30 Apr 2026 09:26:52 -0700 Subject: [PATCH] fix family member renewal target --- .../public/order/purchaseNewUserOnly_test.go | 79 +++++++++++++++++++ internal/logic/public/order/renewalLogic.go | 71 ++++++++++++++++- 2 files changed, 148 insertions(+), 2 deletions(-) diff --git a/internal/logic/public/order/purchaseNewUserOnly_test.go b/internal/logic/public/order/purchaseNewUserOnly_test.go index b976eea..2cede15 100644 --- a/internal/logic/public/order/purchaseNewUserOnly_test.go +++ b/internal/logic/public/order/purchaseNewUserOnly_test.go @@ -699,6 +699,85 @@ func TestPurchase_NewUserOnly_BindEmailScopeSharesHistory(t *testing.T) { assert.Equal(t, int64(0), newOrder.Discount) } +func ensureRenewalSubscribeColumns(t *testing.T, db *gorm.DB) { + t.Helper() + for _, sql := range []string{ + `ALTER TABLE "user_subscribe" ADD COLUMN node_group_id INTEGER NOT NULL DEFAULT 0`, + `ALTER TABLE "user_subscribe" ADD COLUMN group_locked TINYINT NOT NULL DEFAULT 0`, + `ALTER TABLE "user_subscribe" ADD COLUMN expired_download INTEGER NOT NULL DEFAULT 0`, + `ALTER TABLE "user_subscribe" ADD COLUMN expired_upload INTEGER NOT NULL DEFAULT 0`, + } { + require.NoError(t, db.Exec(sql).Error) + } +} + +func insertRenewalUserSubscribe(t *testing.T, db *gorm.DB, id, userID, orderID, subscribeID int64, token, uuid string, start, expire time.Time) { + t.Helper() + require.NoError(t, db.Exec(`INSERT INTO "user_subscribe" + (id, user_id, order_id, subscribe_id, start_time, expire_time, traffic, download, upload, token, uuid, status, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, 0, 0, 0, ?, ?, 1, ?, ?)`, + id, + userID, + orderID, + subscribeID, + start.UTC().Format("2006-01-02 15:04:05"), + expire.UTC().Format("2006-01-02 15:04:05"), + token, + uuid, + start.UTC().Format("2006-01-02 15:04:05"), + time.Now().UTC().Format("2006-01-02 15:04:05"), + ).Error) +} + +func TestRenewalMemberRequestRedirectsToOwnerSubscribe(t *testing.T) { + db := setupNewUserOnlyDB(t) + ensureRenewalSubscribeColumns(t, db) + rds, mr := setupNewUserOnlyRedis(t) + svcCtx := buildNewUserOnlySvcCtx(db, rds, mr) + + const ( + planID = int64(1) + paymentID = int64(2) + ownerUserID = int64(29650) + memberID = int64(20003) + familyID = int64(88001) + memberSubID = int64(10013) + ownerSubID = int64(14074) + ) + + insertTestSubscribe(t, db, planID, false) + insertTestPayment(t, db, paymentID) + member := insertTestUser(t, db, memberID, time.Now().Add(-24*time.Hour)) + insertTestUser(t, db, ownerUserID, time.Now().Add(-24*time.Hour)) + insertTestFamily(t, db, familyID, ownerUserID) + insertTestFamilyMember(t, db, familyID, ownerUserID, user.FamilyRoleOwner, user.FamilyMemberActive, "owner_init") + insertTestFamilyMember(t, db, familyID, memberID, user.FamilyRoleMember, user.FamilyMemberActive, "manual_invite") + + insertRenewalUserSubscribe(t, db, memberSubID, memberID, 7448, planID, "member-token-10013", "member-uuid-10013", + time.Date(2026, 4, 23, 19, 6, 40, 0, time.UTC), + time.Date(2026, 4, 30, 19, 6, 40, 0, time.UTC), + ) + insertRenewalUserSubscribe(t, db, ownerSubID, ownerUserID, 9999, planID, "owner-token-14074", "owner-uuid-14074", + time.Date(2026, 4, 30, 13, 39, 33, 0, time.UTC), + time.Date(2026, 5, 30, 16, 0, 0, 0, time.UTC), + ) + + resp, err := NewRenewalLogic(buildPurchaseCtx(member), svcCtx).Renewal(&types.RenewalOrderRequest{ + UserSubscribeID: memberSubID, + Payment: paymentID, + Quantity: 30, + }) + require.NoError(t, err) + require.NotNil(t, resp) + + var created modelOrder.Order + require.NoError(t, db.Where("order_no = ?", resp.OrderNo).First(&created).Error) + assert.Equal(t, memberID, created.UserId) + assert.Equal(t, ownerUserID, created.SubscriptionUserId) + assert.Equal(t, int64(9999), created.ParentId) + assert.Equal(t, "owner-token-14074", created.SubscribeToken) +} + func TestPreCreateOrder_NewUserOnly_BindEmailScopeUsesEarliestDeviceTime(t *testing.T) { db := setupNewUserOnlyDB(t) rds, mr := setupNewUserOnlyRedis(t) diff --git a/internal/logic/public/order/renewalLogic.go b/internal/logic/public/order/renewalLogic.go index 8d10023..684b0a9 100644 --- a/internal/logic/public/order/renewalLogic.go +++ b/internal/logic/public/order/renewalLogic.go @@ -41,6 +41,67 @@ func NewRenewalLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RenewalLo } } +func (l *RenewalLogic) resolveRenewalTargetSubscribe(requested *user.SubscribeDetails, entitlement *commonLogic.EntitlementContext, currentUserID int64) (*user.SubscribeDetails, error) { + if requested == nil { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "requested user subscribe is empty") + } + + effectiveUserID := currentUserID + if entitlement != nil && entitlement.EffectiveUserID > 0 { + effectiveUserID = entitlement.EffectiveUserID + } + + if effectiveUserID == currentUserID { + if requested.UserId != currentUserID { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "user subscribe does not belong to current user") + } + return requested, nil + } + + if requested.UserId != currentUserID && requested.UserId != effectiveUserID { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "user subscribe does not belong to current family entitlement") + } + if requested.UserId == effectiveUserID { + return requested, nil + } + + ownerSubscribe, err := l.findOwnerRenewalSubscribe(effectiveUserID, requested.SubscribeId) + if err != nil { + return nil, err + } + commonLogic.SubscriptionTraceInfo(l.Logger, commonLogic.SubscriptionTraceFlowOrder, "renewal_target_resolved", + "[SubscriptionFlow] renewal target redirected to family owner subscription", + logger.Field("user_id", currentUserID), + logger.Field("effective_user_id", effectiveUserID), + logger.Field("requested_user_subscribe_id", requested.Id), + logger.Field("requested_subscribe_owner_user_id", requested.UserId), + logger.Field("resolved_user_subscribe_id", ownerSubscribe.Id), + logger.Field("resolved_subscribe_owner_user_id", ownerSubscribe.UserId), + logger.Field("subscribe_id", requested.SubscribeId), + ) + return ownerSubscribe, nil +} + +func (l *RenewalLogic) findOwnerRenewalSubscribe(ownerUserID, subscribeID int64) (*user.SubscribeDetails, error) { + var target user.SubscribeDetails + err := l.svcCtx.DB.WithContext(l.ctx). + Model(&user.Subscribe{}). + Preload("Subscribe"). + Where("user_id = ? AND subscribe_id = ? AND token != ''", ownerUserID, subscribeID). + Where("status IN ?", []int64{0, 1, 2, 3}). + Order("expire_time DESC"). + Order("updated_at DESC"). + Order("id DESC"). + First(&target).Error + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "owner subscribe not found for renewal") + } + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find owner subscribe error: %v", err.Error()) + } + return &target, nil +} + // Renewal processes subscription renewal orders including discount calculation, // coupon validation, gift amount deduction, fee calculation, and order creation func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.RenewalOrderResponse, err error) { @@ -77,11 +138,15 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene } orderNo := tool.GenerateTradeNo() - // find user subscribe - userSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeID) + // find requested user subscribe, then resolve it to the real entitlement owner. + requestedSubscribe, err := l.svcCtx.UserModel.FindOneUserSubscribe(l.ctx, req.UserSubscribeID) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find user subscribe error: %v", err.Error()) } + userSubscribe, err := l.resolveRenewalTargetSubscribe(requestedSubscribe, entitlement, u.Id) + if err != nil { + return nil, err + } // find subscription sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, userSubscribe.SubscribeId) if err != nil { @@ -251,7 +316,9 @@ func (l *RenewalLogic) Renewal(req *types.RenewalOrderRequest) (resp *types.Rene "[SubscriptionFlow] renewal order persisted", append(commonLogic.OrderTraceFields(&orderInfo), logger.Field("requested_user_subscribe_id", req.UserSubscribeID), + logger.Field("requested_subscribe_owner_user_id", requestedSubscribe.UserId), logger.Field("resolved_user_subscribe_id", userSubscribe.Id), + logger.Field("resolved_subscribe_owner_user_id", userSubscribe.UserId), )..., ) // Deferred task