package acceptance import ( "encoding/json" "fmt" "testing" "time" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/response" "github.com/break/junhong_cmp_fiber/tests/testutils/integ" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // ============================================================ // 验收测试:佣金计算重构 // 来源:openspec/changes/refactor-one-time-commission-allocation/specs/commission-calculation/spec.md // 来源:openspec/changes/refactor-one-time-commission-allocation/specs/commission-trigger/spec.md // ============================================================ func TestCommissionCalculation_SeriesAllocationQuery_Acceptance(t *testing.T) { env := integ.NewIntegrationTestEnv(t) parentShop := env.CreateTestShop("一级代理", 1, nil) childShop := env.CreateTestShop("二级代理", 2, &parentShop.ID) series := createCommissionTestSeries(t, env, "佣金测试系列") createPlatformSeriesAllocationForCommission(t, env, parentShop.ID, series.ID, 10000) createSeriesAllocationForCommission(t, env, parentShop.ID, childShop.ID, series.ID, 5000) // ------------------------------------------------------------ // Scenario: 直接查询系列分配 // GIVEN: 存在 shop_id + series_id 的系列分配记录 // WHEN: 通过 shop_id 和 series_id 查询 // THEN: 返回唯一匹配的记录,包含 one_time_commission_amount // // 破坏点:如果查询 API 不支持 series_id 筛选,此测试将失败 // ------------------------------------------------------------ t.Run("Scenario_直接查询系列分配", func(t *testing.T) { path := fmt.Sprintf("/api/admin/shop-series-allocations?shop_id=%d&series_id=%d", childShop.ID, series.ID) resp, err := env.AsSuperAdmin().Request("GET", path, nil) require.NoError(t, err) defer resp.Body.Close() assert.Equal(t, 200, resp.StatusCode) var result response.Response err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) assert.Equal(t, 0, result.Code) data := result.Data.(map[string]interface{}) items := data["items"].([]interface{}) require.Len(t, items, 1, "应返回唯一匹配记录") allocation := items[0].(map[string]interface{}) assert.Equal(t, float64(5000), allocation["one_time_commission_amount"], "佣金金额应为 5000 分") }) // ------------------------------------------------------------ // Scenario: 系列分配不存在 // GIVEN: shop_id + series_id 组合不存在分配记录 // WHEN: 查询该组合 // THEN: 返回空列表 // // 破坏点:如果查询不正确处理空结果,此测试将失败 // ------------------------------------------------------------ t.Run("Scenario_系列分配不存在", func(t *testing.T) { path := fmt.Sprintf("/api/admin/shop-series-allocations?shop_id=%d&series_id=99999", childShop.ID) resp, err := env.AsSuperAdmin().Request("GET", path, nil) require.NoError(t, err) defer resp.Body.Close() assert.Equal(t, 200, resp.StatusCode) var result response.Response err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) data := result.Data.(map[string]interface{}) list := data["items"].([]interface{}) assert.Empty(t, list, "不存在的组合应返回空列表") }) } func TestCommissionCalculation_EnableOneTimeCommission_Acceptance(t *testing.T) { env := integ.NewIntegrationTestEnv(t) series := createCommissionTestSeriesWithConfig(t, env, "启用佣金系列", true) // ------------------------------------------------------------ // Scenario: 检查系列是否启用一次性佣金 // GIVEN: 系列配置 enable_one_time_commission = true // WHEN: 查询系列详情 // THEN: 响应包含 enable_one_time_commission = true // // 破坏点:如果系列 API 不返回 enable_one_time_commission 字段,此测试将失败 // ------------------------------------------------------------ t.Run("Scenario_检查系列是否启用一次性佣金", func(t *testing.T) { path := fmt.Sprintf("/api/admin/package-series/%d", series.ID) resp, err := env.AsSuperAdmin().Request("GET", path, nil) require.NoError(t, err) defer resp.Body.Close() assert.Equal(t, 200, resp.StatusCode) var result response.Response err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) assert.Equal(t, 0, result.Code) data := result.Data.(map[string]interface{}) enableOneTime, ok := data["enable_one_time_commission"] assert.True(t, ok, "响应应包含 enable_one_time_commission 字段") assert.Equal(t, true, enableOneTime, "应为 true") }) // ------------------------------------------------------------ // Scenario: 批量查询启用一次性佣金的系列 // GIVEN: 存在多个系列,部分启用一次性佣金 // WHEN: 查询系列列表并按 enable_one_time_commission 筛选 // THEN: 返回符合条件的系列 // // 破坏点:如果不支持 enable_one_time_commission 筛选,此测试将失败 // ------------------------------------------------------------ t.Run("Scenario_批量查询启用一次性佣金的系列", func(t *testing.T) { createCommissionTestSeriesWithConfig(t, env, "禁用佣金系列", false) resp, err := env.AsSuperAdmin(). Request("GET", "/api/admin/package-series?enable_one_time_commission=true", nil) require.NoError(t, err) defer resp.Body.Close() assert.Equal(t, 200, resp.StatusCode) var result response.Response err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) data := result.Data.(map[string]interface{}) list := data["items"].([]interface{}) for _, item := range list { seriesItem := item.(map[string]interface{}) enableVal, hasField := seriesItem["enable_one_time_commission"] if hasField { assert.Equal(t, true, enableVal, "筛选结果应全部为启用状态") } } }) } func TestCommissionCalculation_ChainAllocation_Acceptance(t *testing.T) { env := integ.NewIntegrationTestEnv(t) level1Shop := env.CreateTestShop("一级代理", 1, nil) level2Shop := env.CreateTestShop("二级代理", 2, &level1Shop.ID) level3Shop := env.CreateTestShop("三级代理", 3, &level2Shop.ID) series := createCommissionTestSeries(t, env, "链式分配系列") // ------------------------------------------------------------ // Scenario: 链式分配金额计算 // GIVEN: // - 平台给一级:one_time_commission_amount = 10000(100元) // - 一级给二级:one_time_commission_amount = 8000(80元) // - 二级给三级:one_time_commission_amount = 5000(50元) // WHEN: 查询各级的系列分配 // THEN: 各级金额正确 // // 破坏点:如果分配金额不正确保存,此测试将失败 // ------------------------------------------------------------ t.Run("Scenario_链式分配金额计算", func(t *testing.T) { createPlatformSeriesAllocationForCommission(t, env, level1Shop.ID, series.ID, 10000) createSeriesAllocationForCommission(t, env, level1Shop.ID, level2Shop.ID, series.ID, 8000) createSeriesAllocationForCommission(t, env, level2Shop.ID, level3Shop.ID, series.ID, 5000) verifyAllocationAmount(t, env, level1Shop.ID, series.ID, 10000) verifyAllocationAmount(t, env, level2Shop.ID, series.ID, 8000) verifyAllocationAmount(t, env, level3Shop.ID, series.ID, 5000) }) // ------------------------------------------------------------ // Scenario: 单级代理 // GIVEN: 一级代理直接销售(无下级) // WHEN: 查询一级的系列分配 // THEN: 一级获得完整的 one_time_commission_amount // // 破坏点:如果单级分配不生效,此测试将失败 // ------------------------------------------------------------ t.Run("Scenario_单级代理", func(t *testing.T) { singleShop := env.CreateTestShop("单级代理", 1, nil) singleSeries := createCommissionTestSeries(t, env, "单级系列") createPlatformSeriesAllocationForCommission(t, env, singleShop.ID, singleSeries.ID, 10000) verifyAllocationAmount(t, env, singleShop.ID, singleSeries.ID, 10000) }) } func TestCommissionCalculation_TriggerConfig_Acceptance(t *testing.T) { env := integ.NewIntegrationTestEnv(t) parentShop := env.CreateTestShop("一级代理", 1, nil) childShop := env.CreateTestShop("二级代理", 2, &parentShop.ID) series := createCommissionTestSeries(t, env, "触发配置系列") createPlatformSeriesAllocationForCommission(t, env, parentShop.ID, series.ID, 10000) // ------------------------------------------------------------ // Scenario: 累计达到阈值触发佣金配置 // GIVEN: 系列分配设置为累计充值触发,阈值 1000 元 // WHEN: 创建系列分配 // THEN: 配置正确保存 // // 破坏点:如果触发配置不保存,此测试将失败 // ------------------------------------------------------------ t.Run("Scenario_累计达到阈值触发佣金配置", func(t *testing.T) { body := map[string]interface{}{ "shop_id": childShop.ID, "series_id": series.ID, "one_time_commission_amount": 5000, "enable_one_time_commission": true, "one_time_commission_trigger": "accumulated_recharge", "one_time_commission_threshold": 100000, } jsonBody, _ := json.Marshal(body) resp, err := env.AsSuperAdmin(). Request("POST", "/api/admin/shop-series-allocations", jsonBody) require.NoError(t, err) defer resp.Body.Close() assert.Equal(t, 200, resp.StatusCode) var result response.Response err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) assert.Equal(t, 0, result.Code) data := result.Data.(map[string]interface{}) assert.Equal(t, true, data["enable_one_time_commission"]) assert.Equal(t, "accumulated_recharge", data["one_time_commission_trigger"]) assert.Equal(t, float64(100000), data["one_time_commission_threshold"]) }) // ------------------------------------------------------------ // Scenario: 首次充值触发配置 // GIVEN: 系列分配设置为首次充值触发,阈值 100 元 // WHEN: 创建系列分配 // THEN: 配置正确保存 // // 破坏点:如果 first_recharge 触发类型不支持,此测试将失败 // ------------------------------------------------------------ t.Run("Scenario_首次充值触发配置", func(t *testing.T) { newChildShop := env.CreateTestShop("首充测试店铺", 2, &parentShop.ID) body := map[string]interface{}{ "shop_id": newChildShop.ID, "series_id": series.ID, "one_time_commission_amount": 5000, "enable_one_time_commission": true, "one_time_commission_trigger": "first_recharge", "one_time_commission_threshold": 10000, } jsonBody, _ := json.Marshal(body) resp, err := env.AsSuperAdmin(). Request("POST", "/api/admin/shop-series-allocations", jsonBody) require.NoError(t, err) defer resp.Body.Close() assert.Equal(t, 200, resp.StatusCode) var result response.Response err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) assert.Equal(t, 0, result.Code) data := result.Data.(map[string]interface{}) assert.Equal(t, "first_recharge", data["one_time_commission_trigger"]) }) } func TestCommissionStats_Allocation_Acceptance(t *testing.T) { env := integ.NewIntegrationTestEnv(t) parentShop := env.CreateTestShop("一级代理", 1, nil) series := createCommissionTestSeries(t, env, "统计测试系列") // ------------------------------------------------------------ // Scenario: 创建佣金统计记录关联系列分配 // GIVEN: 存在系列分配记录 // WHEN: 查询佣金统计 // THEN: 统计记录的 allocation_id 指向 ShopSeriesAllocation.id // // 破坏点:如果统计不关联系列分配,此测试将失败 // ------------------------------------------------------------ t.Run("Scenario_佣金统计关联系列分配", func(t *testing.T) { allocation := createPlatformSeriesAllocationForCommission(t, env, parentShop.ID, series.ID, 10000) path := fmt.Sprintf("/api/admin/shop-series-commission-stats?shop_id=%d&series_id=%d", parentShop.ID, series.ID) resp, err := env.AsSuperAdmin().Request("GET", path, nil) require.NoError(t, err) defer resp.Body.Close() if resp.StatusCode == 200 { var result response.Response err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) if result.Code == 0 && result.Data != nil { data := result.Data.(map[string]interface{}) if list, ok := data["items"].([]interface{}); ok && len(list) > 0 { stats := list[0].(map[string]interface{}) if allocationID, exists := stats["allocation_id"]; exists { assert.Equal(t, float64(allocation.ID), allocationID, "统计应关联到系列分配 ID") } } } } }) } // ============================================================ // 辅助函数 // ============================================================ func createCommissionTestSeries(t *testing.T, env *integ.IntegrationTestEnv, name string) *model.PackageSeries { t.Helper() timestamp := time.Now().UnixNano() series := &model.PackageSeries{ SeriesCode: fmt.Sprintf("COMM_SERIES_%d", timestamp), SeriesName: name, Status: constants.StatusEnabled, BaseModel: model.BaseModel{ Creator: 1, Updater: 1, }, } err := env.TX.Create(series).Error require.NoError(t, err, "创建测试系列失败") return series } func createCommissionTestSeriesWithConfig(t *testing.T, env *integ.IntegrationTestEnv, name string, enableOneTime bool) *model.PackageSeries { t.Helper() timestamp := time.Now().UnixNano() series := &model.PackageSeries{ SeriesCode: fmt.Sprintf("COMM_SERIES_%d", timestamp), SeriesName: name, Status: constants.StatusEnabled, EnableOneTimeCommission: enableOneTime, BaseModel: model.BaseModel{ Creator: 1, Updater: 1, }, } err := env.TX.Create(series).Error require.NoError(t, err, "创建测试系列失败") return series } func createPlatformSeriesAllocationForCommission(t *testing.T, env *integ.IntegrationTestEnv, shopID, seriesID uint, amount int64) *model.ShopSeriesAllocation { t.Helper() allocation := &model.ShopSeriesAllocation{ ShopID: shopID, SeriesID: seriesID, AllocatorShopID: 0, OneTimeCommissionAmount: amount, Status: constants.StatusEnabled, BaseModel: model.BaseModel{ Creator: 1, Updater: 1, }, } err := env.TX.Create(allocation).Error require.NoError(t, err, "创建平台系列分配失败") return allocation } func createSeriesAllocationForCommission(t *testing.T, env *integ.IntegrationTestEnv, allocatorShopID, shopID, seriesID uint, amount int64) *model.ShopSeriesAllocation { t.Helper() allocation := &model.ShopSeriesAllocation{ ShopID: shopID, SeriesID: seriesID, AllocatorShopID: allocatorShopID, OneTimeCommissionAmount: amount, Status: constants.StatusEnabled, BaseModel: model.BaseModel{ Creator: 1, Updater: 1, }, } err := env.TX.Create(allocation).Error require.NoError(t, err, "创建系列分配失败") return allocation } func verifyAllocationAmount(t *testing.T, env *integ.IntegrationTestEnv, shopID, seriesID uint, expectedAmount int64) { t.Helper() path := fmt.Sprintf("/api/admin/shop-series-allocations?shop_id=%d&series_id=%d", shopID, seriesID) resp, err := env.AsSuperAdmin().Request("GET", path, nil) require.NoError(t, err) defer resp.Body.Close() require.Equal(t, 200, resp.StatusCode) var result response.Response err = json.NewDecoder(resp.Body).Decode(&result) require.NoError(t, err) require.Equal(t, 0, result.Code) data := result.Data.(map[string]interface{}) list := data["items"].([]interface{}) require.NotEmpty(t, list, "应存在分配记录") allocation := list[0].(map[string]interface{}) assert.Equal(t, float64(expectedAmount), allocation["one_time_commission_amount"], "店铺 %d 系列 %d 的佣金金额应为 %d", shopID, seriesID, expectedAmount) }