package pointsmall import ( "errors" "testing" "hfb_sys/backend/internal/database" "hfb_sys/backend/internal/model" ) func TestRedeemDeductsAvailablePointsAndStock(t *testing.T) { db := database.NewTestDB() user := model.User{Phone: "13900009601", PointsAvailable: 500} if err := db.Create(&user).Error; err != nil { t.Fatalf("create user failed: %v", err) } product := model.PointsMallProduct{ Title: "测试兑换码", Description: "测试商品", RedemptionInstructions: "联系客服领取", PointsPrice: 100, Stock: 2, PerUserLimit: 2, Status: "online", } if err := db.Create(&product).Error; err != nil { t.Fatalf("create product failed: %v", err) } repo := NewRepository(db) req := RedeemRequest{ Quantity: 2, ContactName: "张三", ContactPhone: "13800138000", CustomerRemark: "请发送到邮箱", } invalidReq := req invalidReq.ContactPhone = "123" if _, err := repo.Redeem(t.Context(), user.ID, product.ID, invalidReq); !errors.Is(err, ErrInvalidInput) { t.Fatalf("invalid contact phone error = %v, want ErrInvalidInput", err) } redemption, err := repo.Redeem(t.Context(), user.ID, product.ID, req) if err != nil { t.Fatalf("redeem failed: %v", err) } if redemption.TotalPoints != 200 { t.Fatalf("total points = %d, want 200", redemption.TotalPoints) } if redemption.ContactName != req.ContactName || redemption.ContactPhone != req.ContactPhone || redemption.CustomerRemark != req.CustomerRemark { t.Fatalf("redemption contact = %+v, want %+v", redemption, req) } var savedUser model.User if err := db.First(&savedUser, user.ID).Error; err != nil { t.Fatalf("find user failed: %v", err) } if savedUser.PointsAvailable != 300 || savedUser.RenterGrowthPoints != 0 { t.Fatalf("points after redeem = available %d lifetime %d", savedUser.PointsAvailable, savedUser.RenterGrowthPoints) } var savedProduct model.PointsMallProduct if err := db.First(&savedProduct, product.ID).Error; err != nil { t.Fatalf("find product failed: %v", err) } if savedProduct.Stock != 0 || savedProduct.RedeemedCount != 2 { t.Fatalf("product stock/redeemed = %d/%d", savedProduct.Stock, savedProduct.RedeemedCount) } req.Quantity = 1 if _, err := repo.Redeem(t.Context(), user.ID, product.ID, req); !errors.Is(err, ErrRedeemLimitExceeded) && !errors.Is(err, ErrProductUnavailable) { t.Fatalf("repeat redeem error = %v, want limit or unavailable", err) } completed, err := repo.CompleteRedemption(t.Context(), 99, redemption.ID, "已发放") if err != nil { t.Fatalf("complete redemption failed: %v", err) } if completed.Status != "completed" { t.Fatalf("redemption status = %s, want completed", completed.Status) } } func TestSummaryIncludesGrowthLevelRules(t *testing.T) { db := database.NewTestDB() user := model.User{ Phone: "13900009602", RenterGrowthPoints: 1000, PointsAvailable: 432, } if err := db.Create(&user).Error; err != nil { t.Fatalf("create user failed: %v", err) } summary, err := NewRepository(db).Summary(t.Context(), user.ID) if err != nil { t.Fatalf("summary failed: %v", err) } if !summary.GrowthEnabled || len(summary.Levels) != 4 { t.Fatalf("growth config = enabled %v levels %d", summary.GrowthEnabled, len(summary.Levels)) } if summary.LevelCode != "diamond" || summary.PointsMultiplierBps != 15_000 { t.Fatalf( "current level = %s multiplier %d, want diamond/15000", summary.LevelCode, summary.PointsMultiplierBps, ) } if summary.Levels[2].DiscountBps != 9800 || summary.Levels[2].DepositFreeQuotaCent != 100_000 { t.Fatalf("diamond rules = %+v", summary.Levels[2]) } }