vip体验卡问题
This commit is contained in:
parent
4fb1a69353
commit
42e4adab56
@ -113,7 +113,7 @@ const (
|
|||||||
VIPLevelNotFound Code = "VIP_LEVEL_NOT_FOUND"
|
VIPLevelNotFound Code = "VIP_LEVEL_NOT_FOUND"
|
||||||
// VIPLevelDisabled 表示目标 VIP 等级当前不可购买。
|
// VIPLevelDisabled 表示目标 VIP 等级当前不可购买。
|
||||||
VIPLevelDisabled Code = "VIP_LEVEL_DISABLED"
|
VIPLevelDisabled Code = "VIP_LEVEL_DISABLED"
|
||||||
// VIPDowngradeNotAllowed 表示用户当前有效 VIP 等级高于本次购买目标等级。
|
// VIPDowngradeNotAllowed 表示用户当前有效 VIP 等级高于购买或体验卡的目标等级。
|
||||||
VIPDowngradeNotAllowed Code = "VIP_DOWNGRADE_NOT_ALLOWED"
|
VIPDowngradeNotAllowed Code = "VIP_DOWNGRADE_NOT_ALLOWED"
|
||||||
// VIPProgramInactive 表示当前 App 未启用可执行的 VIP 体系。
|
// VIPProgramInactive 表示当前 App 未启用可执行的 VIP 体系。
|
||||||
VIPProgramInactive Code = "VIP_PROGRAM_INACTIVE"
|
VIPProgramInactive Code = "VIP_PROGRAM_INACTIVE"
|
||||||
|
|||||||
@ -171,7 +171,8 @@ func (s *Service) GrantVipTrialCard(ctx context.Context, command ledger.GrantVip
|
|||||||
return s.repository.GrantVipTrialCard(ctx, command)
|
return s.repository.GrantVipTrialCard(ctx, command)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EquipVipTrialCard 只切换通用装备指针,任一卡片的 effective/expires 时间都不会变化。
|
// EquipVipTrialCard 只切换通用装备指针,任一卡片的 effective/expires 时间都不会变化;
|
||||||
|
// repository 会在同一事务内拒绝低于当前 paid/trial 最终等级的体验卡。
|
||||||
func (s *Service) EquipVipTrialCard(ctx context.Context, command ledger.EquipVipTrialCardCommand) (ledger.VipTrialCard, ledger.VipState, error) {
|
func (s *Service) EquipVipTrialCard(ctx context.Context, command ledger.EquipVipTrialCardCommand) (ledger.VipTrialCard, ledger.VipState, error) {
|
||||||
command.RequestID = strings.TrimSpace(command.RequestID)
|
command.RequestID = strings.TrimSpace(command.RequestID)
|
||||||
command.EquipmentCommandID = strings.TrimSpace(command.EquipmentCommandID)
|
command.EquipmentCommandID = strings.TrimSpace(command.EquipmentCommandID)
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// TestFamiVIPReplaceAndTrialCardSwitch 使用真实 MySQL repository 验证 P1 最容易回归的三条状态边界:
|
// TestFamiVIPReplaceAndTrialCardSwitch 使用真实 MySQL repository 验证 P1 最容易回归的三条状态边界:
|
||||||
// 高级购买丢弃剩余时间、体验卡可跨等级自由切换且旧卡不改期、体验卡排除金币返现。
|
// 高级购买丢弃剩余时间、体验卡不能降低当前等级且旧卡不改期、体验卡排除金币返现。
|
||||||
func TestFamiVIPReplaceAndTrialCardSwitch(t *testing.T) {
|
func TestFamiVIPReplaceAndTrialCardSwitch(t *testing.T) {
|
||||||
repository := mysqltest.NewRepository(t)
|
repository := mysqltest.NewRepository(t)
|
||||||
service := walletservice.New(repository)
|
service := walletservice.New(repository)
|
||||||
@ -158,28 +158,28 @@ func TestFamiVIPReplaceAndTrialCardSwitch(t *testing.T) {
|
|||||||
|
|
||||||
resourceEventsBefore := repository.CountRows("wallet_outbox", "app_code = ? AND user_id = ? AND event_type = ?", "fami", userID, "UserResourceChanged")
|
resourceEventsBefore := repository.CountRows("wallet_outbox", "app_code = ? AND user_id = ? AND event_type = ?", "fami", userID, "UserResourceChanged")
|
||||||
vipEventsBefore := repository.CountRows("wallet_outbox", "app_code = ? AND user_id = ? AND event_type = ?", "fami", userID, "VipEffectiveChanged")
|
vipEventsBefore := repository.CountRows("wallet_outbox", "app_code = ? AND user_id = ? AND event_type = ?", "fami", userID, "VipEffectiveChanged")
|
||||||
genericCard3, err := service.EquipUserResource(ctx, resourcedomain.EquipUserResourceCommand{
|
_, err = service.EquipUserResource(ctx, resourcedomain.EquipUserResourceCommand{
|
||||||
AppCode: "fami", RequestID: "generic-equip-trial-3", UserID: userID,
|
AppCode: "fami", RequestID: "generic-equip-trial-3", UserID: userID,
|
||||||
ResourceID: card3.TrialCard.ResourceID, EntitlementID: card3.TrialCard.EntitlementID,
|
ResourceID: card3.TrialCard.ResourceID, EntitlementID: card3.TrialCard.EntitlementID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if !xerr.IsCode(err, xerr.VIPDowngradeNotAllowed) {
|
||||||
t.Fatalf("generic backpack equip VIP3 trial card failed: %v", err)
|
t.Fatalf("generic backpack must reject VIP3 trial card below paid VIP6, got %v", err)
|
||||||
}
|
}
|
||||||
if !genericCard3.Equipped || genericCard3.EntitlementID != card3.TrialCard.EntitlementID {
|
if metadata := xerr.MetadataOf(err); metadata["current_effective_level"] != "6" || metadata["required_level"] != "6" {
|
||||||
t.Fatalf("generic backpack response must return the selected equipped entitlement: %+v", genericCard3)
|
t.Fatalf("lower trial rejection must expose current level metadata, got %+v", metadata)
|
||||||
}
|
}
|
||||||
if got := repository.CountRows("wallet_outbox", "app_code = ? AND user_id = ? AND event_type = ?", "fami", userID, "UserResourceChanged"); got != resourceEventsBefore+1 {
|
if got := repository.CountRows("wallet_outbox", "app_code = ? AND user_id = ? AND event_type = ?", "fami", userID, "UserResourceChanged"); got != resourceEventsBefore {
|
||||||
t.Fatalf("generic trial equip must publish UserResourceChanged exactly once: before=%d after=%d", resourceEventsBefore, got)
|
t.Fatalf("rejected lower trial must not publish UserResourceChanged: before=%d after=%d", resourceEventsBefore, got)
|
||||||
}
|
}
|
||||||
if got := repository.CountRows("wallet_outbox", "app_code = ? AND user_id = ? AND event_type = ?", "fami", userID, "VipEffectiveChanged"); got != vipEventsBefore+1 {
|
if got := repository.CountRows("wallet_outbox", "app_code = ? AND user_id = ? AND event_type = ?", "fami", userID, "VipEffectiveChanged"); got != vipEventsBefore {
|
||||||
t.Fatalf("generic trial equip must publish VipEffectiveChanged exactly once: before=%d after=%d", vipEventsBefore, got)
|
t.Fatalf("rejected lower trial must not publish VipEffectiveChanged: before=%d after=%d", vipEventsBefore, got)
|
||||||
}
|
}
|
||||||
state3, err := service.GetVipState(ctx, userID)
|
state3, err := service.GetVipState(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("get state after generic VIP3 trial equip failed: %v", err)
|
t.Fatalf("get state after rejected VIP3 trial equip failed: %v", err)
|
||||||
}
|
}
|
||||||
if state3.EffectiveSource != ledger.VipEffectiveSourceTrial || state3.EffectiveVip.Level != 3 || state3.PaidVip.Level != 6 {
|
if state3.EffectiveSource != ledger.VipEffectiveSourcePaid || state3.EffectiveVip.Level != 6 || state3.EquippedTrialCard != nil {
|
||||||
t.Fatalf("lower trial card must freely overlay paid VIP without destroying it: %+v", state3)
|
t.Fatalf("rejected lower trial card must preserve paid VIP6 without equipment: %+v", state3)
|
||||||
}
|
}
|
||||||
_, err = service.EquipUserResource(ctx, resourcedomain.EquipUserResourceCommand{
|
_, err = service.EquipUserResource(ctx, resourcedomain.EquipUserResourceCommand{
|
||||||
AppCode: "fami", RequestID: "generic-equip-trial-6", UserID: userID,
|
AppCode: "fami", RequestID: "generic-equip-trial-6", UserID: userID,
|
||||||
@ -188,6 +188,12 @@ func TestFamiVIPReplaceAndTrialCardSwitch(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("generic backpack switch to VIP6 trial card failed: %v", err)
|
t.Fatalf("generic backpack switch to VIP6 trial card failed: %v", err)
|
||||||
}
|
}
|
||||||
|
if got := repository.CountRows("wallet_outbox", "app_code = ? AND user_id = ? AND event_type = ?", "fami", userID, "UserResourceChanged"); got != resourceEventsBefore+1 {
|
||||||
|
t.Fatalf("accepted equal-level trial must publish UserResourceChanged exactly once: before=%d after=%d", resourceEventsBefore, got)
|
||||||
|
}
|
||||||
|
if got := repository.CountRows("wallet_outbox", "app_code = ? AND user_id = ? AND event_type = ?", "fami", userID, "VipEffectiveChanged"); got != vipEventsBefore+1 {
|
||||||
|
t.Fatalf("accepted equal-level trial must publish VipEffectiveChanged exactly once: before=%d after=%d", vipEventsBefore, got)
|
||||||
|
}
|
||||||
state6, err := service.GetVipState(ctx, userID)
|
state6, err := service.GetVipState(ctx, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("get state after generic VIP6 trial equip failed: %v", err)
|
t.Fatalf("get state after generic VIP6 trial equip failed: %v", err)
|
||||||
@ -195,14 +201,10 @@ func TestFamiVIPReplaceAndTrialCardSwitch(t *testing.T) {
|
|||||||
if state6.EffectiveVip.Level != 6 || state6.EquippedTrialCard == nil || state6.EquippedTrialCard.EntitlementID != card6.TrialCard.EntitlementID {
|
if state6.EffectiveVip.Level != 6 || state6.EquippedTrialCard == nil || state6.EquippedTrialCard.EntitlementID != card6.TrialCard.EntitlementID {
|
||||||
t.Fatalf("trial card switch mismatch: %+v", state6)
|
t.Fatalf("trial card switch mismatch: %+v", state6)
|
||||||
}
|
}
|
||||||
card3Again, _, err := service.EquipVipTrialCard(ctx, ledger.EquipVipTrialCardCommand{
|
if _, _, err := service.EquipVipTrialCard(ctx, ledger.EquipVipTrialCardCommand{
|
||||||
AppCode: "fami", RequestID: "equip-trial-3-again", UserID: userID, EntitlementID: card3.TrialCard.EntitlementID,
|
AppCode: "fami", RequestID: "equip-trial-3-again", UserID: userID, EntitlementID: card3.TrialCard.EntitlementID,
|
||||||
})
|
}); !xerr.IsCode(err, xerr.VIPDowngradeNotAllowed) {
|
||||||
if err != nil {
|
t.Fatalf("VIP3 trial must not replace currently equipped VIP6 trial, got %v", err)
|
||||||
t.Fatalf("re-equip VIP3 trial card failed: %v", err)
|
|
||||||
}
|
|
||||||
if card3Again.ExpiresAtMS != card3.TrialCard.ExpiresAtMS {
|
|
||||||
t.Fatalf("switching cards must preserve the old card absolute expiry: first=%d again=%d", card3.TrialCard.ExpiresAtMS, card3Again.ExpiresAtMS)
|
|
||||||
}
|
}
|
||||||
if _, _, err := service.EquipVipTrialCard(ctx, ledger.EquipVipTrialCardCommand{
|
if _, _, err := service.EquipVipTrialCard(ctx, ledger.EquipVipTrialCardCommand{
|
||||||
AppCode: "fami", RequestID: "equip-trial-6-again", UserID: userID, EntitlementID: card6.TrialCard.EntitlementID,
|
AppCode: "fami", RequestID: "equip-trial-6-again", UserID: userID, EntitlementID: card6.TrialCard.EntitlementID,
|
||||||
|
|||||||
@ -589,6 +589,45 @@ func (r *Repository) ensureTieredVipResourceState(ctx context.Context, userID in
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if program.Status == ledger.VipStatusActive && program.TrialCardEnabled &&
|
||||||
|
state.PaidVip.Active && state.EquippedTrialCard != nil &&
|
||||||
|
state.EquippedTrialCard.Level < state.PaidVip.Level {
|
||||||
|
// 写门禁上线前可能已经留下“付费 VIP6 装备 VIP3 卡”的历史指针。这里按
|
||||||
|
// (app,user,type,entitlement) 主键范围精确解除,不销毁仍在有效期内的卡;
|
||||||
|
// 之后用户付费身份失效时仍可重新使用这张卡。
|
||||||
|
result, err := tx.ExecContext(ctx, `
|
||||||
|
DELETE FROM user_resource_equipment
|
||||||
|
WHERE app_code = ? AND user_id = ? AND resource_type = ? AND entitlement_id = ?`,
|
||||||
|
appcode.FromContext(ctx), userID, resourcedomain.TypeVIPTrialCard,
|
||||||
|
state.EquippedTrialCard.EntitlementID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
removed, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if removed > 0 {
|
||||||
|
cleanupCommandID := "vip_trial_lower_cleanup:" + stableHash(fmt.Sprintf(
|
||||||
|
"%s|%d|%s|%d|%d",
|
||||||
|
appcode.FromContext(ctx), userID, state.EquippedTrialCard.EntitlementID, state.PaidVip.Level, nowMS,
|
||||||
|
))
|
||||||
|
if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
|
||||||
|
resourceOutboxEvent("UserResourceChanged", cleanupCommandID, userID, state.EquippedTrialCard.ResourceID, map[string]any{
|
||||||
|
"action": "unequip_lower_vip_trial", "resource_type": resourcedomain.TypeVIPTrialCard,
|
||||||
|
"entitlement_id": state.EquippedTrialCard.EntitlementID, "updated_at_ms": nowMS,
|
||||||
|
}, nowMS),
|
||||||
|
resourceOutboxEvent("VipEffectiveChanged", cleanupCommandID, userID, state.EquippedTrialCard.ResourceID, map[string]any{
|
||||||
|
"source": ledger.VipEffectiveSourcePaid, "level": state.PaidVip.Level,
|
||||||
|
"updated_at_ms": nowMS,
|
||||||
|
}, nowMS),
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.EquippedTrialCard = nil
|
||||||
|
}
|
||||||
if program.Status == ledger.VipStatusActive && state.PaidVip.Active {
|
if program.Status == ledger.VipStatusActive && state.PaidVip.Active {
|
||||||
paid := state.PaidVip
|
paid := state.PaidVip
|
||||||
paid.ProgramType = program.ProgramType
|
paid.ProgramType = program.ProgramType
|
||||||
@ -742,7 +781,7 @@ func filterVipEquipmentByAccess(grouped map[int64][]resourcedomain.UserResourceE
|
|||||||
access := levels[userID]
|
access := levels[userID]
|
||||||
effectiveLevel := access.Paid
|
effectiveLevel := access.Paid
|
||||||
effectiveSource := ledger.VipEffectiveSourcePaid
|
effectiveSource := ledger.VipEffectiveSourcePaid
|
||||||
if trialCardEnabled && access.Trial > 0 {
|
if trialCardEnabled && access.Trial > 0 && access.Trial >= access.Paid {
|
||||||
effectiveLevel = access.Trial
|
effectiveLevel = access.Trial
|
||||||
effectiveSource = ledger.VipEffectiveSourceTrial
|
effectiveSource = ledger.VipEffectiveSourceTrial
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,7 +51,7 @@ func (r *Repository) GetVipProgramConfig(ctx context.Context) (ledger.VipProgram
|
|||||||
return config, benefits, err
|
return config, benefits, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetVipState 合并付费会员和当前佩戴体验卡。启用体验卡时不按等级限制切换;
|
// GetVipState 合并付费会员和当前佩戴体验卡。低等级切换由 EquipVipTrialCard 在写事务拦截;
|
||||||
// 卡片过期、卸下或 App 关闭体验卡能力后都会自然回落到仍有效的付费会员。
|
// 卡片过期、卸下或 App 关闭体验卡能力后都会自然回落到仍有效的付费会员。
|
||||||
func (r *Repository) GetVipState(ctx context.Context, userID int64) (ledger.VipState, error) {
|
func (r *Repository) GetVipState(ctx context.Context, userID int64) (ledger.VipState, error) {
|
||||||
if r == nil || r.db == nil {
|
if r == nil || r.db == nil {
|
||||||
@ -180,7 +180,7 @@ func (r *Repository) batchAllowedVipBenefits(ctx context.Context, userIDs []int6
|
|||||||
access := levels[userID]
|
access := levels[userID]
|
||||||
effectiveLevel := access.Paid
|
effectiveLevel := access.Paid
|
||||||
effectiveSource := ledger.VipEffectiveSourcePaid
|
effectiveSource := ledger.VipEffectiveSourcePaid
|
||||||
if program.TrialCardEnabled && access.Trial > 0 {
|
if program.TrialCardEnabled && access.Trial > 0 && access.Trial >= access.Paid {
|
||||||
effectiveLevel = access.Trial
|
effectiveLevel = access.Trial
|
||||||
effectiveSource = ledger.VipEffectiveSourceTrial
|
effectiveSource = ledger.VipEffectiveSourceTrial
|
||||||
}
|
}
|
||||||
@ -320,7 +320,9 @@ func (r *Repository) getVipStateFromSnapshot(ctx context.Context, querier vipSna
|
|||||||
state.EffectiveVip = paid
|
state.EffectiveVip = paid
|
||||||
state.EffectiveSource = ledger.VipEffectiveSourcePaid
|
state.EffectiveSource = ledger.VipEffectiveSourcePaid
|
||||||
}
|
}
|
||||||
if config.TrialCardEnabled && trial != nil {
|
// 等级相同的体验卡仍按产品规则临时替换 paid;更低等级的历史装备只保留 raw
|
||||||
|
// 事实,最终身份继续使用 paid,ensureTieredVipResourceState 会在写事务中解除旧指针。
|
||||||
|
if config.TrialCardEnabled && trial != nil && (!paid.Active || trial.Level >= paid.Level) {
|
||||||
state.EffectiveVip = ledger.UserVip{
|
state.EffectiveVip = ledger.UserVip{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Level: trial.Level,
|
Level: trial.Level,
|
||||||
|
|||||||
@ -269,7 +269,8 @@ func (r *Repository) vipTrialCardReceiptForCommand(ctx context.Context, command
|
|||||||
return ledger.GrantVipTrialCardReceipt{TrialCard: card, State: state, ServerTimeMS: nowMS}, nil
|
return ledger.GrantVipTrialCardReceipt{TrialCard: card, State: state, ServerTimeMS: nowMS}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// EquipVipTrialCard 只更新 vip_trial_card 单选装备槽。目标 entitlement 必须属于当前用户且仍在绝对有效期内。
|
// EquipVipTrialCard 只更新 vip_trial_card 单选装备槽。目标 entitlement 必须属于当前用户、
|
||||||
|
// 仍在绝对有效期内且等级不低于用户当前 paid/trial 最终等级。
|
||||||
func (r *Repository) EquipVipTrialCard(ctx context.Context, command ledger.EquipVipTrialCardCommand) (ledger.VipTrialCard, ledger.VipState, error) {
|
func (r *Repository) EquipVipTrialCard(ctx context.Context, command ledger.EquipVipTrialCardCommand) (ledger.VipTrialCard, ledger.VipState, error) {
|
||||||
if r == nil || r.db == nil {
|
if r == nil || r.db == nil {
|
||||||
return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
@ -308,6 +309,24 @@ func (r *Repository) EquipVipTrialCard(ctx context.Context, command ledger.Equip
|
|||||||
if program.Status != ledger.VipStatusActive || !program.TrialCardEnabled {
|
if program.Status != ledger.VipStatusActive || !program.TrialCardEnabled {
|
||||||
return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.VIPProgramInactive, "vip trial card is disabled for this app")
|
return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.VIPProgramInactive, "vip trial card is disabled for this app")
|
||||||
}
|
}
|
||||||
|
// membership 主键行既提供付费 VIP 快照,也作为同一用户体验卡切换的稳定串行化点。
|
||||||
|
// 即使 equipment 尚无记录,并发 VIP6/VIP3 佩戴也会按此锁排队,后到的低等级卡
|
||||||
|
// 必须看到先提交的高等级卡,不能再次把最终 VIP 覆盖回低等级。
|
||||||
|
currentPaid, err := r.ensureUserVipForUpdate(ctx, tx, command.UserID, nowMS)
|
||||||
|
if err != nil {
|
||||||
|
return ledger.VipTrialCard{}, ledger.VipState{}, err
|
||||||
|
}
|
||||||
|
currentEffectiveLevel := int32(0)
|
||||||
|
if currentPaid.Active {
|
||||||
|
currentEffectiveLevel = currentPaid.Level
|
||||||
|
}
|
||||||
|
currentTrial, err := r.queryEquippedVipTrialCardWithQuery(ctx, tx, command.UserID, nowMS)
|
||||||
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return ledger.VipTrialCard{}, ledger.VipState{}, err
|
||||||
|
}
|
||||||
|
if err == nil && currentTrial.Level > currentEffectiveLevel {
|
||||||
|
currentEffectiveLevel = currentTrial.Level
|
||||||
|
}
|
||||||
// 锁序必须与 RevokeResourceGrant 保持 entitlement -> trial_card;反向先锁 card 会和后台撤销形成死锁。
|
// 锁序必须与 RevokeResourceGrant 保持 entitlement -> trial_card;反向先锁 card 会和后台撤销形成死锁。
|
||||||
// entitlement 同时承担并发有效性门禁,避免撤销在校验后抢先提交又被重新写回 equipment。
|
// entitlement 同时承担并发有效性门禁,避免撤销在校验后抢先提交又被重新写回 equipment。
|
||||||
entitlement, err := r.getUserResourceEntitlementForUpdateTx(ctx, tx, command.EntitlementID)
|
entitlement, err := r.getUserResourceEntitlementForUpdateTx(ctx, tx, command.EntitlementID)
|
||||||
@ -335,6 +354,17 @@ func (r *Repository) EquipVipTrialCard(ctx context.Context, command ledger.Equip
|
|||||||
if resourcedomain.NormalizeResourceType(entitlement.Resource.ResourceType) != resourcedomain.TypeVIPTrialCard {
|
if resourcedomain.NormalizeResourceType(entitlement.Resource.ResourceType) != resourcedomain.TypeVIPTrialCard {
|
||||||
return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.Conflict, "vip trial card resource type mismatch")
|
return ledger.VipTrialCard{}, ledger.VipState{}, xerr.New(xerr.Conflict, "vip trial card resource type mismatch")
|
||||||
}
|
}
|
||||||
|
if card.Level < currentEffectiveLevel {
|
||||||
|
level := fmt.Sprintf("%d", currentEffectiveLevel)
|
||||||
|
return ledger.VipTrialCard{}, ledger.VipState{}, xerr.NewWithMetadata(
|
||||||
|
xerr.VIPDowngradeNotAllowed,
|
||||||
|
"vip trial card level is below current effective vip",
|
||||||
|
map[string]string{
|
||||||
|
"current_effective_level": level,
|
||||||
|
"required_level": level,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
var currentEntitlementID string
|
var currentEntitlementID string
|
||||||
err = tx.QueryRowContext(ctx, `
|
err = tx.QueryRowContext(ctx, `
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user